前后端处理实时刷新refresh_token的使用

实现方案

后端生成两个token(token和refresh_token),token有效时间短,refresh_token有效时间长; 前端请求登录后,后端把这两个token传给前端,前端缓存下来;

  1. token未到期,前端可正常请求。
  2. token过期,后端会返回与前端约定好的相关参数(比如,响应码401),前端在返回拦截器中判断拦截,并将refresh_token替换掉已经失效的token去调用api_refresh_token的接口请求。后端则判断refresh_token是否过期。
    (1)refresh_token未过期,后端重新生成token和refresh_token返给前端。前端接收到后,缓存token,并重新执行之前失败的接口。
    (2)refresh_token过期,后端返回token失效,前端跳转到登录页。
分析

token和refresh_token有三个时间点需要去考虑;

token和refresh_token的时间点需要实现的结果
token和refersh_token都没失效正常请求
token失效,refresh_token没失效请求提示token失效,前端需要调api_refresh_token的请求,获取新的token
token和refresh_token都失效请求提示token失效,前端需要调api_refresh_token的请求,请求失败,退出登录

网上找的图,很棒!

代码实现

前后端语言和框架分别通过vue和Java/spring security去实现。代码有不完善之处,今后会不断完善。

后端代码

登录生成token和refresh_token

@Component
public class LmsAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String username = authentication.getName();
        User user = userService.getByUsername(username);
        //生成jwt
        String token = JwtUtil.generateToken(user,JwtUtil.EXPIRE_TIME);
        //生成refresh_token
        String refreshToken = JwtUtil.generateToken(user,JwtUtil.EXPIRE_TIME+JwtUtil.REFEASH_TIME_PLUS);

        long userId = user.getId();//获取到userId

        Map<String, Object> map = new HashMap<String, Object>();
        map.put("token", token);
        map.put("refreshToken", refreshToken);

        ResultJson result = ResultJson.ok().data(map).message("登录成功");

        response.setContentType("application/json;charset=UTF-8"); // 响应类型
        PrintWriter out = response.getWriter();
        out.write( JSON.toJSONString(result));

        out.flush();
        out.close();
    }
}

token失效的处理

@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //token失效返回401
        ResultJson result = ResultJson.error().code(ResultCode.Unauthorized).message("token失效");

        response.setContentType("text/json;charset=utf-8"); 
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //设置响应码
        response.getWriter().write(JSON.toJSONString(result));
    }
}

实现api_token_refresh接口

    @GetMapping("/api-token-refresh")
    public ResultJson refresh_token(Principal principal, HttpServletResponse response){
	      User user = userService.getByUsername(principal.getName());
	
	      //生成jwt
	      String token = JwtUtil.generateToken(user, JwtUtil.EXPIRE_TIME);
	      //refresh_token
	      String refreshToken = JwtUtil.generateToken(user, JwtUtil.EXPIRE_TIME + JwtUtil.REFEASH_TIME_PLUS);
	
	      Map<String, Object> map = new HashMap<>();
	      map.put("token", token);
	      map.put("refreshToken", refreshToken);
	      return ResultJson.ok().data(map);
    }
前端代码

axios响应拦截,关于token失效的处理

import axios from "axios";
import { Notification } from "element-ui";
import router from "./router";

const request_domain = "http://127.0.0.1:8088";
axios.defaults.baseURL = request_domain; //全局使用的请求域名

//axios实例对象
const request = axios.create({
  timeout: 5000,
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
});

// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []

function refreshToken () {
  // 我项目中  更新token 需要吧原有的token 换成refreshToken去请求   这里根据需求可以改动
  window.localStorage.setItem('token', window.localStorage.refreshToken)
  return request({method:'get',url: '/api-token-refresh'})
}
// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
function setToken(token,refreshToken){
  console.log("重新缓存token")
  request.defaults.headers['Authorization'] = `Auth ${token}`
  // 这里用到的存储是localStorage
  window.localStorage.setItem('token', token)
    
  window.localStorage.setItem('refreshToken', refreshToken)
}

axios响应拦截
request.interceptors.response.use(
  (response) => {
    // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
    // 否则的话抛出错误
    if (response.status === 200) {
      console.log(response.data.code)

      // 这里可以根据code值进行判断处理,需要与后端协商统一
      if (response.data.code == 0) {
        console.log("test");
      } else if (response.data.code == 20001) {
        console.log("20001报错");
      
        Notification.error({
          title: "错误",
          message: response.data.message,
        });
      }
      return Promise.resolve(response);
    } else {
      return Promise.reject(response);
    }
  },
  // 服务器状态码不是2开头的的情况
  // 这里可以跟你们的后台开发人员协商好统一的错误状态码
  // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
  // 下面列举几个常见的操作,其他需求可自行扩展
  (error) => {
    if (error.response.status) {
      switch (error.response.status) {
        case 401:
          var config = error.config; //获取401失败请求的axios中的config配置数据

          if (!isRefreshing) { //没有刷新
            isRefreshing = true

            return refreshToken().then(res => {//请求刷新token的接口
              const { token ,refreshToken} = res.data.data
              
              setToken(token,refreshToken) //将新的token和refresh_token保存到localStorage中
              config.headers['Authorization'] = `Auth ${token}`
              console.log('token过期刷新接口');
              // 已经刷新了token,将所有队列中的请求进行重试
              requests.forEach(cb => cb(token))
              requests = []
              return request(config) 
            },err=>{
              Notification.error({
                title: "401",
                message: error.response.data.message,
              });
              router.push("/login"); //跳转到登录页
            }).catch(res => {
              console.error('refreshtoken error =>', res)
            }).finally(() => { //无论是否有触发异常,该语句都会执行
              isRefreshing = false
            })
          }else {
            // 正在刷新token,将返回一个未执行resolve的promise
            // 保存函数 等待执行
            // 吧请求都保存起来 等刷新完成后再一个一个调用
             new Promise((resolve) => {
              // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
              requests.push((token) => {
                config.headers['Authorization'] = `Auth ${token}`
                resolve(request(config))
              })
            })
          }
          break;
        default:
      }
      return Promise.reject(error.response);
    }
  }
);
export default request;

本文参考

https://blog.csdn.net/weixin_44115908/article/details/106063316

  • 12
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值