前言
最近在做一个系统时,使用了 token 令牌来进行前后端交互的权限认证。
token 一般用于前端向后端发起请求时的权限认证。
用户登录自己的账号后,会得到一个 token,放在每次请求的请求头里面,后端拿它来做权限认证。
一般来说,每个 token 都会有一个过期时间,如果后端获得了一个过期的 token,会拒绝此次请求,并通知前端此用户登录已过期。
前端拿到后端发来的过期通知,最基础的做法是直接将前端页面重定向到登录页面,让用户重新登陆。
但是这种思路存在一个问题:当用户正在使用该系统的关键时期,突然 token 过期了,就会被迫重新登陆,很显然,无论是开发者还是使用者,都不会容忍这种情况的发生。
这时候,就需要开发者使用无痛刷新 token 的技术,让用户无感知的刷新 token,避免出现以上情况。
实现思路
前端登录时,后端准备两个 token,一个有效时间较短的作为认证 token(token),一个有效时间较长的作为刷新 token(refreshToken),返回给前端。
前端拿到两个 token 后,把它们都存储在 localStorage 中,其中 token 用于每次请求的携带,refreshToken用于 token 过期后对 token 进行刷新的认证。
总体思路如下:
- token 未过期时,可以正常请求。
- token 过期了,refreshToken 未过期,请求后端的刷新token接口对两个接口均进行刷新。
- token 和 refreshToken 均过期了,则用户必须重新登录。
代码实现
前端
前端使用axios的响应拦截器功能。
对每次请求的响应数据进行拦截,检查响应中的状态码,如果是 401(权限过期),则进行刷新尝试。如果不是 401,则直接放行。
下面是 axios 相应拦截实现的核心代码:
//axios实例创建
const service = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000
})
//刷新token的请求函数
function refreshToken(refreshToken) {
return service({
url: '/user/refreshToken/' + refreshToken,
method: 'get'
})
}
//因axios请求为异步请求,故可能会出现同时多次刷新token的情况
//该变量相当于给刷新token上了个锁
let isRefreshing = false
service.interceptors.response.use(
resp => {
//判断状态码
//也要排除掉刷新token的请求
if (resp.data.code === 401 && !resp.config.url.includes('/user/refreshToken')) {
//先查询vuex中是否有refreshToken
//如果没有,则直接重定向到登录页面进行重新登录
if (!store.getters.refreshToken) {
router.push('/login')
return resp
//如果有,则先判断是否已经有过刷新请求,如果没有,进行刷新请求
} else {
if (!isRefreshing) {
isRefreshing = true
return refreshToken(store.getters.refreshToken).then(res => {
//通过该请求响应数据的状态码判断刷新token(refreshToken)是否过期
if (res.data.code === 401) {
router.push('/login')
isRefreshing = false
return res
}else{
//未过期时会得到两个新的token,此时将其持久化
store.dispatch('setToken', res.data.data.token)
store.dispatch('setRefreshToken', res.data.data.refreshToken)
resp.config.headers.token = res.data.data.token
if (resp.config.method === 'post'||resp.config.method === 'put') {
resp.config.data=JSON.parse(resp.config.data)
}
isRefreshing = false
//重新发起因token过期而未能成功实现的请求
return service.request(resp.config)
}
})
}
}
} else {
return resp
}
},
// 请求错误响应(和本文功能无关)
error => {
console.log(error)
return Promise.reject(error)
}
)
后端
配置TokenUtil类
后端对 TokenUtil 添加三个函数。
一个是 refreshTokenSign(User user) 用于登录时对刷新token(refreshToken)的注册。
public static String refreshTokenSign(User user){
String refreshToken;
Date refreshAt=new Date(System.currentTimeMillis()+REFRESH_TIME);
refreshToken= JWT.create()
.withIssuer("auth0")
.withClaim("phone",user.getPhone())
.withClaim("password",user.getPassword())
.withClaim("role",user.getRole())
.withExpiresAt(refreshAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
return refreshToken;
}
一个是 refreshTokenSign(String token) 用于刷新 token 时对 refreshToken 的刷新。
public static String refreshTokenSign(String token){
String refreshToken;
Date refreshAt=new Date(System.currentTimeMillis()+REFRESH_TIME);
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
DecodedJWT jwt = verifier.verify(token);
refreshToken= JWT.create()
.withIssuer("auth0")
.withClaim("phone",jwt.getClaim("phone").asString())
.withClaim("password",jwt.getClaim("password").asString())
.withClaim("role",jwt.getClaim("role").asString())
.withExpiresAt(refreshAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
return refreshToken;
}
最后一个是 refreshToken(String refreshToken) 用于通过 refreshToken 刷新 token。
public static String refreshToken(String refreshToken){
String token;
Date expiresAt=new Date(System.currentTimeMillis()+EXPIRE_TIME);
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build();
DecodedJWT jwt = verifier.verify(refreshToken);
token= JWT.create()
.withIssuer("auth0")
.withClaim("phone",jwt.getClaim("phone").asString())
.withClaim("password",jwt.getClaim("password").asString())
.withClaim("role",jwt.getClaim("role").asString())
.withExpiresAt(expiresAt)
.sign(Algorithm.HMAC256(TOKEN_SECRET));
log.info("刷新token成功");
return token;
}
编写刷新token的接口
该类用于接受前端的 刷新 token 的 api 请求并提供相应响应。
@ApiOperation("刷新token")
@GetMapping("/refreshToken/{refreshToken}")
public Result refreshToken(@PathVariable("refreshToken")String refreshToken){
if(!TokenUtil.verify(refreshToken)){
return Result.error(401,"刷新token已过期");
}
String token = TokenUtil.refreshToken(refreshToken);
String newRefreshToken = TokenUtil.refreshTokenSign(token);
JSONObject jsonObject=new JSONObject();
jsonObject.put("token",token);
jsonObject.put("refreshToken",newRefreshToken);
return Result.success(jsonObject);
}
WebMvcConfig 对接口放行
主要是:excludePath.add("/user/refreshToken/**");
。
该函数指定了不被 token 拦截器拦截的请求。
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> excludePath=new ArrayList<>();
excludePath.add("/user/login/**");
excludePath.add("/user/register");
excludePath.add("/user/refreshToken/**");
excludePath.add("/doc.html");
excludePath.add("/swagger-ui.html");
excludePath.add("/swagger-resources/**");
excludePath.add("/v2/api-docs");
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
}
实现效果
无痛刷新核心意义就是在用户无感知的情况下对过期的 token 进行刷新。
我使用一个过期的 token 和未过期的 refreshToken 对该功能进行测试,结果如下:
从该图片中可以看出,”5“ 即为该 post 请求。
从该网络请求中可以看到,第一个 ”5“ 请求失败,且登录已过期。
该请求为刷新 token 的请求。
该请求为刷新 token 后对上上个请求的再次尝试,可以看出,这一次成功了。
从控制台可以看出,该请求之输出了请求成功的这次响应数据。
综上,无痛刷新 token 功能已经实现。
标签:vue,refreshToken,请求,过期,String,token,刷新,springboot From: https://www.cnblogs.com/huang-guosheng/p/16908977.html