springboot+vue實(shí)現(xiàn)Token自動(dòng)續(xù)期(雙Token方案)
本篇文章的代碼是在springboot+vue項(xiàng)目中使用jwt實(shí)現(xiàn)登錄認(rèn)證的基礎(chǔ)上實(shí)現(xiàn)的Token自動(dòng)續(xù)期的功能。
一、雙Token方案介紹
雙token解決方案是一種用于增強(qiáng)用戶登錄安全性和提升用戶體驗(yàn)的認(rèn)證機(jī)制。它主要涉及兩個(gè)令牌:訪問(wèn)令牌(accessToken)和刷新令牌(refreshToken)。以下是對(duì)雙token解決方案的詳細(xì)介紹:
1. 令牌類(lèi)型與功能
訪問(wèn)令牌(accessToken):
有效期較短,通常設(shè)置為較短的時(shí)間,如兩小時(shí)或根據(jù)業(yè)務(wù)需求自定義(如10分鐘)。 儲(chǔ)存用戶信息權(quán)限等,包含用戶相關(guān)信息,如UserID、Username等。 用于前端與后端之間的通信認(rèn)證,前端在每次請(qǐng)求時(shí)攜帶此令牌進(jìn)行校驗(yàn)。
刷新令牌(refreshToken):
有效期較長(zhǎng),可以設(shè)置為一星期、一個(gè)月或更長(zhǎng)時(shí)間,具體根據(jù)業(yè)務(wù)需求自定義。 不儲(chǔ)存額外信息,只儲(chǔ)存用戶id,用于在accessToken過(guò)期后重新生成新的accessToken。 由于有效期長(zhǎng),因此降低了用戶需要頻繁登錄的頻率。
2.雙Token方案的優(yōu)點(diǎn)
增強(qiáng)安全性:通過(guò)短期有效的accessToken和長(zhǎng)期有效的refreshToken的結(jié)合,即使accessToken泄露,攻擊者也只能在有限時(shí)間內(nèi)進(jìn)行模擬用戶行為,降低了安全風(fēng)險(xiǎn)。
提升用戶體驗(yàn):由于refreshToken的存在,用戶無(wú)需頻繁登錄,特別是在長(zhǎng)時(shí)間操作或后臺(tái)服務(wù)場(chǎng)景下,提高了用戶體驗(yàn)。
3.實(shí)現(xiàn)流程
登錄:用戶輸入用戶名和密碼進(jìn)行登錄,后端驗(yàn)證成功后生成accessToken和refreshToken,并發(fā)送給前端。
請(qǐng)求校驗(yàn):前端在每次請(qǐng)求時(shí)攜帶accessToken進(jìn)行校驗(yàn),如果accessToken有效,則允許請(qǐng)求繼續(xù);如果無(wú)效但refreshToken有效,則使用refreshToken重新生成accessToken。
令牌刷新:當(dāng)accessToken過(guò)期但refreshToken未過(guò)期時(shí),前端可以使用refreshToken向后端請(qǐng)求新的accessToken,無(wú)需用戶重新登錄。
登出:用戶登出時(shí),后端需要同時(shí)使accessToken和refreshToken失效,以確保用戶登出后的安全性。
二、具體實(shí)現(xiàn)
1.后端實(shí)現(xiàn)
1.1 jwt工具類(lèi)
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";
/**
* 創(chuàng)建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構(gòu)造器
JwtBuilder jwtBuilder = Jwts.builder();
// 生成jwt字符串
String jwt = jwtBuilder
//頭部
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
// 載荷
.setClaims(map) // 設(shè)置多個(gè)自定義數(shù)據(jù) 位置只能放在前面,如果放在后面,那前面的載荷會(huì)失效
.setId(UUID.randomUUID().toString())// 唯一標(biāo)識(shí)
.setIssuer("liukang")// 簽發(fā)人
.setIssuedAt(new Date())// 簽發(fā)時(shí)間
.setSubject("jwtDemo")// 主題
.setExpiration(new Date(System.currentTimeMillis()+expire))//過(guò)期時(shí)間
// 自定義數(shù)據(jù)
// .claim("uname","liukang")
// 簽名
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
return jwt;
}
/**
* 創(chuàng)建jwt
* @author liukang
* @date 10:36 2024/6/10
* @param expire
* @return java.lang.String
**/
public static String generateToken(long expire){
// 床jwt構(gòu)造器
JwtBuilder jwtBuilder = Jwts.builder();
// 生成jwt字符串
String jwt = jwtBuilder
//頭部
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
// 載荷
.setId(UUID.randomUUID().toString())// 唯一標(biāo)識(shí)
.setIssuer("liukang")// 簽發(fā)人
.setIssuedAt(new Date())// 簽發(fā)時(shí)間
.setSubject("jwtDemo")// 主題
.setExpiration(new Date(System.currentTimeMillis()+expire))//過(guò)期時(shí)間
// 自定義數(shù)據(jù)
// .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 響應(yīng)工具類(lèi)
代碼如下(示例):
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 {
// 構(gòu)造響應(yīng)頭
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("utf-8");
// 解決跨域問(wèn)題 設(shè)置跨域頭
response.setHeader("Access-Control-Allow-Origin","*");
// 輸出流
PrintWriter out = response.getWriter();
// 輸出
out.write(new ObjectMapper().writeValueAsString(rm));
// 關(guān)閉流
out.close();
}
}
1.3 實(shí)體類(lèi)
登錄用戶實(shí)體類(lèi)
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;
}
響應(yīng)vo類(lèi)
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 過(guò)濾器
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過(guò)濾器
* @Date 2024/6/10 9:46
* @Author liukang
**/
@WebFilter(urlPatterns = "/*") // 過(guò)濾所有路徑
public class JwtFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 得到兩個(gè)對(duì)象
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//直接放行
if(HttpMethod.OPTIONS.toString().equals(request.getMethod())){
filterChain.doFilter(request,response);
return;
}
String requestURI = request.getRequestURI(); // 不含主機(jī)和端口號(hào)
if(requestURI.contains("/login")){
filterChain.doFilter(request,response);
return;
}
// 得到請(qǐng)求頭的信息(accessToken)
String token = request.getHeader("accessToken");
if(!StringUtils.hasText(token)){
//響應(yīng)前端錯(cuò)誤的消息提示
ResponseModel responseModel = new ResponseModel(500,"failure","令牌缺失!");
ResponseUtil.write(responseModel,response);
return;
}
// 解析Token信息
try {
JwtUtil.parseToken(token);
}catch (Exception e){
//響應(yīng)前端錯(cuò)誤的消息提示
ResponseModel responseModel = new ResponseModel(401,"failure","令牌過(guò)期!");
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")){
// 生成jwt
accessToken = JwtUtil.generateToken(1000*10);// 設(shè)置有效期為10s
refreshToken = JwtUtil.generateToken(1000*30);// 設(shè)置有效期為30s
tokenMap.put("accessToken",accessToken);
tokenMap.put("refreshToken",refreshToken);
}else {
code = 500;
msg = "failure";
}
return new ResponseModel(code,msg,tokenMap);
}
}
測(cè)試請(qǐng)求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","測(cè)試請(qǐng)求接口成功!");
}
}
刷新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 啟動(dòng)類(lèi)
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")// 這個(gè)包下激活WebFilter這個(gè)注解
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
2、前端實(shí)現(xiàn)
2.1 登錄頁(yè)面
<template>
<div class="hello">
<form>
用戶名:<input v-model="username"/>
密碼<input v-model="password" />
<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頁(yè)面
<template>
<div>
<button @click="test">請(qǐng)求受保護(hù)的接口</button>
</div>
</template>
<script>
import intercepterConfig from './js/config'
export default {
data () {
return {
}
},
methods:{
test(){
const accessToken = sessionStorage.getItem('accessToken')
let token = null
if(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 請(qǐng)求攔截器和響應(yīng)攔截器
import axios from "axios";
//axios請(qǐng)求攔截器
axios.interceptors.request.use(
config=>{// 正確的請(qǐng)求攔截器
let token = null;
let url = config.url
// url.indexOf('/newToken')==-1 如果是刷新Token的請(qǐng)求 不用在攔截器里面加accessToken 這個(gè)請(qǐng)求已經(jīng)在請(qǐng)求頭中設(shè)置accessToken,加了會(huì)覆蓋
if(sessionStorage.getItem('accessToken')!=null && url.indexOf('/newToken')==-1){
token = sessionStorage.getItem('accessToken')
config.headers['accessToken'] = token
}
// 加入頭信息的配置
return config // 這句沒(méi)寫(xiě)請(qǐng)求會(huì)發(fā)不出去
},
error=>{ // 出現(xiàn)異常的請(qǐng)求攔截器
return Promise.reject(error)
})
// axios響應(yīng)攔截器
axios.interceptors.response.use(
async res => {
// 判斷 401狀態(tài)碼 自動(dòng)續(xù)期
if (res.data.code == 401 &&!res.config.isRefresh) {//!res.config.isRefresh 不是刷新Token的請(qǐng)求才攔截 是則不攔截
// 1.自動(dòng)續(xù)期
const res2 = await getNewToken()
if(res2.data.code == 200){
console.log('自動(dòng)續(xù)期成功'+new Date().toLocaleString())
// 2.更新sessionStorage里面的Token 沒(méi)有這一步會(huì)死循環(huán)
sessionStorage.setItem('accessToken',res2.data.token.accessToken)
sessionStorage.setItem('refreshToken',res2.data.token.refreshToken)
//3.重新發(fā)送請(qǐng)求
res = await axios.request(res.config)// res.config 代表請(qǐng)求的所有參數(shù)(這里是上一次請(qǐng)求的所有參數(shù)),包括url和攜帶的所有數(shù)據(jù)
}
}
return res // 將重新請(qǐng)求的響應(yīng)作為響應(yīng)返回
},
error=>{
return Promise.reject(error)
})
function getNewToken(){
let url = "http://localhost:8088/newToken"
let token = null
if(sessionStorage.getItem('refreshToken')!=null){
token = sessionStorage.getItem('refreshToken')
}
return axios.get(url,{headers:{accessToken:token},isRefresh:true})
// 注意這里參數(shù)是accessToken:token 因?yàn)楹蠖诉^(guò)濾器里面獲取的是accessToken,所以要寫(xiě)這個(gè),不然過(guò)濾器通不過(guò)過(guò)濾器
}
效果展示
1.登錄頁(yè)面

2.輸入用戶名和密碼點(diǎn)擊【登錄】

3.點(diǎn)擊【請(qǐng)求受保護(hù)的資源】按鈕

3.等待10秒,accessToken過(guò)期,但refreshToken未過(guò)期時(shí),點(diǎn)擊【請(qǐng)求受保護(hù)的資源】按鈕

4.等待30秒后,refreshToken和accessToken都過(guò)期,再次點(diǎn)擊【請(qǐng)求受保護(hù)的資源】按鈕

到此這篇關(guān)于springboot+vue實(shí)現(xiàn)Token自動(dòng)續(xù)期(雙Token方案)的文章就介紹到這了,更多相關(guān)springboot Token自動(dòng)續(xù)期內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue如何優(yōu)雅處理Token過(guò)期并自動(dòng)續(xù)期
- SpringBoot基于Redis實(shí)現(xiàn)token的在線續(xù)期的實(shí)踐
- SpringBoot中Token登錄授權(quán)、續(xù)期和主動(dòng)終止的方案流程分析
- SpringBoot中基于JWT的單token授權(quán)和續(xù)期方案步驟詳解
- SpringBoot實(shí)現(xiàn)JWT token自動(dòng)續(xù)期的示例代碼
- Spring?Boot實(shí)現(xiàn)JWT?token自動(dòng)續(xù)期的實(shí)現(xiàn)
- JAVA實(shí)現(xiàn)Token自動(dòng)續(xù)期機(jī)制的示例代碼
相關(guān)文章
Java將Date日期類(lèi)型字段轉(zhuǎn)換成json字符串的方法
這篇文章主要給大家介紹了關(guān)于Java將Date日期類(lèi)型字段轉(zhuǎn)換成json字符串的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
Java實(shí)現(xiàn)指定線程執(zhí)行順序的三種方式示例
這篇文章主要介紹了Java實(shí)現(xiàn)指定線程執(zhí)行順序的三種方式,包括通過(guò)共享對(duì)象鎖加上可見(jiàn)變量,通過(guò)主線程Join()以及通過(guò)線程執(zhí)行時(shí)Join()等三種實(shí)現(xiàn)方法,需要的朋友可以參考下2019-01-01
阿里的Easyexcel讀取Excel文件的方法(最新版本)
這篇文章主要介紹了阿里的Easyexcel讀取Excel文件(最新版本)的方法,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
Java Integer[]和int[]互相轉(zhuǎn)換方式
這篇文章主要介紹了Java Integer[]和int[]互相轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel)
這篇文章主要介紹了詳解SpringBoot Redis自適應(yīng)配置(Cluster Standalone Sentinel),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Spring?Cloud?+?Nacos?+?Seata整合過(guò)程(分布式事務(wù)解決方案)
Seata 是一款開(kāi)源的分布式事務(wù)解決方案,致力于在微服務(wù)架構(gòu)下提供高性能和簡(jiǎn)單易用的分布式事務(wù)服務(wù),這篇文章主要介紹了Spring?Cloud?+?Nacos?+?Seata整合過(guò)程(分布式事務(wù)解決方案),需要的朋友可以參考下2022-03-03
Spring中異步注解@Async的使用、原理及使用時(shí)可能導(dǎo)致的問(wèn)題及解決方法
這篇文章主要介紹了Spring中異步注解@Async的使用、原理及使用時(shí)可能導(dǎo)致的問(wèn)題及解決方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07

