当前位置:首页>IT那点事>前端Token无感刷新完整实践指南

前端Token无感刷新完整实践指南

基本概念

Token的类型

  1. 访问令牌(Access Token)
  • 用于访问受保护资源的凭证
  • 具有较短的有效期
  • 用于日常API请求认证
  1. 刷新令牌(Refresh Token)
  • 用于获取新的访问令牌
  • 具有较长的有效期
  • 在Access Token过期时使用

实现原理

基本流程

  1. 设置拦截器监听请求
  2. 检测Token是否过期
  3. 使用Refresh Token获取新的Access Token
  4. 更新存储的Token
  5. 重试原始请求

具体实现方案

1. XMLHttpRequest实现

// 创建XMLHttpRequest实例
const xhr = new XMLHttpRequest();

// 登录成功后保存Token
function onLoginSuccess(response) {
    localStorage.setItem('accessToken', response.data.accessToken);
    localStorage.setItem('refreshToken', response.data.refreshToken);
}

// 发送请求函数
function sendRequest(url, method, data) {
    return new Promise((resolve, reject) => {
        xhr.open(method, url);
        xhr.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('accessToken')}`);
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject({ status: xhr.status, response: xhr.responseText });
                }
            }
        };
        xhr.send(data ? JSON.stringify(data) : null);
    });
}

// 刷新Token函数
async function refreshToken() {
    const refreshToken = localStorage.getItem('refreshToken');
    try {
        const response = await fetch('/api/refresh', {
            method: 'POST',
            body: JSON.stringify({ refresh_token: refreshToken })
        });
        const data = await response.json();
        if (data.success) {
            localStorage.setItem('accessToken', data.newAccessToken);
            return true;
        }
        return false;
    } catch(err) {
        return false;
    }
}

2. Axios实现

import axios from 'axios';

const apiClient = axios.create({
    baseURL: 'https://api.example.com'
});

// 响应拦截器
apiClient.interceptors.response.use(
    response => response,
    async error => {
        const originalRequest = error.config;

        if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;

            try {
                const refreshed = await refreshToken();
                if (refreshed) {
                    return apiClient(originalRequest);
                }
            } catch (refreshError) {
                // 处理刷新失败
                return Promise.reject(refreshError);
            }
        }

        return Promise.reject(error);
    }
);

async function refreshToken() {
    try {
        const response = await apiClient.post('/refresh', {
            refreshToken: localStorage.getItem('refreshToken')
        });

        if (response.data.success) {
            const newToken = response.data.accessToken;
            localStorage.setItem('accessToken', newToken);
            apiClient.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
            return true;
        }
        return false;
    } catch (error) {
        return false;
    }
}

3. Fetch API实现

async function fetchWithToken(url, options = {}) {
    let token = localStorage.getItem('accessToken');

    const headers = {
        ...options.headers,
        'Authorization': `Bearer ${token}`
    };

    try {
        let response = await fetch(url, { ...options, headers });

        if (response.status === 401) {
            const refreshed = await refreshToken();
            if (refreshed) {
                headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;
                return fetch(url, { ...options, headers });
            } else {
                throw new Error('Token refresh failed');
            }
        }

        return response;
    } catch (error) {
        throw error;
    }
}

4. Uni-app实现

let isRefreshing = false;
let requests = [];

// 响应拦截器
http.interceptor.response((response) => {
    if (response.statusCode === 401) {
        const retryOriginalRequest = new Promise(resolve => {
            requests.push(token => {
                response.header.Authorization = `Bearer ${token}`;
                resolve(http.request(response.config));
            });
        });

        if (!isRefreshing) {
            isRefreshing = true;
            refreshToken().then(newToken => {
                requests.forEach(callback => callback(newToken));
                requests = [];
            }).catch(err => {
                // 处理刷新失败
                uni.navigateTo({ url: '/pages/login/login' });
            }).finally(() => {
                isRefreshing = false;
            });
        }

        return retryOriginalRequest;
    }
    return response;
});

安全性考虑

1. Token存储

  • 避免存储在localStorage(XSS攻击风险)
  • 考虑使用httpOnly cookie
  • 加密存储敏感信息

2. 并发请求处理

  • 使用请求队列
  • 避免重复刷新
  • 处理竞态条件

3. 错误处理

  • 合理的失败重试机制
  • 清晰的错误提示
  • 用户友好的重新登录流程

最佳实践建议

  1. 合理设置Token过期时间
  2. 实现防重放机制
  3. 定期清理过期Token
  4. 监控Token使用情况
  5. 实现Token吊销机制

这种无感刷新Token的机制可以大大提升用户体验,但需要在安全性和便利性之间找到平衡点。建议根据具体项目需求选择合适的实现方案。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

给TA打赏
共{{data.count}}人
人已打赏
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
搜索

本站承接WordPress建站仿站、二次开发、主题插件定制等PHP开发服务!