双 Token 无感刷新机制实现

✅作者简介:热爱Java后端开发的一名学习者,大家可以跟我一起讨论各种问题喔。
🍎个人主页:Hhzzy99
🍊个人信条:坚持就是胜利!
💞当前专栏:项目实践
🥭本文内容:实现双Token的无感刷新。

双 Token 无感刷新机制实现

      • 后端依赖
      • 安全配置
      • Jwt过滤器 *
      • 前端的配置

在现代 Web 应用开发中,前后端分离已经成为一种趋势。Vue.js 作为前端框架,Java
作为后端语言的组合被广泛应用。在用户认证方面,JWT因为其无状态、易于扩展等特点也备受青睐。本文将详细介绍如何在 Vue 前端和 Java
后端实现双 Token 的无感刷新机制。

后端依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <!-- 其他依赖 -->
</dependencies>

安全配置

配置Jwt过滤器,以及认证失败过滤器。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;
	/**
	* Jwt过滤器
	*/
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .cors().and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .headers().cacheControl().disable().and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers("/user/login", "/user/forgetPassword/**", "/user/sendUpdatePasswordEmailCode/**", "/user/register", "/swagger-ui.html", "/user/sendEmailLoginCode", "/user/verifyEmailLoginCode/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();

        return http.build();
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 使用BCryptPasswordEncoder作为security默认的passwordEncoder
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Jwt过滤器 *

这个过滤器是实现token刷新机制的核心,每次前端的请求携带accessToken与refreshToken过来,此过滤器拿到之后,先对accessToken进行解析,如果解析失败(过期),那么接下来会对refreshToken进行解析,解析完成之后,如果没有过期,就会生成新的accessToken与refreshToken返回给前端,并且设置一个新的请求头Token-Refreshed,值可以随便设,前端能拿到就好。

package com.hblog.backend.config;

import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hblog.backend.entity.LoginUser;
import com.hblog.backend.entity.User;
import com.hblog.backend.exception.BusinessException;
import com.hblog.backend.exception.EnumException;
import com.hblog.backend.mapper.IUserMapper;
import com.hblog.backend.response.CommonResponse;
import com.hblog.backend.utli.*;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName: JwtAuthenticationFilter
 * @author: Hhzzy99
 * @date: 2024/3/17 16:09
 * description:继承每个请求只会经过一次的过滤器
 */
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Value("${token.expiration}")
    private Long expiration;

    @Autowired
    private IUserMapper userMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 获取当前请求路径
        String requestPath = request.getRequestURI();

        // 排除不需要过滤的路径
        if (requestPath.equals("/user/login") || requestPath.equals("/user/register")) {
            filterChain.doFilter(request, response);
            return;
        }

        // 获取token
        String accessToken = request.getHeader("access_token");
        String refreshToken = request.getHeader("refresh_token");
        if ("null".equals(accessToken) || "".equals(accessToken) || "undefined".equals(accessToken) || null == accessToken) {
            // 放行
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String userId = "";
        boolean isRefresh = false;

        try {
            userId = JwtUtils.parseJWT(accessToken).getSubject();
        } catch (Exception e) {
            isRefresh = true;
            e.printStackTrace();
        }
        if (isRefresh) {
            try {
                userId = JwtUtils.parseJWT(refreshToken).getSubject();
                accessToken = JwtUtils.createJWT(userId);
                refreshToken = JwtUtils.createRefreshToken(userId);
                User loginUser = userMapper.getUserById(Long.valueOf(userId));
                Integer ttl = expiration.intValue() / 1000;
                log.warn("@@@@@@@@@@@@@@@@@@刷新token@@@@@@@@@@@@@@@@@@@@@");
                redisCache.setCacheObject("userInfo:" + userId, loginUser, ttl, TimeUnit.SECONDS);
                writeTokenResponse(response, accessToken, refreshToken);
                return;
            } catch (Exception e1) {
                throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);
            }
        }
        // 从redis里面获取用户信息
        User loginUser = redisCache.getCacheObject("userInfo:" + userId);
        if (Objects.isNull(loginUser)) {
            throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);
        }
        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);
    }

    private void writeTokenResponse(HttpServletResponse response, String accessToken, String refreshToken) throws IOException {
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("accessToken", accessToken);
        tokenMap.put("refreshToken", refreshToken);
        Map<String, String> headers = new HashMap<>();
        headers.put("Token-Refreshed", "true");
        CommonResponse<Map<String, String>> commonResponse = new CommonResponse<>(200, "Token refreshed successfully", tokenMap);
        WebUtil.renderString(response, headers, JSON.toJSONString(commonResponse));
    }
}


前端的配置

