1.Token的演变历史
1.1纯静态时代
很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议, 就是请求加响应, 尤其是我不用记住是谁刚刚发了HTTP请求,每个请求对我来说都是全新的。
1.2 Session
随着交互式Web应用的兴起,像在线购物网站,需要登录的网站等等,马上就面临一个问题,那就是要管理会话,必须记住哪些人登录系统,因为HTTP请求是无状态的,所以想出的办法就是给大家发一个会话标识(session id), 说白了就是一个随机的字串,每个人收到的都不一样, 每次大家向我发起HTTP请求的时候,把这个字符串给一并捎过来, 这样我就能区分开谁是谁了。
1.3 Session粘连和复制
随着Web应用用户越来越多,服务器要保存成千上万的sessionID,单台内存容量有限,开始需要使用多台机器集群。这就面临着内存不共享,无法获取到其他机器的sessionId。
小技巧:session sticky(Session粘连) , 就是让小F的请求一直粘连在机器A上, 但是这也不管用, 要是机器A挂掉了, 还得转到机器B去。
只好做session 的复制了, 把session id 在两个机器之间搬来搬去,内存极度浪费。
后来有了Memcached和Redis等缓存数据库,可以统一从一台机器中获取,稍微好了一点。
1.4Token时代
随着手机App时代的兴起,Session不被App支持,虽然可以模拟发送sessionId,但是十分繁琐。开始有人思考一个问题,干嘛一定要让服务器保存sessionId,内存都快买不起了。凭证应该让用户持有,无论是App还是Web应用,每次请求将Token带过来,服务器验证有效性不就行了,这样请求就又变成了无状态了,无论多少台机器扩展都不是难事。
那么存在一个问题,Token如何生成呢?如果一个随机的Token,发送过来没有任何意义,因为服务器不保存就无法验证,明文保存userID?这样客户端可以随意伪造,也不安全。
于是我们的Token应该要经过加密或签名避免伪造。
2.Token相对SessionID的优势
- 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
- 无状态(也称:服务端可扩展性):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
- 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
- 去耦合: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
- CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 了解csrf: https://blog.youkuaiyun.com/xiaoxinshuaiga/article/details/80766369
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
3.Token验证流程
3.1Token验证流程图
流程描述:
1.客户端使用用户名跟密码请求登录
2.服务端收到请求,去验证用户名与密码
3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4.客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6.服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
3.2Token解决方案
3.2.1JWT解决方案(后台不保存)
3.2.1.1什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
3.2.1.2JWT的组成部分
3.2.1.2.1头部(Header)
标题包含了令牌的元数据,HEADER主要为了描述该JWT的最基本信息,主要包含两个部分:声明类型和声明加密算法(通常直接使用HMAC,SHA256,HS256等)。
3.2.1.2.2载荷(Payload)
载荷主要是存放有效信息,这些信息按照职能可以分成三个部分。标准的注册声明,公共的声明,私有的声明。
标准的声明:(建议但不强制要求)
公共的声明 :公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为BASE64是对称解密的,意味着该部分信息可以归类为明文信息。
然后对载荷按照BASE64进行编码(该编码是可以对称解密),这样就构成了JWT的第二部分。
3.2.1.2.3签证(Signature)
JWT的第三部分是一个签证信息,这个签证信息主要由三个部分组成:Header(BASE64后),Payload(BASE64后),secret。
首先这个部分需要BASE64加密后的header和payload,然后使用进行连接组成的字符串,然后通过header中指定的加密方式,进行加盐值secret组合加密,然后就构成了JWT的第三部分。
3.2.1.3.2创建maven的web项目
3.2.1.3.3在maven的web项目pom.xml文件中添加依赖
1.<dependencies>
2. <!-- junit测试包 -->
3. <dependency>
4. <groupId>junit</groupId>
5. <artifactId>junit</artifactId>
6. <version>4.12</version>
7. <scope>test</scope>
8. </dependency>
9. <!-- 服务器编译依赖包 -->
10. <dependency>
11. <groupId>javax.servlet</groupId>
12. <artifactId>servlet-api</artifactId>
13. <version>2.5</version>
14.</dependency>
15. <dependency>
16. <groupId>javax.servlet</groupId>
17. <artifactId>jstl</artifactId>
18. <version>1.2</version>
19.</dependency>
20.<!-- 工具包 -->
21. <dependency>
22. <groupId>org.apache.commons</groupId>
23. <artifactId>commons-lang3</artifactId>
24. <version>3.7</version>
25.</dependency>
26.<!-- jjwt开源包 -->
27. <dependency>
28. <groupId>io.jsonwebtoken</groupId>
29. <artifactId>jjwt-api</artifactId>
30. <version>0.10.5</version>
31.</dependency>
32.<dependency>
33. <groupId>io.jsonwebtoken</groupId>
34. <artifactId>jjwt-impl</artifactId>
35. <version>0.10.5</version>
36. <scope>runtime</scope>
37.</dependency>
38.<dependency>
39. <groupId>io.jsonwebtoken</groupId>
40. <artifactId>jjwt-jackson</artifactId>
41. <version>0.10.5</version>
42. <scope>runtime</scope>
43.</dependency>
44.<dependency>
45. <groupId>org.bouncycastle</groupId>
46. <artifactId>bcprov-jdk15on</artifactId>
47. <version>1.60</version>
48. <scope>runtime</scope>
49.</dependency>
50. </dependencies>
3.2.1.3.4编写JWT工具类
1.package org.token.jwt.utils;
2.
3.import java.io.IOException;
4.import java.security.Key;
5.import java.util.Date;
6.import java.util.LinkedHashMap;
7.import java.util.Map;
8.import java.util.Properties;
9.
10.import javax.crypto.spec.SecretKeySpec;
11.import javax.xml.bind.DatatypeConverter;
12.
13.import io.jsonwebtoken.Claims;
14.import io.jsonwebtoken.JwtBuilder;
15.import io.jsonwebtoken.JwtException;
16.import io.jsonwebtoken.Jwts;
17.import io.jsonwebtoken.SignatureAlgorithm;
18.import io.jsonwebtoken.lang.Strings;
19.
20./**
21. * jwt令牌生成解析工具类
22. *
23. */
24.public class JwtUtil {
25.
26. private static final Properties PROPS = new Properties();
27. private static byte[] signingSecretBytes;
28.
29. static {
30. try {
31. PROPS.load(JwtUtil.class.getResourceAsStream("/jwt.properties"));
32. // 密钥转换成Base64编码
33. signingSecretBytes = DatatypeConverter.parseBase64Binary(PROPS.getProperty("jwt.signkey"));
34. } catch (IOException e) {
35. e.printStackTrace();
36. }
37. }
38.
39. /**
40. * 生成token
41. *
42. * @param payload
43. * 载荷
44. * @param exp
45. * 有效时长
46. * @return token
47. */
48. public static String createToken(String id, Map<String, Object> claims, Long exp) {
49. // 签名算法使用SHA256算法加密
50. String alg = PROPS.getProperty("jwt.header.alg");
51. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(alg);
52. // 加密JWT
53. Key signingKey = new SecretKeySpec(signingSecretBytes, signatureAlgorithm.getJcaName());
54. // 设置JWT声明格式,生成JWT
55. JwtBuilder jwtBuilder = createBuilder(id, claims, exp)
56. .signWith(signingKey, signatureAlgorithm); // 签名算法及签名密钥,将header与payload加密拼接后形成JWT
57.
58. return jwtBuilder.compact(); // 返回JWT
59. }
60.
61.
62. private static JwtBuilder createBuilder(String id, Map<String, Object> claims, Long exp){
63. Date iat = new Date();
64. JwtBuilder builder = Jwts.builder()
65. .setClaims(claims)
66. .setHeaderParam("typ", PROPS.getProperty("jwt.header.typ"))
67. .setHeaderParam("alg", PROPS.getProperty("jwt.header.alg"))
68. .setSubject(PROPS.getProperty("jwt.payload.sub"))
69. .setIssuer(PROPS.getProperty("jwt.payload.iss"))
70. .setIssuedAt(iat);
71. if(Strings.hasText(id)){
72. builder.setId(id);
73. }
74. if(exp == null){
75. String expStr = PROPS.getProperty("jwt.default.exp");
76. try {
77. exp = Long.parseLong(expStr);
78. } catch (Exception e) {
79. throw new JwtException("JwtBuilder创建失败,无法设置过期时间");
80. }
81. }
82. builder.setExpiration(new Date(iat.getTime() + exp)); // 设置token有效期
83. return builder;
84. }
85.
86. /**
87. * 解析token信息
88. *
89. * @param token
90. * JWT信息
91. * @return payload
92. */
93. public static Claims parseToken(String token) {
94. return Jwts.parser().setSigningKey(signingSecretBytes).parseClaimsJws(token).getBody();
95. }
96.
97. /**
98. * 验证token是否有效
99. *
100. * @param token
101. * @return
102. */
103. public static boolean verifyToken(String token) {
104. try {
105. parseToken(token);
106. return true;
107. } catch (Exception e) {
108. return false;
109. }
110. }
111.
112.
113. public static void main(String[] args) throws InterruptedException {
114.
115. Map<String, Object> claims = new LinkedHashMap<>();
116. claims.put("uid", "123");
117. long start = System.currentTimeMillis();
118. for (int i = 0; i < 10; i++) {
119. String token = createToken(null, claims, 5000L);
120. Thread.sleep(1000);
121. System.out.println("jwt:" + token);
122. Claims c = parseToken(token);
123. System.out.println(c.get("uid"));
124. }
125. long end = System.currentTimeMillis();
126. System.out.println(end - start);
127. }
128.}
3.2.1.3.5配套配置文件jwt.properties
1.jwt.signkey=KwznzkXNzAlRCwHimDIHkziyyQNxrzyPXidoZoGeMsfdObfLDBoJOWCDljWrNglaFYxVjqFFapBqclIgMIJhXlMofNVoGsMUczRZinCdDtZxDRFvEjfXFsZNrPEodQAFfTRLVApTryJBLCyvbCsBDDbiZedFkKhpeuuqGukvQkVDFMVnAOCkKdztZhTYPwRWnDBXQCShXMjfmuzXdaHeUTwJNDIHwSnrmrbYFyAXndrUpJgIXJWSTHylFwzDNvyE
2.jwt.header.alg=HS256
3.jwt.header.typ=jwt
4.jwt.payload.iss=ktjiaoyu
jwt.default.exp=60
3.2.1.3.6编写jwt过滤器
1.package org.token.jwt.filter;
2.
3.import java.io.IOException;
4.
5.import javax.servlet.Filter;
6.import javax.servlet.FilterChain;
7.import javax.servlet.FilterConfig;
8.import javax.servlet.ServletException;
9.import javax.servlet.ServletRequest;
10.import javax.servlet.ServletResponse;
11.import javax.servlet.http.HttpServletRequest;
12.import javax.servlet.http.HttpServletResponse;
13.
14.import org.token.jwt.utils.JwtUtil;
15.
16./**
17. * Servlet Filter implementation class JwtFilter
18. */
19.public class JwtFilter implements Filter {
20.
21. /**
22. * Default constructor.
23. */
24. public JwtFilter() {
25. }
26.
27. /**
28. * @see Filter#destroy()
29. */
30. public void destroy() {
31. }
32.
33. /**
34. * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
35. */
36. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
37.
38. HttpServletRequest req = (HttpServletRequest)request;
39. HttpServletResponse resp = (HttpServletResponse)response;
40. // 简单验证包含login字符串认为是登录相关业务(仅做测试,不安全)
41. String url = req.getRequestURL().toString();
42. if(!url.contains("login")
43. && !url.contains("/js/")){
44. String token = req.getHeader("Authorization");
45. // 如果验证失败,退回登录页面
46. if(!JwtUtil.verifyToken(token)){
47. resp.getWriter().println("no authorization");
48. return;
49. }
50. }
51. chain.doFilter(request, response);
52. }
53.
54. /**
55. * @see Filter#init(FilterConfig)
56. */
57. public void init(FilterConfig fConfig) throws ServletException {
58. }
59.
60.}
缓存Token(后台使用Redis等缓存数据库保存token)
Token数据结构
返回给前端的JSON数据
解析:Token数据结构是一个Json格式的,在这个Json格式中有几个比较重要的字段,expTime表示过期时间,genTime表示生成时间,它们都是百万毫秒数。token字段为返回给客户端的一个token标识,具体token字符串格式为:
token:客户端标识-USERCODE-USERID-CREATIONDATE-RANDEM[6位]
token: 为前缀
客户端标识有:PC就是普通电脑,MOBILE为移动端。
USERCODE:是采用32位的MD5进行加密后转换成这种USERCODE。
USERID:为用户的编号。
:为token的创建日期。
RANDEM:为六位的MD5码,是根据用户在客户端发送http请求中的user-agent加密生成。
ticket:是用于减轻在服务器中对token的查询压力。
不同的业务系统Token的生成规则也可能不同,但是大体的思路是一样的。
3.2.2.1.2Token请求流程
Token维护
在Token机制下会有这么一个缺点,过了两个小时后用户是无法再次访问本网站,就必须要去重新登录,因为当过完两个小时后,token信息会被redis数据库自动删除。
那么如何在用户不需要重新登录的情况下,也能再次访问本网站呢?那token给出了刷新的机制,也叫置换规则。大概的意思是,将即将过期或者是即将被redis删除的token给置换掉。
我们之前将token保存到redis的原理是,将用户对象信息转换成JSON字符串并作为保存到redis中的值,然后将生成的token字符串作为key。在置换的过程中,我们重新去生成一个token字符串,并将它作为key,其值仍然不变。然后再保存到redis中。
在上图置换的规则中,首先这个置换的规则必须要由前端进行发起,然后再去走接下来的置换流程。我们会为这个置换规则定义一个临界值,在指定的时间临界区间中token是必须要进行置换的,不然两个小时候后token过期会被redis进行删除。因为我们不能进行频繁地进行token置换,那样会造成服务器的压力,所以我们一般定义一个合理的时间临界区间,如上图中的登录之后一个小时~快要过期前的5分钟,我们就让token进行置换操作。但是如果用户在这5分钟之后还是忘记置换,那么用户只能是要求强制登录。然后上图中是以到期前5分钟对这个置换规则进行解释。
在上图置换规则中,整个流程分为三个阶段。
1)阶段一:登录阶段
登录后获取token和过期时间如果是PC端,那么将token和过期时间保存到Cookie中,如果是移动端就保存到Redis或者其他缓存数据库中。
2)阶段二:登录后的正常访问阶段0-1小时55分钟
当前端去请求业务API时从Cookie或Redis中获取token信息判断token的过期时间和当前 时间是否到该置换token的时间段了(过期时间-当前时间<5分钟)注:这里的过期时间是用token 生成 时间+两个小时。如果小于5分钟就到了该置换的时间段了调用reload置换接口重新得到新的 token重新将token设置到Cookie或Redis中正常token访问API。
如果大于5分钟(即还没有到reload的时间段)正常token访问API。
3)阶段三:登录后的快过期的5分钟阶段。