Token无感知刷新

本文介绍了在前后端分离应用中,如何通过无感知刷新Token解决因token有效期导致的认证问题,包括使用axios进行请求、处理401状态、定时刷新以及考虑RefreshToken的安全性。

前言

在前后端分离的应用中,使用token进行交互验证是一种比较常见的方式。但是,由于token的有效期限制,需要不断地刷新token,否则会导致用户认证失败。

栗子

可以幻想一下,你在一个应用中,填写了很多个表单,最后提交的时候,401认证失败,这个时候你心里肯定一万个....所以为了解决这个问题,给用户更好的体验,本文介绍无感知刷新token的实现。

token验证的原理

在web应用中,常见的token认证方式有基于token和基于cookie的认证。基于token的认证方式是,将认证信息每次放在请求头中,在每次发起请求时,将token发给服务端认证,而基于cookie的认证方式是,客户端本地存储的cookie文件来记录用户的操作状态在随后的访问请求中,客户端浏览器会检索与用户请求资源相匹配的Cookie,并将其提交给Web服务器进行验证。如果Cookie合法,则允许用户访问请求的资源,反之则拒绝用户的访问请求。

什么是无感知刷新token

无感知刷新Token是指在Token过期之前,系统自动使用Refresh Token获取新的Access Token,从而实现Token的无感知刷新,用户可以无缝继续使用应用。在实现无感知刷新token的时候需要考虑以下几点:

  1. 如何判断token是否过期?
  2. 如何在token过期时,自动使用Refresh Token获取新的Access Token?
  3. Refresh Token安全性?

代码实现-第一步

使用axios作为客户端请求库

import axios from 'axios';  
// 设置 tokens 到 localStorage 或其他持久化存储中  
function setTokens(data) {  
    localStorage.setItem('access_token', data.access_token);  
    localStorage.setItem('refresh_token', data.refresh_token);  
}  
// 假设 login 是用户登录函数,从后端获取 tokens  
async function login() {  
    const response = await axios.post('/login', { username: 'USERNAME', password: 'PASSWORD' });  
    setTokens(response.data);  
}  

代码实现-第二步

设置请求头,添加token

axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

有不明白axios基本设置的同学,可以查看axios官方文档

代码实现-第三步

拦截401 Authorization状态

// 当请求失败并返回 401 时,尝试使用 refresh token 获取新的 access token  
axios.interceptors.response.use(undefined, (error) => {  
    if (error.response && error.response.status === 401 && localStorage.getItem('refresh_token')) {  
        return axios.post('/refresh_token', { refresh_token: localStorage.getItem('refresh_token') })  
            .then((response) => {  
                setTokens(response.data);  
                error.config.headers['Authorization'] = 'Bearer ' + response.data.access_token; // 用新的token发送请求  
                return axios(error.config); // 让请求再次发送  
            });  
    } else {  
        return Promise.reject(error); // 否则,返回原始错误信息  
    }  
});

代码实现-第四步

设置定时刷新token

为了避免Access Token过期时间太长,可以使用定时器定时刷新token,在每次刷新后重新设置Access TokenRefresh Token

function refreshToken() {
  const refreshToken = localStorage.getItem('refresh_token');
  axios.post('/refresh_token', { refresh_token: localStorage.getItem('refresh_token'))
    .then(response => {
      setTokens(response.data);
      axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
    })
    .catch(error => {
      console.error(error);
    });
}

setInterval(refreshToken, 5 * 60 * 1000); // 每5分钟刷新Token

安全性考虑

在实现无感知刷新Token的过程中,需要考虑到Refresh Token的安全性问题。因为Refresh Token具有长期的有效期限,一旦Refresh Token被泄露,攻击者就可以使用Refresh Token获取新的Access Token,从而绕过认证机制,访问受保护的API。

  1. 限制访问次数
  2. 请求加签(目前团队中大佬已经实现加签)
  3. 加密

后续优化

在实现过程中,会发现该实现方式大部分都是在请求拦截和相应拦截中设置的,这样带来的问题就是,耦合性高,接口重试的机制不太好。另一方面,我觉得token无感知刷新涉及到了接口重发,我理解的是接口维度的。所以在后续会考虑封装一个class类来实现。

### 若依框架中实现无感知刷新 Token 功能 #### 1. 登录接口设计 在用户成功登录之后,服务端会生成 `accessToken` 和 `refreshToken` 并将其返回给前端。这两个令牌用于不同的目的: - **AccessToken**: 用户访问受保护资源所需的凭证。 - **RefreshToken**: 当 `accessToken` 失效时用来换取新的 `accessToken`。 ```java // Java代码片段展示如何创建两个类型的token String accessToken = tokenService.createToken(loginUser); String refreshToken = tokenService.createRefreshToken(loginUser.getUsername()); ``` [^3] #### 2. 前端配置 Axios 拦截器 为了实现在每次 HTTP 请求前自动检查并更新过期的 `accessToken` ,可以在 Vue 应用程序中通过配置 axios 的请求/响应拦截器来完成此操作: ```javascript import axios from 'axios'; const instance = axios.create({ baseURL: process.env.VUE_APP_BASE_API, }); instance.interceptors.request.use( config => { const token = localStorage.getItem('access_token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, error => Promise.reject(error) ); let isRefreshing = false; // 控制并发防止多次刷新token let failedQueue = []; // 存储失败后的请求队列 function onAccessTokenFetched(access_token){ while(failedQueue.length > 0){ let promiseResolve = failedQueue.shift(); promiseResolve({ access_token }); } } instance.interceptors.response.use(undefined, async function (error) { const originalRequest = error.config; if (error?.response?.status === 401 && !originalRequest._retry) { if(isRefreshing){ return new Promise(function(resolve){ failedQueue.push(() => resolve(originalRequest)); }) } originalRequest._retry = true; try{ isRefreshing = true; const res = await refreshAccessToken(); // 自定义函数 if(res.status===200){ updateLocalStorageTokens(res.data.tokens); // 更新本地存储中的tokens originalRequest.headers.Authorization = "Bearer "+res.data.tokens.access_token; onAccessTokenFetched(res.data.tokens.access_token); return instance(originalRequest); }else{ logoutUser(); // 如果无法获得新token则登出用户 } }catch(err){ console.error("Failed to fetch a fresh access token", err); logoutUser(); }finally{ isRefreshing=false; } } return Promise.reject(error); }); ``` [^1][^2] 上述 JavaScript 代码展示了如何利用 Axios 拦截器机制,在遇到未授权错误(`401`)的情况下尝试使用 `refreshToken` 获取一个新的 `accessToken`, 同时也处理了多个并发请求的情况. #### 3. 定义 Refresh Access Token 函数 还需要编写一个专门负责向服务器发送请求以获取最新 `accessToken` 的辅助方法 `refreshAccessToken()` : ```javascript async function refreshAccessToken() { const refreshToken = localStorage.getItem('refresh_token'); const response = await axios.post('/auth/refresh', {}, { headers: {'Authorization': `Bearer ${refreshToken}`} }); return response; } ``` [^4] 该函数将携带当前有效的 `refreshToken` 发送到 `/auth/refresh` 接口,并期望接收到包含新鲜 `accessToken` 及其对应的新 `refreshToken` 的 JSON 对象作为回应.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LiuPing_Xie

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值