VUE项目双token刷新 vue项目token无感刷新
该方法无需让后端单独提供刷新接口
开发环境
前端:vue3 + vite + element + axios
后端:thinkphp8.0
token生成方式:jwt
前端代码
import axios from "axios";
import { toast, getToken, setToken, removeToken } from "~/composables/util";
import { logout } from "./api/admin";
// 创建axios实例,并设置基础URL
const service = axios.create({
baseURL: '/api',
});
// 标识是否正在刷新Token,避免重复刷新
let isRefreshing = false;
// 请求队列,用于存放在Token刷新过程中等待的请求
let requestsQueue = [];
// 添加请求拦截器
service.interceptors.request.use(config => {
// 从本地存储中获取AuthToken,并将其添加到请求头中
const authToken = getToken('AuthToken');
if (authToken) {
config.headers['AuthToken'] = authToken;
}
return config; // 返回修改后的配置
}, error => {
console.error(error);
return Promise.reject(error); // 请求错误时直接拒绝
});
// 添加响应拦截器
service.interceptors.response.use(response => {
return response.data; // 对响应数据直接返回
}, async error => {
const { response } = error; // 获取错误响应对象
const originalRequest = error.config; // 保存原始请求配置
if (response && response.status === 401 && !originalRequest._retry) {
removeToken('AuthToken');
originalRequest._retry = true;
if (!isRefreshing) {
isRefreshing = true;
try {
const refreshToken = getToken('RefreshToken');
if (!refreshToken) {
throw new Error('No RefreshToken available');
}
// 发送携带RefreshToken的请求来获取新的Token
const refreshResponse = await service({
method: originalRequest.method,
url: originalRequest.url,
headers: { 'RefreshToken': refreshToken },
data: originalRequest.data,
params: originalRequest.params,
});
// 获取新Token并存储
const newAuthToken = refreshResponse.data.new_auth_token;
const newRefreshToken = refreshResponse.data.new_refresh_token;
setToken('AuthToken', newAuthToken);
setToken('RefreshToken', newRefreshToken);
// 执行所有挂起的请求
requestsQueue.forEach(cb => cb(newAuthToken));
requestsQueue = [];
// 更新原始请求的AuthToken头并重新发送请求
originalRequest.headers['AuthToken'] = newAuthToken;
return service(originalRequest);
} catch (refreshError) {
logout(getToken('RefreshToken')).finally(() => {
removeToken('AuthToken');
removeToken('RefreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
})
} finally {
isRefreshing = false;
}
} else {
// 如果Token正在刷新,将请求加入队列,并在Token刷新完成后重新发送请求
return new Promise(resolve => {
requestsQueue.push((newToken) => {
originalRequest.headers['AuthToken'] = newToken;
resolve(service(originalRequest));
});
});
}
}
// 处理402状态码 - 跳转至登录页面
if (response && response.status === 402) {
removeToken('AuthToken');
removeToken('RefreshToken');
window.location.href = '/login';
return Promise.reject(error);
}
if (response.status !== 401) {
toast(response.data.resultdesc || '请求失败', 'error');
}
return Promise.reject(error);
});
export default service;
后端代码
中间件
<?php
namespace app\middleware;
use Psr\SimpleCache\InvalidArgumentException;
use think\facade\Request;
use think\facade\Cache;
use think\Response;
class CheckWebToken
{
public function handle($request, \Closure $next)
{
$authToken = Request::header('AuthToken');
$refreshToken = Request::header('RefreshToken');
if (!$authToken) {
if(!$refreshToken){
return webResponse(-5, '未获取到refreshToken,请重新登录', [], 402);
}
return $this->handleRefreshToken($refreshToken);
}
if (!$this->validateToken($authToken)) {
return webResponse(-5, '暂无权限', [], 401);
}
return $next($request);
}
private function handleRefreshToken(string $refreshToken): Response
{
$tokenData = Cache::store('token')->get($refreshToken);
if (!$tokenData) {
return webResponse(-5, 'refreshToken已过期,请重新登录', [], 402);
}
if (!$this->validateTokenData($refreshToken,$tokenData['password'])) {
Cache::store('token')->delete($refreshToken);
return webResponse(-5, 'refreshToken验证不通过', [], 402);
}
$newTokens = $this->createNewTokens($tokenData);
$this->updateTokenCache($refreshToken, $newTokens, $tokenData);
return webResponse(0, 'success', $newTokens);
}
private function validateToken(string $token): bool
{
$tokenData = Cache::store('token')->get($token);
if (!$tokenData) {
return false;
}
return $this->validateTokenData($token,$tokenData['password']);
}
private function validateTokenData(string $token,string $password): bool
{
return verifyToken($token, $password);
}
private function createNewTokens(array $tokenData): array
{
$newAuthToken = createToken($tokenData,config('token.auth_token'));
$newRefreshToken = createToken($tokenData, config('token.refresh_token'));
return [
'new_auth_token' => $newAuthToken,
'new_refresh_token' => $newRefreshToken,
];
}
private function updateTokenCache(string $oldRefreshToken, array $newTokens, array $tokenData): void
{
Cache::store('token')->delete($oldRefreshToken);
$tokenData['long_token']=$newTokens['new_refresh_token'];
Cache::store('token')->set($newTokens['new_auth_token'], $tokenData,config('token.auth_token'));
unset($tokenData['long_token']);
Cache::store('token')->set($newTokens['new_refresh_token'], $tokenData,config('token.refresh_token'));
}
}
登录 这里将用户信息提前缓存到了redis中 在这里直接通过redis对登录信息进行验证
Login类
<?php
namespace app\controller\web;
use think\facade\Cache;
use think\facade\Request;
use app\service\web\LoginService as Service;
use think\Response;
class Login
{
public function index(): Response
{
$loginData = [
'username' => Request::post('username'),
'password' => Request::post('password')
];
$service = new Service();
return $service->login($loginData);
}
public function logout(): Response
{
$auth_token = Request::header('AuthToken');
$auth_token_info = Cache::store('token')->get($auth_token);
$refresh_token = $auth_token_info['long_token'];
Cache::store('token')->delete($auth_token);
Cache::store('token')->delete($refresh_token);
return webResponse(0,'success');
}
}
LoginService类
<?php
namespace app\service\web;
use app\validate\TelValidate;
use think\exception\ValidateException;
use think\facade\Cache;
use think\Response;
class LoginService
{
public function login($data): Response
{
try {
validate(TelValidate::class)
->scene('login')
->check($data);
} catch (ValidateException $e) {
return webResponse(-9,$e->getError());
}
$username_in_redis = Cache::store('admin')->get($data['username']);
if(!$username_in_redis){
return webResponse(-1,'用户名错误',[],410);
}
if($username_in_redis['status'] != 1){
return webResponse(-3,'用户名状态异常',[],410);
}
if(!empty($username_in_redis['long_token'])){
Cache::store('token')->delete($username_in_redis['long_token']);
}
$md5_password = password($data['password'],$username_in_redis['salt']);
if($md5_password != $username_in_redis['password']){
return webResponse(-2,'密码错误',[],410);
}
$auth_token= createToken($username_in_redis,config('token.auth_token'));
$refresh_token=createToken($username_in_redis,config('token.refresh_token'));
$response_data = [
'auth_token'=>$auth_token,
'refresh_token'=>$refresh_token
];
$username_in_redis['long_token'] = $refresh_token;
Cache::store('token')->set($auth_token,$username_in_redis,config('token.auth_token'));
unset($username_in_redis['long_token']);
Cache::store('token')->set($refresh_token,$username_in_redis,config('token.refresh_token'));
$username_in_redis['long_token']=$refresh_token;
Cache::store('admin')->set($username_in_redis['username'],$username_in_redis);
return webResponse(0,'success',$response_data);
}
}
将jwt写入到了common.php中
jwt加密
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
if(!function_exists('createToken')){
function createToken($params,int $expiration=600): string
{
$expireTime = time() + $expiration;
$payload = [
'iss' => 'zero_step',
'iat' => time(),
'user' => $params['username'],
'uid'=>$params['id'],
'exp' => $expireTime
];
return JWT::encode($payload, $params['password'], 'HS256');
}
}
jwt解密
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
if(!function_exists('verifyToken')){
function verifyToken(string $token, string $password): bool
{
try {
JWT::decode($token, new Key($password, 'HS256'));
return true;
} catch (InvalidArgumentException|UnexpectedValueException|DomainException $e) {
return false;
}
}
}
代码有不足之处及提高的地方,欢迎各位码有私信留言。