认证与鉴权
1、Token的作用及原理
Token,即令牌,是服务器产生的,具有随机性和不可预测性,它主要有两个作用:
(1) 防止表单的重复提交:
1.防止页面提交表当重复提交的简单步骤:
(1,使用form 表单)
- 在服务器端生成 唯一的随机标识号,Token,同时在当前session中保存(真正的项目中要考虑存在缓存中,redis enchae 缓存)
- 将token发送至客户端,在form表单中来隐藏存储这个Token,表单提交时候连带着Token一起提交道服务器端
- 在服务器端进行验证提交过来的token是否是一致的,如果不一致、或前端为空或者后端为空,那么就是重复提交,如果相同清楚session中的重复的标识号
2、具体代码(jsp):
FromServlet.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class FormServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String token = TokenUtil.getInstance().makeToken(); System.out.println("在FormServlet中生成的token:"+token); request.getSession().setAttribute("token", token); request.getRequestDispatcher("/Form.jsp").forward(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } } }
|
Form.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>Form.jsp</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body> <form action="DoFormServlet" method="post"> <%--使用隐藏域存储生成的token--%> <%-- <input type="hidden" name="token" value="<%=session.getAttribute("token") %>"> --%> <%--使用EL表达式取出存储在session中的token--%> <input type="hidden" name="token" value="${token}" /> 用户名:<input type="text" name="username"> <input type="submit" value="提交"> </form> </body> </html>
|
TokenUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; import sun.misc.BASE64Encoder;
public class TokenUtil {
private TokenUtil(){ }
private static final TokenUtil instance = new TokenUtil();
public static TokenUtil getInstance(){ return instance; }
public String makeToken(){ String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + ""; try { MessageDigest md = MessageDigest.getInstance("md5"); byte md5[] = md.digest(token.getBytes()); BASE64Encoder encoder = new BASE64Encoder(); return encoder.encode(md5); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }
|
DoFormServlet.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class DoFormServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { boolean isRepeat = isRepeatSubmit(request); if(isRepeat){ request.setAttribute("MSG", "请不要重复提交"); System.out.println("请不要重复提交"); request.getRequestDispatcher("/Msg.jsp").forward(request, response); return; } request.getSession().removeAttribute("token"); request.setAttribute("MSG","处理用户提交请求!!"); request.getRequestDispatcher("/Msg.jsp").forward(request, response); System.out.println("处理用户提交请求!!"); }
private boolean isRepeatSubmit(HttpServletRequest request) { String client_token = request.getParameter("token"); if(client_token==null){ return true; } String server_token = (String) request.getSession().getAttribute("token"); if(server_token==null){ return true; } if(!client_token.equals(server_token)){ return true; } return false; } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } }
|
Msg.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>result show</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <!-- <link rel="stylesheet" type="text/css" href="styles.css"> --> </head> <body>${MSG}</body> </html>
|
(2) 用于身份认证(Cookie、Session和Token认证):
前言: HTTP是一种无状态的协议,与服务其进行连接及断开,为了辨别这个请求是谁进行发起的,需要浏览器自己去解决,用户通过浏览器登录一个网站,在该浏览器器页面,不许要进行重新登录。而http是无状态的协议,那么网站后端是如何进行判断用户已经登录?
详情:(3条消息) Cookie、Session和Token认证_谢公子的博客-CSDN博客_cookie认证
(3条消息) Token ,Cookie和Session的区别–学习笔记_CharliChen ‘s Blog-CSDN博客
基于Token 认证和session 认证的比较 - 想飞_毛毛虫 - 博客园 (cnblogs.com)
两者的区别:
1.使用session 进行存储
服务端生成用户相关的 session 数据, 发给客户端 sesssion_id 存放到 cookie ,客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,
缺点: 服务端需要存储session ,会增加维护成本和减弱可扩展性
2. 使用token进行存储

- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
3、 access_token和refresh_token双令牌无感知登录
1.1.1 access_token
access_token 由后端颁发给前台,一般采用对称加密算法,可以反向解析出参数信息,如:用户ID,失效时间等
场景:
每次前端访问后端API接口,header中会带上此token,后端拦截器检验此token是否失效,当前请求用户ID等
JWT token: https://www.jianshu.com/p/b56fd5b24636
1.1.2 refresh_token
它的作用就是避免让用户重复输入账号密码登录再次验证
1.1.3 结合使用:
一般我们可以将access_token的过期时间设置为2小时的,refresh_token的过期时间设置为1个月,然后用户第一次进来,用了一段时间access_token过期了,过期后前端携带refresh_token去获取新的access_token,返回的新的access_token依旧是2小时,那么除此之外,refresh_token自身再刷新一次,刷新一次后他还是1个月的过期时间(不累加),这就保证了用户在一个月内只要访问了应用,就可以享受无感知的体验.
1.1.4 安全性:
- access_token 泄露概率比较大,毕竟每次api请求都附带它,如果单独把 access_token 过期时间设置过长,一旦泄露,就相当于密码泄露
- refresh_token 泄露概率比较小,只有每次access_token 失效时才会使用它,所以二者组合可以很好的避免token泄露带来的安全风险,同时又能保证用户体验
4、JWT令牌:
1.jwt令牌是什么
- 1.1全称
JSON Web Token
,是目前最流行的跨域身份验证解决方案
2.jwt数据结构:
token分为三部分,header.payload.signatrue
以 .
号分隔
- header 头文件
- payload 有效荷载
- signatrue 签名文件
如:签名使用的算法HS256,typ属性表示令牌的类型
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
jwt默认提供了以下7个属性设置,也可以自定义属性字段(类似Map中设置)
1 2 3 4 5 6 7 8
| 默认参数: iss:发行人 exp:到期时间 sub:主题 aud:用户 nbf:在此之前不可用 iat:发布时间 jti:JWT ID用于标识该JWT
|
4.签名哈希:
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
1 2 3 4 5
| var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload) var signature = HMACSHA256(encodedString, 'secret') 最终解果 : token = base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + signature
|
5、代码测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.junit.Test;
import java.util.Base64; import java.util.Date;
public class JwtTest {
@Test public void testJwt() { String secret = "loveqq";
String token = Jwts.builder().claim("userId", "007") .setSubject("shusm") .setExpiration(new Date(System.currentTimeMillis() + 3600000))
.setIssuedAt(new Date()) .setIssuer("junit") .signWith(SignatureAlgorithm.HS256, secret) .compact();
System.out.println("加密后access_token为:" + token);
String[] split = token.split("\\."); String header = new String(Base64.getDecoder().decode(split[0].getBytes())); System.out.println("header:" + header); String payload = new String(Base64.getDecoder().decode(split[1].getBytes())); System.out.println("payLoad:" + payload);
Claims body = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
System.out.println("------------JWT解析结果--------------"); body.forEach((k, v) -> { System.out.println("K:" + k + "\t v:" + v); }); } }
|
二、基于JWT的token认证实现(验证登录)(spring):
基本思路
①用户首次登录,将输入的账号和密码提交给服务器;
②服务器对输入内容进行校验,若账号和密码匹配则验证通过,登录成功,并生成一个token值,将其保存到数据库,并返回给客户端;
③客户端拿到返回的token值将其保存在本地(如cookie/localStorage),作为公共参数,以后每次请求服务器时都携带该token(放在响应头里),提交给服务器进行校验;
④服务器接收到请求后,首先验证是否携带token,若携带则取出请求头里的token值与数据库存储的token进行匹配校验,若token值相同则登录成功,且当前正处于登录状态,此时正常返回数据,让app显示数据;若不存在或两个值不一致,则说明原来的登录已经失效,此时返回错误状态码,提示用户跳转至登录界面重新登录;
⑤注意:用户每进行一次登录,登录成功后服务器都会更新一个token新值返回给客户端;

