前言

最近在做一个系统时,使用了 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 功能已经实现。

原文地址:http://www.cnblogs.com/huang-guosheng/p/16908977.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性