前端对所有的axios请求进行全局配置,先在每次请求的时候设置好请求头accessToken与refreshToken,并且将每次请求都保存起来,如果在请求时后端解析到accessToken失效,并且返回了新的accessToken与refreshToken,在请求头拿到了后端设置好的Token-Refreshed,此时就可以重新将新的accessToken与refreshToken保存在浏览器本地,并且重新发送之前保存好的请求,就可以实现无感刷新。

request.js

import axios from 'axios'
import {ref} from "vue";

// create an axios instance
const service = axios.create({
    baseURL: '/api', // url = base url + request url
    timeout: 20000 // request timeout
})

const retryRequest = ref(null)

// request interceptor
service.interceptors.request.use(
    config => {
        // 加入头信息配置
        if (localStorage.getItem("access_token") !== null && localStorage.getItem("access_token") !== undefined){
            config.headers['access_token'] = localStorage.getItem("access_token")
        }
        if (localStorage.getItem("refresh_token") !== null && localStorage.getItem("refresh_token") !== undefined){
            config.headers['refresh_token'] = localStorage.getItem("refresh_token")
        }
        retryRequest.value = config
        return config
    }
)

// response interceptor
service.interceptors.response.use(
    response => {
        if (response.headers['token-refreshed']) {
            console.log('Token刷新成功');
            // 如果有Token-Refreshed头部,更新本地存储中的Token
            localStorage.setItem('access_token', response.data.data.accessToken);
            localStorage.setItem('refresh_token', response.data.data.refreshToken);
            console.log("继续")
            // 继续发送原始请求
            return axios(retryRequest.value)
        }
        return response;
    },
    async error => {
        const originalRequest = error.config;

        // 如果是Token过期导致的401错误,并且没有retry标记,尝试刷新Token
        if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;

            const refreshToken = localStorage.getItem('refresh_token');
            if (refreshToken) {
                try {
                    const response = await axios.post('/refresh-token', { refreshToken });
                    const { accessToken, refreshToken: newRefreshToken } = response.data.data;
                    localStorage.setItem('access_token', accessToken);
                    localStorage.setItem('refresh_token', newRefreshToken);

                    // 更新原始请求的Authorization头部
                    originalRequest.headers['access_token'] = accessToken;
                    // 重新发送原始请求
                    return instance(originalRequest);
                } catch (refreshError) {
                    // 刷新Token失败,跳转到登录页或执行其他处理
                    console.error('Token刷新失败:', refreshError);
                    // 这里可以跳转到登录页或者执行其他处理
                }
            }
        }

        return Promise.reject(error);
    }
)

export default service



通过以上步骤,我们就可以实现双 Token 无感刷新机制。该机制通过短期有效的访问 Token 和长期有效的刷新 Token 相结合,在 Token 过期时自动刷新。

本示例仅展示了基础的实现方式,实际生产环境中还需要考虑更多安全性和健壮性的问题。

希望这篇文章能帮助你更好地理解和实现 JWT 双 Token 无感刷新机制。如有任何问题或建议,欢迎在评论区留言讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/780041.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

批量文本编辑管理神器:一键修改多处内容,轻松转换编码,助力工作效率飞跃提升!

在信息爆炸的时代&#xff0c;文本处理已成为我们日常工作中不可或缺的一部分。无论是处理文档、整理数据还是编辑资料&#xff0c;都需要对大量的文本进行管理和修改。然而&#xff0c;传统的文本编辑方式往往效率低下&#xff0c;容易出错&#xff0c;难以满足现代工作的高效…

QListWidget 缩略图IconMode示例

1、实现的效果如下&#xff1a; 2、实现代码 &#xff08;1&#xff09;头文件 #pragma once #include <QtWidgets/QMainWindow> #include "ui_QListViewDemo.h" enum ListDataType { ldtNone -1, ldtOne 0, ldtTwo 1, }; struct ListData…

树莓派4B_OpenCv学习笔记19:OpenCV舵机云台物体追踪

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; Python 版本3.7.3&#xff1a; ​​ 今日学习&#xff1…

Apache Seata应用侧启动过程剖析——RM TM如何与TC建立连接

本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Apache Seata应用侧启动过程剖析——RM & TM如何与TC建立连接 前言 看过官网 README 的第…

Python | Leetcode Python题解之第217题存在重复元素

题目&#xff1a; 题解&#xff1a; class Solution(object):def containsDuplicate(self, nums):if len(set(nums)) ! len(nums):return Trueelse:return False

TCP一定可靠吗

背景 公司某个服务发送TCP报文后,得到的响应是非预期数据 原因竟然是:TCP包的 payload 数据某个bit位被翻转,但是 checksum 的值一样,错误的包被分发给了上层服务 Checksum介绍 IP 头有自己的 Checksum,TCP、UDP 也有自己的 Checksum,分别校验不同部分的数据 IP 头的 …

赛元单片机开发工具SOC_Programming_Tool_Enhance_V1.50 分享

