双Token方案实现Token自动续期(基于springboot+vue前后端分离项目)

文章目录

  • 前言
  • 一、双Token方案介绍
    • 1. 令牌类型与功能
    • 2.双Token方案的优点
    • 3.实现流程
  • 二、具体实现
    • 1.后端实现
      • 1.1 jwt工具类
      • 1.2 响应工具类
      • 1.3 实体类
      • 1.4 过滤器
      • 1.5 controller
      • 1.6 启动类
    • 2、前端实现
      • 2.1 登录页面
      • 2.2 index页面
      • 2.3 请求拦截器和响应拦截器
  • 效果展示


前言

更多jwt相关文章:
jwt理论介绍
springboot+vue项目中使用jwt实现登录认证

本篇文章的代码是在springboot+vue项目中使用jwt实现登录认证的基础上实现的Token自动续期的功能。


一、双Token方案介绍

双token解决方案是一种用于增强用户登录安全性和提升用户体验的认证机制。它主要涉及两个令牌:访问令牌(accessToken)和刷新令牌(refreshToken)。以下是对双token解决方案的详细介绍:

1. 令牌类型与功能

  • 访问令牌(accessToken):
    有效期较短,通常设置为较短的时间,如两小时或根据业务需求自定义(如10分钟)。 储存用户信息权限等,包含用户相关信息,如UserID、Username等。 用于前端与后端之间的通信认证,前端在每次请求时携带此令牌进行校验。
  • 刷新令牌(refreshToken):
    有效期较长,可以设置为一星期、一个月或更长时间,具体根据业务需求自定义。 不储存额外信息,只储存用户id,用于在accessToken过期后重新生成新的accessToken。 由于有效期长,因此降低了用户需要频繁登录的频率。
  • 2.双Token方案的优点

  • 增强安全性:通过短期有效的accessToken和长期有效的refreshToken的结合,即使accessToken泄露,攻击者也只能在有限时间内进行模拟用户行为,降低了安全风险。
  • 提升用户体验:由于refreshToken的存在,用户无需频繁登录,特别是在长时间操作或后台服务场景下,提高了用户体验。
  • 3.实现流程

  • 登录:用户输入用户名和密码进行登录,后端验证成功后生成accessToken和refreshToken,并发送给前端。
  • 请求校验:前端在每次请求时携带accessToken进行校验,如果accessToken有效,则允许请求继续;如果无效但refreshToken有效,则使用refreshToken重新生成accessToken。
  • 令牌刷新:当accessToken过期但refreshToken未过期时,前端可以使用refreshToken向后端请求新的accessToken,无需用户重新登录。
  • 登出:用户登出时,后端需要同时使accessToken和refreshToken失效,以确保用户登出后的安全性。
  • 二、具体实现

    1.后端实现

    1.1 jwt工具类

    package com.etime.util;import io.jsonwebtoken.*;import java.util.Date;
    import java.util.Map;
    import java.util.UUID;/*** @Date 2024/6/10 10:04* @Author liukang**/
    public class JwtUtil {
    //    private static long expire = 1000*60*5;// 单位是毫秒private static String secret = "secret";/*** 创建jwt* @author liukang* @date 10:36 2024/6/10* @param expire* @param map* @return java.lang.String**/public static String generateToken(long expire, Map map){// 床jwt构造器JwtBuilder jwtBuilder = Jwts.builder();// 生成jwt字符串String jwt = jwtBuilder//头部.setHeaderParam("typ","JWT").setHeaderParam("alg","HS256")// 载荷.setClaims(map) // 设置多个自定义数据  位置只能放在前面,如果放在后面,那前面的载荷会失效.setId(UUID.randomUUID().toString())// 唯一标识.setIssuer("liukang")// 签发人.setIssuedAt(new Date())// 签发时间.setSubject("jwtDemo")// 主题.setExpiration(new Date(System.currentTimeMillis()+expire))//过期时间// 自定义数据
    //                .claim("uname","liukang")// 签名.signWith(SignatureAlgorithm.HS256,secret).compact();return jwt;}/*** 创建jwt* @author liukang* @date 10:36 2024/6/10* @param expire* @return java.lang.String**/public static String generateToken(long expire){// 床jwt构造器JwtBuilder jwtBuilder = Jwts.builder();// 生成jwt字符串String jwt = jwtBuilder//头部.setHeaderParam("typ","JWT").setHeaderParam("alg","HS256")// 载荷.setId(UUID.randomUUID().toString())// 唯一标识.setIssuer("liukang")// 签发人.setIssuedAt(new Date())// 签发时间.setSubject("jwtDemo")// 主题.setExpiration(new Date(System.currentTimeMillis()+expire))//过期时间// 自定义数据
    //                .claim("uname","liukang")// 签名.signWith(SignatureAlgorithm.HS256,secret).compact();return jwt;}/*** 解析jwt* @author liukang* @date 10:36 2024/6/10* @param jwt* @return io.jsonwebtoken.Claims**/public static Claims parseToken(String jwt){Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt);Claims playload = claimsJws.getBody();return playload;}
    }

    1.2 响应工具类

    代码如下(示例):

    package com.etime.util;import com.etime.vo.ResponseModel;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.MediaType;import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;/*** @Date 2024/6/10 10:00* @Author liukang**/
    public class ResponseUtil {public static void write(ResponseModel rm, HttpServletResponse response) throws IOException {// 构造响应头response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding("utf-8");// 解决跨域问题 设置跨域头response.setHeader("Access-Control-Allow-Origin","*");// 输出流PrintWriter out = response.getWriter();// 输出out.write(new ObjectMapper().writeValueAsString(rm));// 关闭流out.close();}
    }

    1.3 实体类

    登录用户实体类

    package com.etime.entity;import lombok.Data;/*** @Date 2024/6/10 10:39* @Author liukang**/
    @Data
    public class User {private String username;private String password;
    }

    响应vo类

    package com.etime.vo;import lombok.Data;import java.util.Objects;/*** @Date 2024/6/10 10:37* @Author liukang**/
    @Data
    public class ResponseModel {private Integer code;private String msg;private Object token;public ResponseModel(Integer code, String msg, Object token) {this.code = code;this.msg = msg;this.token = token;}
    }

    1.4 过滤器

    package com.etime.filter;import com.etime.util.JwtUtil;
    import com.etime.util.ResponseUtil;
    import com.etime.vo.ResponseModel;
    import com.sun.deploy.net.HttpResponse;
    import org.springframework.http.HttpMethod;
    import org.springframework.http.HttpRequest;
    import org.springframework.util.StringUtils;import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;/*** @Description jwt过滤器* @Date 2024/6/10 9:46* @Author liukang**/
    @WebFilter(urlPatterns = "/*") // 过滤所有路径
    public class JwtFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {// 得到两个对象HttpServletRequest request =  (HttpServletRequest) servletRequest;HttpServletResponse response =  (HttpServletResponse) servletResponse;//直接放行if(HttpMethod.OPTIONS.toString().equals(request.getMethod())){filterChain.doFilter(request,response);return;}String requestURI = request.getRequestURI(); // 不含主机和端口号if(requestURI.contains("/login")){filterChain.doFilter(request,response);return;}// 得到请求头的信息(accessToken)String token = request.getHeader("accessToken");if(!StringUtils.hasText(token)){//响应前端错误的消息提示ResponseModel responseModel = new ResponseModel(500,"failure","令牌缺失!");ResponseUtil.write(responseModel,response);return;}// 解析Token信息try {JwtUtil.parseToken(token);}catch (Exception e){//响应前端错误的消息提示ResponseModel responseModel = new ResponseModel(401,"failure","令牌过期!");ResponseUtil.write(responseModel,response);return;}filterChain.doFilter(request,response);}
    }

    1.5 controller

    登录Controller

    package com.etime.controller;import com.etime.entity.User;
    import com.etime.util.JwtUtil;
    import com.etime.vo.ResponseModel;
    import org.springframework.web.bind.annotation.*;import java.util.HashMap;
    import java.util.Map;/*** @Date 2024/6/10 10:38* @Author liukang**/
    @RestController
    @CrossOrigin
    public class LoginController {@PostMapping("/login")public ResponseModel login(@RequestBody User user){Integer code = 200;String msg = "success";String accessToken = null;String refreshToken = null;Map tokenMap = new HashMap();if(user.getUsername().equals("admin")&&user.getPassword().equals("123")){// 生成jwtaccessToken = JwtUtil.generateToken(1000*10);// 设置有效期为10srefreshToken = JwtUtil.generateToken(1000*30);// 设置有效期为30stokenMap.put("accessToken",accessToken);tokenMap.put("refreshToken",refreshToken);}else {code = 500;msg = "failure";}return new ResponseModel(code,msg,tokenMap);}}

    测试请求Controller

    package com.etime.controller;import com.etime.vo.ResponseModel;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;/*** @Date 2024/6/10 12:51* @Author liukang**/
    @CrossOrigin
    @RestController
    public class TestController {@PostMapping("/test")public ResponseModel test() {return new ResponseModel(200,"success","测试请求接口成功!");}}

    刷新Token的Controller

    package com.etime.controller;import com.etime.util.JwtUtil;
    import com.etime.vo.ResponseModel;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
    import java.util.Map;/*** @Date 2024/6/10 15:48* @Author liukang**/
    @CrossOrigin
    @RestController
    public class NewTokenController {@GetMapping("/newToken")public ResponseModel newToken(){String accessToken = JwtUtil.generateToken(1000*10);String refreshToken = JwtUtil.generateToken(1000*30);Map tokenMap = new HashMap();tokenMap.put("accessToken",accessToken);tokenMap.put("refreshToken",refreshToken);return new ResponseModel(200,"success",tokenMap);}
    }

    1.6 启动类

    package com.etime;import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.web.servlet.ServletComponentScan;/*** @Author liukang* @Date 2022/7/4 11:32*/
    @SpringBootApplication
    @ServletComponentScan(basePackages = "com.etime.filter")// 这个包下激活WebFilter这个注解
    public class Application {public static void main(String[] args) {SpringApplication.run(Application.class);}
    }

    2、前端实现

    2.1 登录页面

    <template><div class="hello"><form>用户名:<input v-model="username"/><br>密码<input v-model="password" /><br><button @click="login">登录</button></form></div></template><script>export default {data () {return {username:'',password:'',}},methods:{login(){this.axios.post('http://localhost:8088/login',{username:this.username,password:this.password,}).then(response => {console.log(response.data);if(response.data.code==200){sessionStorage.setItem("accessToken",response.data.token.accessToken)sessionStorage.setItem("refreshToken",response.data.token.refreshToken)this.$router.push({ path: 'index'});}}).catch(error => {console.error(error);});}},}</script><style scoped></style>

    2.2 index页面

    <template><div><button @click="test">请求受保护的接口</button></div></template><script>
    import intercepterConfig from './js/config'export default {data () {return {}},methods:{test(){const accessToken = sessionStorage.getItem('accessToken')let token = nullif(accessToken){token = accessToken}// console.log(token)this.axios.post('http://localhost:8088/test',{},/*{headers:{accessToken:'token'}}*/).then(response => {// if(response.data.code==200){console.log(response.data);// }}).catch(error => {console.error(error);});},},}</script><style scoped></style>

    2.3 请求拦截器和响应拦截器

    import axios from "axios";
    //axios请求拦截器
    axios.interceptors.request.use(config=>{// 正确的请求拦截器let token = null;let url = config.url// url.indexOf('/newToken')==-1  如果是刷新Token的请求 不用在拦截器里面加accessToken 这个请求已经在请求头中设置accessToken,加了会覆盖if(sessionStorage.getItem('accessToken')!=null && url.indexOf('/newToken')==-1){token = sessionStorage.getItem('accessToken')config.headers['accessToken'] = token}// 加入头信息的配置return config // 这句没写请求会发不出去},error=>{ // 出现异常的请求拦截器return Promise.reject(error)})
    // axios响应拦截器
    axios.interceptors.response.use(async res => {// 判断 401状态码 自动续期if (res.data.code == 401 &&!res.config.isRefresh) {//!res.config.isRefresh  不是刷新Token的请求才拦截 是则不拦截// 1.自动续期const res2 = await getNewToken()if(res2.data.code == 200){console.log('自动续期成功'+new Date().toLocaleString())// 2.更新sessionStorage里面的Token   没有这一步会死循环sessionStorage.setItem('accessToken',res2.data.token.accessToken)sessionStorage.setItem('refreshToken',res2.data.token.refreshToken)//3.重新发送请求res = await axios.request(res.config)// res.config 代表请求的所有参数(这里是上一次请求的所有参数),包括url和携带的所有数据}}return res     // 将重新请求的响应作为响应返回},error=>{return Promise.reject(error)})function getNewToken(){let url = "http://localhost:8088/newToken"let token = nullif(sessionStorage.getItem('refreshToken')!=null){token = sessionStorage.getItem('refreshToken')}return  axios.get(url,{headers:{accessToken:token},isRefresh:true})// 注意这里参数是accessToken:token  因为后端过滤器里面获取的是accessToken,所以要写这个,不然过滤器通不过过滤器
    }

    效果展示

    1.登录页面
    在这里插入图片描述
    2.输入用户名和密码点击【登录】
    在这里插入图片描述
    3.点击【请求受保护的资源】按钮
    在这里插入图片描述
    3.等待10秒,accessToken过期,但refreshToken未过期时,点击【请求受保护的资源】按钮
    在这里插入图片描述
    4.等待30秒后,refreshToken和accessToken都过期,再次点击【请求受保护的资源】按钮

    在这里插入图片描述


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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

短剧APP小程序开发之小程序内存管理挑战:短剧缓存与释放策略探讨(第二篇)

在上一篇帖子中&#xff0c;我们探讨了小程序内存管理的限制以及缓存策略的设计。本篇将进一步探讨释放策略的具体实现以及优化方案&#xff0c;以支持大量短剧内容的加载和播放。 释放策略的具体实现 监听内存警告&#xff1a;小程序提供了监听内存警告的API&#xff0c;开发…

【译】SQLAlchemy文档:SQLAlchemy 统一教程

SQLAlchemy Unified Tutorial SQLAlchemy 是 Python SQL工具包和ORM&#xff0c;它为应用程序开发人员提供了 SQL 的全部功能和灵活性。它提供了一整套企业级持久性模式&#xff0c;专为高效和高性能的数据库访问而设计。 SQLAlchemy呈现为两层API&#xff1a;Core和ORM&…

【记录】ChatGLM3-6B大模型部署、微调(一):部署

ChatGLM3介绍 源码连接&#xff1a; ChatGLM3 是智谱AI和清华大学 KEG 实验室联合发布的对话预训练模型。ChatGLM3-6B 是 ChatGLM3 系列中的开源模型&#xff0c;在保留了前两代模型对话流畅、部署门槛低等众多优秀特性的基础上&#xff0c;ChatGLM3-6B 引入了如下特性&#xf…

如何高效管理和监控 Elasticsearch 别名及索引?

0、引言 在 Elasticsearch 项目中&#xff0c;管理和监控索引是开发者的一项重要任务。 尤其是当我们需要在项目的管理部分展示索引和别名的统计信息时&#xff0c;了解如何有效地列出这些别名和索引显得尤为重要。 本篇博客将介绍几种在 Elasticsearch 中列出别名和索引的方法…

7.Nginx动静分离

介绍 把动态和静态请求分开,不能理解成只是单纯的把动态页面和静态页面物理分离。 动静分离从目前实现角度分为两种: 1.纯粹把静态文件独立成单独的域名,放在独立的静态资源服务器上,目前主流推崇的方案。 2.动态和静态文件混合在一起发布,通过nginx来分开。 通过loc…

【微信小程序】事件传参的两种方式

文章目录 1.什么是事件传参2.data-*方式传参3.mark自定义数据 1.什么是事件传参 事件传参:在触发事件时&#xff0c;将一些数据作为参数传递给事件处理函数的过程&#xff0c;就是事件传参 在微信小程序中&#xff0c;我们经常会在组件上添加一些自定义数据&#xff0c;然后在…

毕业年薪20w起!25届最近5南京邮电大学自动化考研院校分析

南京邮电大学 目录 一、学校学院专业简介 二、考试科目指定教材 三、近5年考研分数情况 四、近5年招生录取情况 五、最新一年分数段图表 六、历年真题PDF 七、初试大纲复试大纲 八、学费&奖学金&就业方向 一、学校学院专业简介 二、考试科目指定教材 1、考试…

点云格式转化:将 ros PointCloud2格式数据转为livox CustomMsg格式

前言 览沃科技有限公司&#xff08;Livox&#xff09;成立于2016年。为了革新激光雷达行业&#xff0c;Livox致力于提供高性能、低成本的激光雷达传感器。通过降低使用门槛和生产成本&#xff0c;Livox将激光雷达技术集成到更多产品和应用之中&#xff0c;从而为自动驾驶、智慧…

数据挖掘丨轻松应用RapidMiner机器学习内置数据分析案例模板详解(下篇)

RapidMiner 案例模板 RapidMiner 机器学习平台提供了一个可视化的操作界面&#xff0c;允许用户通过拖放的方式构建数据分析流程。RapidMiner目前内置了 13 种案例模板&#xff0c;这些模板是预定义的数据分析流程&#xff0c;可以帮助用户快速启动和执行常见的数据分析任务。 …

AI “黏土画风”轻松拿捏,手把手带你云端部署 ComfyUI

作者&#xff1a;鸥弋、筱姜 AI 绘画领域&#xff0c;Stable Diffusion WebUI、Midjourney 、DALL-E 都聚拢了一大批的应用开发者和艺术创作者。ComfyUI 出现时间略晚&#xff0c;但是它让创作者通过工作流的方式&#xff0c;实现自动化水平更高的 AI 生图流程&#xff0c;一面…

ISO17025认证是什么?怎么做?

ISO17025认证是一种国际通用的实验室质量管理体系认证&#xff0c;其目标是确保实验室的技术能力、管理水平以及测试结果的可靠性和准确性达到国际认可的标准。该认证由国际标准化组织&#xff08;ISO&#xff09;和国际电工委员会&#xff08;IEC&#xff09;联合发布&#xf…

不停“整活”的零食很忙,怎么就跨入万店时代了?

6月12日&#xff0c;合并后的零食很忙、赵一鸣零食宣布&#xff0c;全国门店总数已突破10000家。同时&#xff0c;集团名称也变更为鸣鸣很忙集团。根据第三方机构弗若斯特沙利文认证&#xff0c;鸣鸣很忙集团全国门店数位居零食连锁行业第一。 在此之前&#xff0c;尽管零食很…

Photoshop 2024 mac/win版:探索图像处理的全新境界

Photoshop 2024是Adobe推出的最新图像处理与设计软件&#xff0c;它在继承了前作所有优秀特性的基础上&#xff0c;实现了多个方面的质的飞跃。这款软件凭借其卓越的图像处理性能、丰富的创意工具以及精确的选区编辑功能&#xff0c;成为了图像处理领域的佼佼者。 Photoshop 2…

Spring的循环依赖

循环依赖概述 循环依赖其实也很好理解&#xff0c;可以将这个词拆分成两部分&#xff0c;一个是循环&#xff0c;一个是依赖。循环&#xff0c;顾名思义就是指形成了一个闭合环路&#xff0c;也就是闭环。依赖就是指某个事件的发生要依赖另一个事件。 在Spring中的循环依赖就…

CTFHUB-SQL注入-Cookie注入

由于本关是cookie注入&#xff0c;就不浪费时间判断注入了&#xff0c;在该页面使用 burp工具 抓包&#xff0c;修改cookie后面&#xff0c;加上SQL语句&#xff0c;关掉burp抓包&#xff0c;就可以在题目页面显示结果了 判断字段数量 发现字段数量是2列 使用id-1 union sele…

NewStarCTF_RE(week1,2)

[NewStarCTF 2023 公开赛道]easy_RE ida 可能会把 一个数组或字符串拆开&#xff0c;可以通过计算地址&#xff0c;知道是一起的 也有的会藏在汇编窗口 Segments IDA的Segments窗口 &#xff1a;shiftf7 https://www.cnblogs.com/sch01ar/p/9477697.html ida 各种窗口也是需要…

Prometheus监控系统

目录 一、Prometheus简介 1.Prometheus概念 ①Prometheus概念 ②监控通知流程 ③监控系统的数据产生流程 ④zabbix和prometheus 区别 ⑤TSDB 作为 Prometheus 的存储引擎完美契合了监控数据的应用场景 &#xff12;.Prometheus的基础组件 ①如何采集数据exporter组件 …

PgSQL技术内幕 - psql与服务端连接与交互机制

PgSQL技术内幕 - 客户端psql与服务端连接与交互机制 简单来说&#xff0c;PgSQL的psql客户端向服务端发起连接请求&#xff0c;服务端接收到请求后&#xff0c;fork出一个子进程&#xff0c;之后由该子进程和客户端进行交互&#xff0c;处理客户端的SQL等&#xff0c;并将结果返…

Python第二语言(八、Python包)

目录 1. 什么是Python包 2. 创包步骤 2.1 new包 2.2 查看创建的包 2.3 拖动文件到包下 3. 导入包 4. 安装第三方包 4.1 什么是第三方包 4.2 安装第三方包-pip 4.3 pip网络优化 1. 什么是Python包 包下有__init__.py就是包&#xff0c;无__init__.py就是文件夹。于Ja…

Linux常用基本命令-操作

目录 一、shell 1、什么是shell 二、Linux基本的命令分类 1、内部命令和外部命令 2、查看内部命令 2.1、help命令 2.2、enable 命令 2.3、type命令 2.4、whereis命令 2.5、which 命令 2.6、hash缓存 ​编辑 三、Linux常用命令 1、Linux命令格式 2、编辑Linux命…