前端后台管理系统登录与keep-live

213 阅读5分钟

登录鉴权

OAuth2.0 授权方式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式

image.png

A步骤中,客户端申请认证的URI,包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"
  • client_id:表示客户端的ID,必选项
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

下面是一个例子。

C步骤中,服务器回应客户端的URI,包含以下参数:

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

下面是一个例子。

D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
  • code:表示上一步获得的授权码,必选项。
  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
  • client_id:表示客户端ID,必选项。

E步骤中,认证服务器发送的HTTP回复,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
import axios from 'axios';

// 用于存储当前的访问令牌
let accessToken: string | null = localStorage.getItem('access_token');
// 用于存储当前的刷新令牌
let refreshToken: string | null = localStorage.getItem('refresh_token');
// 记录令牌的过期时间(以毫秒为单位)
let tokenExpirationTime: number | null = null;

// 如果本地存储中有令牌过期时间的记录,解析并设置它
const storedTokenExpirationTime = localStorage.getItem('token_expiration_time');
if (storedTokenExpirationTime) {
    tokenExpirationTime = parseInt(storedTokenExpirationTime);
}

// 锁标志,用于控制是否正在进行token刷新操作
let isTokenRefreshing: boolean = false;

// 记录token刷新的重试次数
let tokenRefreshRetryCount: number = 0;

// 设置token刷新重试次数限制
const tokenRefreshRetryLimit: number = 3;

// 创建Axios实例
const api = axios.create({
    baseURL: 'YOUR_BASE_URL',
});

// 请求拦截器
api.interceptors.request.use(
    (config) => {
        // 检查访问令牌是否存在且未过期
        if (accessToken && (!tokenExpirationTime || new Date().getTime() < tokenExpirationTime)) {
            config.headers.Authorization = `Bearer ${accessToken}`;
        } else {
            // 如果访问口气令不存在或已过期,触发刷新令牌操作
            return refreshAccessToken().then(() => {
                config.headers.Authorization = `Bearer ${access_token}`;
                return config;
            });
        }
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

// 响应拦截器
api.interceptors.response.use(
    (response) => {
        return response;
    },
    async (error) => {
        // 重点处理401状态码
        if (error.response && error.response.status === 401) {
            // 检查是否有可用的刷新令牌
            if (!refreshToken) {
                console.error('没有可用的刷新令牌,无法刷新访问令牌,将重定向到登录页面。');
                // 清除本地存储的登录相关信息
                localStorage.removeItem('access_token');
                localStorage.removeItem('user_info');
                // 清除其他相关缓存或状态变量,比如全局的登录状态变量
                if (typeof window!== 'undefined') {
                    const globalState: any = window;
                    globalState.isUserLoggedIn = false;
                }
                // 重定向到登录页面
                window.location.href = '/login';
                return Promise.reject(error);
            }

            // 如果已经在进行token刷新操作,将当前请求加入队列等待刷新完成后重新发送
            if (isTokenRefreshing) {
                return new Promise((resolve, reject) => {
                    // 将当前请求的配置信息存储起来,以便后续重新发送
                    const requestQueue = (window as any).requestQueue || [];
                    requestQueue.push({ config: error.config, resolve, reject });
                    (window as any).requestQueue = requestQueue;
                });
            }

            try {
                // 设置正在进行token刷新操作的标志
                isTokenRefreshing = true;

                // 触发刷新访问令牌操作
                await refreshAccessToken();

                // 重新发送所有等待的原始请求
                const requestQueue = (window as any).requestQueue || [];
                for (const item of requestQueue) {
                    const response = await api(item.config);
                    item.resolve(response);
                }
                (window as any).requestQueue = [];

                // 清除正在进行token刷新操作的标志
                isTokenRefreshing = false;

                // 重置token刷新重试次数
                tokenRefreshRetryCount = 0;

                // 重新发送原始请求
                return api(error.config);
            } catch (error) {
                console.error('刷新访问令牌失败,将重定向到登录页面。');
                // 增加token刷新重试次数
                tokenRefreshRetryCount++;

                // 检查是否超过重试次数限制
                if (tokenRefreshRetryCount > tokenRefreshRetryLimit) {
                    console.error('刷新令牌次数已超过限制,将重定向到登录页面。');
                    // 清除本地存储的登录相关信息
                    localStorage.removeItem('access_token');
                    localStorage.removeItem('user_info');
                    // 清除其他相关缓存或状态变量,比如全局的登录状态变量
                    if (typeof window!== 'undefined') {
                        const globalState: any = window;
                        globalState.isUserLoggedIn = false;
                    }
                    // 重定向到登录页面
                    window.location.href = '/login';

                    // 清理requestQueue,避免内存泄漏等问题
                    const requestQueue = (window as any).requestQueue || [];
                    requestQueue.length = 0;
                    (window as any).requestQueue = [];

                    return Promise.reject(error);
                }

                // 重新触发刷新访问令牌操作
                await refreshAccessToken();

                // 重新发送原始请求
                return api(error.config);
            }
        }
        return Promise.reject(error);
    }
);

// 刷新访问令牌函数
const refreshAccessToken = async () => {
    if (!refreshToken) {
        console.error('没有可用的刷新令牌,无法刷新访问令牌。');
        return Promise.reject('No valid refresh token');
    }

    const refreshTokenRequestParams = {
        client_id: 'YOUR_CLIENT_ID', // 替换为实际的客户端ID
        client_secret: 'YOUR_CLIENT_SECRET', // 替换为实际的客户端秘密
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        // 可添加其他必要参数,如redirect_uri等,根据实际情况而定
    };

    
    const response = await axios.post('https://authorization-server.com/token', refreshTokenRequestParams);
    const { access_token, refresh_token, expires_in } = response.data;

    // 更新本地存储中的令牌及相关信息
    localStorage.setItem('access_token', access_token);
    localStorage.setItem('refresh_token', refresh_token);
    const newTokenExpirationTime = new Date().getTime() + expires_in * 1000;
    localStorage.setItem('token_expiration_time', newTokenExpirationTime.toString());

    // 更新全局变量
    accessToken = access_token;
    refreshToken = refresh_token;
    tokenExpirationTime = newTokenExpirationTime;

    return Promise.resolve();
  } 

keep_alive的问题 react 解决hooks

原理:使用css层面 控制组件的显示与隐藏,使用刷新key的方式解决组件不刷新的问题

具体参考 github.com/yosong-gith…