简单实现:
引入java-jwt依赖
1 2 3 4 5
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.3.0</version> </dependency>
|
签名工具 JwtUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.HashMap; import java.util.Map;
public class JwtUtil {
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
private static final String TOKEN_SECRET = "f26e587c28064d0e855e72c0a6a0e618";
public static boolean verify(String token) { try { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; } }
public static String sign(String username,String userId) { try { @Autowired private IUserService userService;
@RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public ApiResponse login(@RequestBody Map<String, String> map) { String loginName = map.get("loginName"); String password = map.get("password"); boolean isSuccess = userService.checkUser(loginName, password); if (isSuccess) { User user = userService.getUserByLoginName(loginName); if (user != null) { String token = JwtUtil.sign(user.getName(), user.getId()); if (token != null) { return ApiResponseUtil.getApiResponse(token); } } } return ApiResponseUtil.getApiResponse(ApiResponseEnum.LOGIN_FAIL); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 配置拦截器TokenInterceptor.java
import com.alibaba.fastjson.JSONObject; import com.joe.entity.ApiResponse; import com.joe.enums.ApiResponseEnum; import com.joe.util.ApiResponseUtil; import com.joe.util.JwtUtil; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter;
public class TokenInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.setCharacterEncoding("utf-8"); String token = request.getHeader("access_token"); if (null != token) { boolean result = JwtUtil.verify(token); if (result) { return true; } } ApiResponse apiResponse = ApiResponseUtil.getApiResponse(ApiResponseEnum.AUTH_ERROR); responseMessage(response,response.getWriter(),apiResponse); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
private void responseMessage(HttpServletResponse response, PrintWriter out, ApiResponse apiResponse) { response.setContentType("application/json; charset=utf-8"); out.print(JSONObject.toJSONString(apiResponse)); out.flush(); out.close(); }
}
|
spring-mvc.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <context:annotation-config/>
<mvc:annotation-driven/> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**" /> <mvc:exclude-mapping path="/login/"/> <bean class="com.joe.interceptor.TokenInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
<context:component-scan base-package="com.joe"/> </beans>
|
转载
(3条消息) Token的作用及原理_0945v1-CSDN博客
(3条消息) Java实现基于token认证_程序员阿坤的博客-CSDN博客
小程序登录基本思路
转载:
1、(3条消息) 面试官问我:如何设计 QQ、微信等第三方账号登陆 ?还要我说出数据库表设计!…_GitHubDaily-CSDN博客
2、微信小程序-getUserInfo回调的实例详解_JavaScript_脚本之家 (jb51.net)
3、(3条消息) 微信小程序前段+java后端实现登陆功能(超详细)_最菜的黑客的博客-CSDN博客_java后端实现微信小程序登录
4、(3条消息) 微信小程序登录的后端Java详细实现_Lu、ck的博客-CSDN博客_微信小程序的后端是如何实现的
小程序实现3rd_session登录:

1、小程序客户端代码表现:
(3条消息) 微信小程序之微信登陆 —— 微信小程序教程系列(20)_michael的博客-CSDN博客_微信小程序登陆
2、小程序Java端:
按照微信的建议此时需要生成一个不重复值作为openId的唯一性标识。这里采用的是java的uuid。然后把这个uuid值作为key,把openid以及后面会用到的session_key作为value,存进redis。并且把uuid值返回给小程序。这样小程序就可以直接拿uuid值跟服务端交互。
也许会有人问,如果有人得到uuid值其实跟得到openid没什么区别啊,都相当于是会员的唯一性标志。
所以这里要对这个uuid值进行一个处理。首先存入redis时要有时效性。session_key在微信服务器有效期是30天,建议服务端缓存session_key不超过30天。当小程序传过来的uuid值过期时,认为这是过期的uuid,则重新走wx.login步骤。
为了方便redis中不仅会寸uuid与openid的对应关系。还会再存一条openid对应uuid的记录,目的是为了下一次重新wx.login步骤时根据openid找到之前老的uuid,如果存在的话就删掉,然后查询一条新的uuid值,并且把openid对应的这条记录也更新掉。这样redis服务器中就不会有多余的脏数据,减轻服务器的负担。
采用jfinal框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public Ret userInfo(String code) throws WxErrorException, SQLException { WxMaService wxMaService = new WxMaServiceImpl(); WxMaDefaultConfigImpl wxMaDefaultConfigImpl = new WxMaDefaultConfigImpl(); wxMaDefaultConfigImpl.setAppid(WxaConfig.appid); wxMaDefaultConfigImpl.setSecret(WxaConfig.secret); wxMaService.setWxMaConfig(wxMaDefaultConfigImpl);
WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code); WxMaSecCheckService session1 = wxMaService.getSecCheckService(); UserThirdAuth userThirdAuth = getUserIdByWx(session.getOpenid()); if (userThirdAuth == null) { return Ret.by("msg", "用户没有权限").set("ok", "1"); } JSONObject deptCode = staffService.findByUserId(userThirdAuth.getUserId()); if (deptCode == null) { return Ret.by("msg", "用户没有对应的权限").set("ok", "1"); } String openId = session.getOpenid();
String rsessionKey = UUID.randomUUID().toString();
LOG.debug("rsession = " + rsessionKey);
String oldSessionKey = CacheKit.get("logical", openId); LOG.debug("oldSessionKey = " + oldSessionKey); if (oldSessionKey != null && "".equals(oldSessionKey)) {
CacheKit.remove("logical", oldSessionKey); LOG.debug("老的缓存删除之后" + CacheKit.get("logical",oldSessionKey).toString()); }
JSONObject wexSession = new JSONObject(); wexSession.put("openid", openId); wexSession.put("sessionKey", session.getSessionKey());
CacheKit.put("logical",rsessionKey,wexSession);
CacheKit.put("logical",openId,rsessionKey); LOG.debug("缓存之后"+CacheKit.get("logical",rsessionKey).toString() ); return Ret.create().set("user", deptCode).set("sessionKey",rsessionKey);
}
|
转载:
java实现微信小程序登录态维护 - 知乎 (zhihu.com)
微信小程序登录状态java后台解密 - xxlfly - 博客园 (cnblogs.com)
解决jwt失效

服务器不存jwt会出一些问题的,jwt在签发的时候带有权限和账户状态这些动态信息,服务器不能全相信这些信息。比如管理员已经把张三的某个权限下掉了或者把账号冻结了,但是张三客户端jwt中还是有这个权限和状态,还是可以访问已经没有权限的资源。如果在使用jwt的情况下每次请求还去找数据库或者缓存做鉴权,jwt就失去意义了,既然状态还是存在服务器上要查询,为什么不用加密用户名而用jwt呢。
解决一般都是在jwt生成后在redis中存一份,有效期设置为jwt的有效期,当用户的状态发生变动时删除redis中对应用户的jwt。这样每次做认证和鉴权时只要确认redis中储存了客户端发送来jwt(reids的haskey使用布隆过滤器,性能非常好),如果存在可以直接信任这个jwt。如果在认证和鉴权时发现redis中没有这个jwt,说明账号的状态发生了变化,强制要求客户端重新获取jwt即可。