登录鉴权
OAuth2.0 授权方式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
授权码模式
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的方式解决组件不刷新的问题