VUE项目双token刷新 vue项目token无感刷新

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和RefreshToken
        $authToken = Request::header('AuthToken');
        $refreshToken = Request::header('RefreshToken');
        
        // 如果AuthToken不存在,则尝试使用RefreshToken
        if (!$authToken) {
            if(!$refreshToken){
                return webResponse(-5, '未获取到refreshToken,请重新登录', [], 402);
            }
            return $this->handleRefreshToken($refreshToken);
        }
        
        // 验证AuthToken
        if (!$this->validateToken($authToken)) {
            return webResponse(-5, '暂无权限', [], 401);
        }

        return $next($request);
    }
    
    /**
     * 处理RefreshToken逻辑
     * @param string $refreshToken
     * @return Response
     * @throws InvalidArgumentException
     */
    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);
        }
        
        // 创建新的AuthToken和RefreshToken
        $newTokens = $this->createNewTokens($tokenData);
        
        // 更新缓存
        $this->updateTokenCache($refreshToken, $newTokens, $tokenData);
        
        return webResponse(0, 'success', $newTokens);
    }
    
    /**
     * 验证Token是否有效
     *
     * @param string $token
     * @return bool
     * @throws InvalidArgumentException
     */
    private function validateToken(string $token): bool
    {
        $tokenData = Cache::store('token')->get($token);
        if (!$tokenData) {
            return false;
        }
        return $this->validateTokenData($token,$tokenData['password']);
    }
    
    /**
     * 验证Token数据
     * @param string $token
     * @param string $password
     * @return bool
     */
    private function validateTokenData(string $token,string $password): bool
    {
        return verifyToken($token, $password);
    }
    
    /**
     * 创建新的Token
     *
     * @param array $tokenData
     * @return array
     */
    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,
        ];
    }
    
    /**
     * 更新Token缓存
     *
     * @param string $oldRefreshToken
     * @param array $newTokens
     * @param array $tokenData
     * @throws InvalidArgumentException
     */
    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
    {
//        print_r();exit();
        //验证用户名合规性 验证是否为手机号
        try {
            validate(TelValidate::class)
                ->scene('login')
                ->check($data);
        } catch (ValidateException $e) {
            // 验证失败 输出错误信息
           return webResponse(-9,$e->getError());
        }
        //从redis中验证用户名是否存在
        $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);
        }
        //如果合法,生成token
        $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);
        //双token
        return webResponse(0,'success',$response_data);
    }
}

将jwt写入到了common.php中

jwt加密

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
if(!function_exists('createToken')){
    /**
     * 生成 JWT Token
     * @param $params
     * @param int $expiration 过期时间(秒),默认为 3600 秒
     * @return string 生成的 JWT Token
     */
    function createToken($params,int $expiration=600): string
    {
        $expireTime = time() + $expiration;
        
        // 构建负载数据
        $payload = [
            'iss' => 'zero_step',  // 签发者
            'iat' => time(), // 签发时间
            'user' => $params['username'], // 目标用户
            'uid'=>$params['id'],
            'exp' => $expireTime
        ];
        // 使用 JWT 类库生成 JWT Token,并返回
        return JWT::encode($payload, $params['password'], 'HS256');
    }
}

jwt解密

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
if(!function_exists('verifyToken')){
    /**
     * @param string $token
     * @param string $password
     * @return bool
     */
    function verifyToken(string $token, string $password): bool
    {
        // 使用 JWT 类库解码 JWT Token,并验证是否过期
        try {
           JWT::decode($token, new Key($password, 'HS256'));
            return true;
        } catch (InvalidArgumentException|UnexpectedValueException|DomainException $e) {
            return false;
        }
    }
}

代码有不足之处及提高的地方,欢迎各位码有私信留言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue中实现无感刷新token的方法可以通过拦截器来实现。首先,你可以在axios的响应拦截器中判断返回的状态码是否为401,如果是的话,说明token已过期。然后,你可以设置一个变量isRefreshing来控制是否正在刷新token的状态。当第一个请求触发token过期时,isRefreshing会被设置为true,然后发送刷新token的请求,获取最新的token并进行覆盖。在刷新token的过程中,其他请求会被拦截并等待token刷新完成后再继续发送。当token刷新完成后,isRefreshing会被设置为false,其他请求会继续发送。以下是一个示例代码: ```javascript import axios from 'axios' let isRefreshing = false axios.interceptors.response.use( response => { if (response.status === 401) { if (!isRefreshing) { isRefreshing = true return refreshToken().then(res => { const { token } = res.data // 更新token response.headers.Authorization = `${token}` // 继续发送之前被拦截的请求 return axios(response.config) }).catch(err => { // 刷新token失败,跳转到登录页 router.push('/login') return Promise.reject(err) }).finally(() => { isRefreshing = false }) } } return response && response.data }, error => { // 处理错误 return Promise.reject(error) } ) ``` 在上述代码中,refreshToken()是一个发送刷新token请求的函数,你可以根据你的实际情况进行实现。当token过期时,其他请求会被拦截并等待token刷新完成后再继续发送。这样就实现了Vue中的无感刷新token的功能。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [VUE中拦截请求并无感刷新token](https://blog.csdn.net/Wangyuan_wo/article/details/121209540)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Vue 无感刷新token](https://blog.csdn.net/weixin_45308405/article/details/127643153)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [VUE前端实现token无感刷新,即refresh_token](https://blog.csdn.net/yu1431/article/details/130835868)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值