JWT 实现自动刷新登陆token
JWT 的全称为Json Web Token,简而言之json类型的web服务身份认证令牌(个人理解哟,勿喷)。
适合做前后端分离身份认证,集群服务身份认证,系统群单点登录等。
1、学习这个技术之前先来了解一下这个技术有哪些优点吧。(参考:https://blog.csdn.net/qq_34037264/article/details/108273333)
1 1、支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输. 2 3 2、无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息. 4 5 4、更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.(居于前面两点得出这个更适用于CDN内容分发网络) 6 7 5、去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.(这个似乎也在继续说前面第一点和第二点的好处。。。) 8 9 6、更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。 10 11 7、CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。(如果token是用cookie保存,CSRF还是需要考虑,一般建议使用1、在HTTP请求中以参数的形式加入一个服务器端产生的token。或者2.放入http请求头中也就是一次性给所有该类请求加上csrftoken这个HTTP头属性,并把token值放入其中) 12 ps:后面会推出一些常见的网络安全的处理 13 14 8、性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多. 15 16 9、不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理. 17 18 10、基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).
2、基础性知识和demo搭建参考问题1所提的博客就可以了。话不多说进入主题,学习如下内容需要对JWT有简单的了解。
3、在使用JWT的时候,你会发现token的生命周期不能够进行续命。此处有不少小伙伴可能不理解什么意思。
我举个栗子,用户A进入一个系统,获取到一个token,这个token默认有效期是30分钟,但是由于是业务系统,几乎工作时间都在使用这个系统。那么等到30分钟token到期的时候,用户还是需要重新登录。
期望:用户不断操作给token续命,如果token还未失效,只要用户处于活跃状态就一直给token顺延到期时间,即当前时间向后推迟30分钟,如果token已经失效,则重新登录。
现状:JWT由于无法更新失效时间(主要原因token的产生和失效时间有关系),当用户登录系统没到30分钟就需要重新登录一次,无论用户是否活跃。
4、针对这个问题,网友也各抒己见。
1) 用redis存储jwt(这个可以实现,但是有违jwt的初衷)
2) 利用refreshToken来刷新JWT (这个方案个人觉得意义不大)
5、下面我们直接开始撸代码。
1、添加JWT依赖。
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
2、创建JWT测试代码类。
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import net.sf.json.JSONObject; import org.apache.commons.codec.binary.Base64; import java.io.UnsupportedEncodingException; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author LH * @version V1.0 * @Package com.purang.test * @date 2021/3/25 16:59 * @description: 修改描述 */ public class JwtUtils { //SING签名的设置,不能对外暴露(可以提取到配置文件) public static final String SING = "token!J1JK3JH^&g%f*f@f*(f!)fs*#s*$H3J4DK43"; //产生token的有效时长,单位分钟(可以提取到配置文件) public static final int TOKEN_EFFECTIVE_TIME_MINUTE = 1; //回话连接的有效时长,单位分钟(可以提取到配置文件) public static final int SCOKET_EFFECTIVE_TIME_MINUTE = 140; public static void main(String[] args) throws UnsupportedEncodingException { /* //1、模拟第一次登录获取token Map<String,String> tokenMap = new HashMap<>();
tokenMap.put("userName","张三zhangsan1");
tokenMap.put("passWord","张三zhangsan1pass");
String token = getToken(tokenMap);*/
//================================================ //2、模拟用户操作系统校验token,有效则直接获取所需信息,直接访问,其他异常做相应处理 String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJ1c2VyTmFtZSI6IuW8oOS4iXpoYW5nc2FuMSIsImV4cCI6MTYxNjcyMjI0MH0.T6lm9kQjzUpJlio-MT2arAhAciFMzpdHyVQtpdkFjjA"; JSONObject tokenPayloadJson = null; try { //根据前台请求头中获取token的Base64编码值 String[] tokenSplit = token.split("\\."); //解析载荷信息,用于用户信息的续命 tokenPayloadJson = decodeBase64(tokenSplit[1]); System.out.println("旧token=="+token); System.out.println("旧tokenStr=="+tokenPayloadJson.toString()); //校验token合法性 verify(token); System.out.println("有效签名,验证通过"); } catch (SignatureVerificationException e) { e.printStackTrace(); System.out.println("无效签名"); } catch (TokenExpiredException e) { e.printStackTrace(); //只有在token过期的时候才可以进行token重生策略的调用 System.out.println("token已过期!"); //因JWT只精确到秒,所以此处获取的时间戳你会发现少了后面三位(也就是毫秒) Long token_time = tokenPayloadJson.getLong("exp"); Date date = new Date(); //token已经超期多少时间 System.out.println("token已经超期的时间="+(date.getTime()/1000 - token_time)/60+"分钟"); //token创建时间点到现在的时间 int time = (int) ((date.getTime()/1000 - token_time)/60 - TOKEN_EFFECTIVE_TIME_MINUTE); System.out.println("token创建时间点到现在的时间="+time+"分钟"); if(time < SCOKET_EFFECTIVE_TIME_MINUTE){ System.out.println(">>>>>>>>>>>>>>>>>>token可以重新产生"); Map<String,String> map = new HashMap<>(); tokenPayloadJson.forEach((k, v) -> { if(!"exp".equals((String)k)) map.put((String)k, (String)v); }); map.put("newToken","new"+Math.random()); String newToken = getToken(map); System.out.println("新token=="+newToken); System.out.println("新tokenStr=="+decodeBase64(getTokenInfo(newToken).getPayload())); } } catch (AlgorithmMismatchException e) { e.printStackTrace(); System.out.println("token算法不一致!"); } catch (Exception e) { e.printStackTrace(); System.out.println("token无效!!!"); } } /** * @Description 生成token header.payload.sing * @Author LH * @Date 2021-03-26 10:48:52 * @Param Map 集合存入payload信息 * @Return token * @Exception */ public static String getToken(Map<String, String> map) { //设置令牌的过期时间 Calendar instance = Calendar.getInstance(); //设置失效时间 instance.add(Calendar.MINUTE, TOKEN_EFFECTIVE_TIME_MINUTE); //创建JWT builder JWTCreator.Builder builder = JWT.create(); //payload map.forEach((k, v) -> { builder.withClaim(k, v); }); System.out.println("时间的times"+instance.getTime().getTime()); String token = builder.withExpiresAt(instance.getTime()) //指定令牌过期时间 .sign(Algorithm.HMAC256(SING)); return token; } /** * @Description 验证令牌是否合法 * @Author LH * @Date 2021-03-26 10:57:04 */ public static void verify(String token) { JWT.require(Algorithm.HMAC256(SING)).build().verify(token); } /** * @Author LH * @Date 2021-03-26 11:13:52 * @Return DecodedJWT JWT信息获取 * @Description 获取JWT的信息 */ public static DecodedJWT getTokenInfo(String token) { DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token); return verify; } /** * @Author LH * @Date 2021-03-26 11:13:52 * @Return 解析TOKEN后的JSONObject内容 * @Description 根据Base64获取解析内容 */ public static JSONObject decodeBase64(String tokenPayload) throws UnsupportedEncodingException { byte[] bytes = Base64.decodeBase64(tokenPayload); String decode = new String(bytes, "UTF-8"); return JSONObject.fromObject(decode); } }
3、新建token,运行结果如下
打开如下代码注释:
//1、模拟第一次登录获取token
Map<String,String> tokenMap = new HashMap<>();
tokenMap.put("userName","张三zhangsan1");
tokenMap.put("passWord","张三zhangsan1pass");
String token = getToken(tokenMap);
========================================================================
时间的times1616731158511 旧token==eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJ1c2VyTmFtZSI6IuW8oOS4iXpoYW5nc2FuMSIsImV4cCI6MTYxNjczMTE1OH0.tf7dT_kHFAto-AJ8BzCYI8YY8pmmgY8javepTNwGZHY 旧tokenStr=={"passWord":"张三zhangsan1pass","userName":"张三zhangsan1","exp":1616731158} 有效签名,验证通过
4、校验token有效期,进行动态token续命。
旧token==eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJ1c2VyTmFtZSI6IuW8oOS4iXpoYW5nc2FuMSIsImV4cCI6MTYxNjczMTE1OH0.tf7dT_kHFAto-AJ8BzCYI8YY8pmmgY8javepTNwGZHY 旧tokenStr=={"passWord":"张三zhangsan1pass","userName":"张三zhangsan1","exp":1616731158} com.auth0.jwt.exceptions.TokenExpiredException: The Token has expired on Fri Mar 26 11:59:18 CST 2021. at com.auth0.jwt.JWTVerifier.assertDateIsFuture(JWTVerifier.java:379) at com.auth0.jwt.JWTVerifier.assertValidDateClaim(JWTVerifier.java:370) at com.auth0.jwt.JWTVerifier.verifyClaims(JWTVerifier.java:295) at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:278) at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:261) at com.purang.track.testCase.JwtUtils.verify(JwtUtils.java:131) at com.purang.track.testCase.JwtUtils.main(JwtUtils.java:55) token已过期! token已经超期的时间=88分钟 token创建时间点到现在的时间=87分钟 >>>>>>>>>>>>>>>>>>token可以重新产生 时间的times1616736521241 新token==eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzV29yZCI6IuW8oOS4iXpoYW5nc2FuMXBhc3MiLCJuZXdUb2tlbiI6Im5ldzAuMjEzNTkzMjYwNTMxNDY3IiwidXNlck5hbWUiOiLlvKDkuIl6aGFuZ3NhbjEiLCJleHAiOjE2MTY3MzY1MjF9.r3HaEzSRwgY4UlBFNtVX39vUAC0_drIZhWL4j4tZOVc 新tokenStr=={"passWord":"张三zhangsan1pass","newToken":"new0.213593260531467","userName":"张三zhangsan1","exp":1616736521}
5、有了新的token就可以进行前台返回了,后面请求就会拿新token进行验证了。