下载地址&#xff1a; SOC_Programming_Tool_Enhance_V1.50(LIB0D30).rar: https://545c.com/f/45573183-1320016694-557ebd?p7526 (访问密码: 7526)

使用Spring Boot和自定义缓存注解优化应用性能

在现代应用开发中&#xff0c;缓存是提高系统性能和响应速度的关键技术之一。Spring Boot提供了强大的缓存支持&#xff0c;但有时我们需要更灵活的缓存控制。本文将介绍如何使用Spring Boot和自定义缓存注解来优化应用性能。 1. 为什么需要自定义缓存注解&#xff1f; Sprin…

干货 | 2024大模型场景下智算平台的设计与优化实践(免费下载)

诚挚邀请您微信扫描以下二维码加入方案驿站知识星球&#xff0c;获取上万份PPT/WORD解决方案&#xff01;&#xff01;&#xff01;感谢支持&#xff01;&#xff01;&#xff01;

在linux系统centos上面安装php7gmp扩展

ps:在ubuntu上面安装gmp(最简单) $ sudo apt-get install php7.0-gmp然后再php.ini添加extensionphp_gmp.so <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<…

Vue3中生成本地pdf并下载

1. 前言 前端中经常会遇到在系统中根据数据导出一个pdf文件出来,一般都是后端来实现的,既然后端可以实现,前端为什么就不行呢,正好有一次也写了这个需求,就写了个小demo 示例图: 2. 实现步骤 首先下载html2pdf.js这个库yarn add html2pdf.js // 或 npm i html2pdf.js在项…

欧洲杯数据控@20240706

点击标题下「蓝色微信名」可快速关注 上半区西班牙、法国脱颖而出&#xff0c;将会争夺一个决赛的席位&#xff0c;下半区两场比赛&#xff0c;将会决出另外两支进入半决赛的球队&#xff0c; 今日射手榜&#xff0c;随着球队的淘汰&#xff0c;能争夺金靴的球员越来越少了&…

17.优化算法之解决拓扑排序4

0.基础 1.课程表1 207. 课程表 - 力扣&#xff08;LeetCode&#xff09; class Solution {public boolean canFinish(int n, int[][] p) {// 1. 准备⼯作int[] in new int[n]; // 统计每⼀个顶点的⼊度Map<Integer, List<Integer>> edges new HashMap<>…

整洁架构SOLID-开闭原则(OCP)

文章目录 1 定义2 最佳实践2.1 需求2.2 需求变更2.3 变更原则2.4 实现逻辑2.4.1 组件化2.4.2 组件关系 2.5 依赖方向的控制 3 本章小结 1 定义 开闭原则(OCP)是Bertrand Meyer在1988年提出的&#xff0c;该设计原则认为&#xff1a; 设计良好的计算机软件应该易于扩展&#xf…

认识并理解webSocket

今天逛牛客&#xff0c;看到有大佬分享说前端面试的时候遇到了关于webSocket的问题&#xff0c;一看自己都没见过这个知识点&#xff0c;赶紧学习一下&#xff0c;在此记录&#xff01; WebSocket 是一种网络通信协议&#xff0c;提供了全双工通信渠道&#xff0c;即客户端和服…

Unity3D游戏 RPG

丛林探险游戏 人物进行探险游戏 拥有登录&#xff0c;首页&#xff0c;3D物体旋转浏览的功能&#xff0c;还能进行种植树等功能

GD32 MCU ADC采样率如何计算?

大家在使用ADC采样的时候是否计算过ADC的采样率&#xff0c;这个问题非常关键&#xff01; 以下为GD32F303系列MCU中有关ADC的参数&#xff0c;其中ADC时钟最大值为40MHz&#xff0c;12位分辨率下最大采样率为2.86MSPS.如果ADC时钟超频的话&#xff0c;可能会造成ADC采样异常&…

【总线】AXI4第七课时:AXI的额外的控制信息(PROT和CACHE)

大家好,欢迎来到今天的总线学习时间!如果你对电子设计、特别是FPGA和SoC设计感兴趣&#xff0c;那你绝对不能错过我们今天的主角——AXI4总线。作为ARM公司AMBA总线家族中的佼佼者&#xff0c;AXI4以其高性能和高度可扩展性&#xff0c;成为了现代电子系统中不可或缺的通信桥梁…

Vue3.js“非原始值”响应式实现基本原理笔记(二)

如果您觉得这篇文章有帮助的话&#xff01;给个点赞和评论支持下吧&#xff0c;感谢~ 作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主 此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来 书籍&a…

STM32F103C8T6核心板原理图和PCB分享

PCB图 原理图 资料下载地址&#xff1a; 原理图PCB库: https://545c.com/d/45573183-61875742-29897c?p7526 (访问密码: 7526)