springboot+vue實現(xiàn)Token自動續(xù)期(雙Token方案)
本篇文章的代碼是在springboot+vue項目中使用jwt實現(xiàn)登錄認證的基礎上實現(xiàn)的Token自動續(xù)期的功能。
一、雙Token方案介紹
雙token解決方案是一種用于增強用戶登錄安全性和提升用戶體驗的認證機制。它主要涉及兩個令牌:訪問令牌(accessToken)和刷新令牌(refreshToken)。以下是對雙token解決方案的詳細介紹:
1. 令牌類型與功能
訪問令牌(accessToken):
有效期較短,通常設置為較短的時間,如兩小時或根據業(yè)務需求自定義(如10分鐘)。 儲存用戶信息權限等,包含用戶相關信息,如UserID、Username等。 用于前端與后端之間的通信認證,前端在每次請求時攜帶此令牌進行校驗。
刷新令牌(refreshToken):
有效期較長,可以設置為一星期、一個月或更長時間,具體根據業(yè)務需求自定義。 不儲存額外信息,只儲存用戶id,用于在accessToken過期后重新生成新的accessToken。 由于有效期長,因此降低了用戶需要頻繁登錄的頻率。
2.雙Token方案的優(yōu)點
增強安全性:通過短期有效的accessToken和長期有效的refreshToken的結合,即使accessToken泄露,攻擊者也只能在有限時間內進行模擬用戶行為,降低了安全風險。
提升用戶體驗:由于refreshToken的存在,用戶無需頻繁登錄,特別是在長時間操作或后臺服務場景下,提高了用戶體驗。
3.實現(xiàn)流程
登錄:用戶輸入用戶名和密碼進行登錄,后端驗證成功后生成accessToken和refreshToken,并發(fā)送給前端。
請求校驗:前端在每次請求時攜帶accessToken進行校驗,如果accessToken有效,則允許請求繼續(xù);如果無效但refreshToken有效,則使用refreshToken重新生成accessToken。
令牌刷新:當accessToken過期但refreshToken未過期時,前端可以使用refreshToken向后端請求新的accessToken,無需用戶重新登錄。
登出:用戶登出時,后端需要同時使accessToken和refreshToken失效,以確保用戶登出后的安全性。
二、具體實現(xiàn)
1.后端實現(xiàn)
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";
/**
* 創(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構造器
JwtBuilder jwtBuilder = Jwts.builder();
// 生成jwt字符串
String jwt = jwtBuilder
//頭部
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
// 載荷
.setClaims(map) // 設置多個自定義數據 位置只能放在前面,如果放在后面,那前面的載荷會失效
.setId(UUID.randomUUID().toString())// 唯一標識
.setIssuer("liukang")// 簽發(fā)人
.setIssuedAt(new Date())// 簽發(fā)時間
.setSubject("jwtDemo")// 主題
.setExpiration(new Date(System.currentTimeMillis()+expire))//過期時間
// 自定義數據
// .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構造器
JwtBuilder jwtBuilder = Jwts.builder();
// 生成jwt字符串
String jwt = jwtBuilder
//頭部
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
// 載荷
.setId(UUID.randomUUID().toString())// 唯一標識
.setIssuer("liukang")// 簽發(fā)人
.setIssuedAt(new Date())// 簽發(fā)時間
.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 {
@Override
public 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")){
// 生成jwt
accessToken = JwtUtil.generateToken(1000*10);// 設置有效期為10s
refreshToken = JwtUtil.generateToken(1000*30);// 設置有效期為30s
tokenMap.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、前端實現(xiàn)
2.1 登錄頁面
<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頁面
<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 = 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 請求攔截器和響應攔截器
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 // 這句沒寫請求會發(fā)不出去
},
error=>{ // 出現(xiàn)異常的請求攔截器
return Promise.reject(error)
})
// axios響應攔截器
axios.interceptors.response.use(
async res => {
// 判斷 401狀態(tài)碼 自動續(xù)期
if (res.data.code == 401 &&!res.config.isRefresh) {//!res.config.isRefresh 不是刷新Token的請求才攔截 是則不攔截
// 1.自動續(xù)期
const res2 = await getNewToken()
if(res2.data.code == 200){
console.log('自動續(xù)期成功'+new Date().toLocaleString())
// 2.更新sessionStorage里面的Token 沒有這一步會死循環(huán)
sessionStorage.setItem('accessToken',res2.data.token.accessToken)
sessionStorage.setItem('refreshToken',res2.data.token.refreshToken)
//3.重新發(fā)送請求
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 = null
if(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都過期,再次點擊【請求受保護的資源】按鈕

到此這篇關于springboot+vue實現(xiàn)Token自動續(xù)期(雙Token方案)的文章就介紹到這了,更多相關springboot Token自動續(xù)期內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MyBatis學習教程(八)-Mybatis3.x與Spring4.x整合圖文詳解
這篇文章主要介紹了MyBatis學習教程(八)-Mybatis3.x與Spring4.x整合圖文詳解的相關資料,需要的朋友可以參考下2016-05-05

