springboot+vue實現(xiàn)Token自動續(xù)期(雙Token方案)
本篇文章的代碼是在springboot+vue項目中使用jwt實現(xiàn)登錄認(rèn)證的基礎(chǔ)上實現(xiàn)的Token自動續(xù)期的功能。
一、雙Token方案介紹
雙token解決方案是一種用于增強(qiáng)用戶登錄安全性和提升用戶體驗的認(rèn)證機(jī)制。它主要涉及兩個令牌:訪問令牌(accessToken)和刷新令牌(refreshToken)。以下是對雙token解決方案的詳細(xì)介紹:
1. 令牌類型與功能
訪問令牌(accessToken):
有效期較短,通常設(shè)置為較短的時間,如兩小時或根據(jù)業(yè)務(wù)需求自定義(如10分鐘)。 儲存用戶信息權(quán)限等,包含用戶相關(guān)信息,如UserID、Username等。 用于前端與后端之間的通信認(rèn)證,前端在每次請求時攜帶此令牌進(jìn)行校驗。
刷新令牌(refreshToken):
有效期較長,可以設(shè)置為一星期、一個月或更長時間,具體根據(jù)業(yè)務(wù)需求自定義。 不儲存額外信息,只儲存用戶id,用于在accessToken過期后重新生成新的accessToken。 由于有效期長,因此降低了用戶需要頻繁登錄的頻率。
2.雙Token方案的優(yōu)點
增強(qiáng)安全性:通過短期有效的accessToken和長期有效的refreshToken的結(jié)合,即使accessToken泄露,攻擊者也只能在有限時間內(nèi)進(jìn)行模擬用戶行為,降低了安全風(fēng)險。
提升用戶體驗:由于refreshToken的存在,用戶無需頻繁登錄,特別是在長時間操作或后臺服務(wù)場景下,提高了用戶體驗。
3.實現(xiàn)流程
登錄:用戶輸入用戶名和密碼進(jìn)行登錄,后端驗證成功后生成accessToken和refreshToken,并發(fā)送給前端。
請求校驗:前端在每次請求時攜帶accessToken進(jìn)行校驗,如果accessToken有效,則允許請求繼續(xù);如果無效但refreshToken有效,則使用refreshToken重新生成accessToken。
令牌刷新:當(dāng)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構(gòu)造器 JwtBuilder jwtBuilder = Jwts.builder(); // 生成jwt字符串 String jwt = jwtBuilder //頭部 .setHeaderParam("typ","JWT") .setHeaderParam("alg","HS256") // 載荷 .setClaims(map) // 設(shè)置多個自定義數(shù)據(jù) 位置只能放在前面,如果放在后面,那前面的載荷會失效 .setId(UUID.randomUUID().toString())// 唯一標(biāo)識 .setIssuer("liukang")// 簽發(fā)人 .setIssuedAt(new Date())// 簽發(fā)時間 .setSubject("jwtDemo")// 主題 .setExpiration(new Date(System.currentTimeMillis()+expire))//過期時間 // 自定義數(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)識 .setIssuer("liukang")// 簽發(fā)人 .setIssuedAt(new Date())// 簽發(fā)時間 .setSubject("jwtDemo")// 主題 .setExpiration(new Date(System.currentTimeMillis()+expire))//過期時間 // 自定義數(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)工具類
代碼如下(示例):
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"); // 解決跨域問題 設(shè)置跨域頭 response.setHeader("Access-Control-Allow-Origin","*"); // 輸出流 PrintWriter out = response.getWriter(); // 輸出 out.write(new ObjectMapper().writeValueAsString(rm)); // 關(guān)閉流 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; }
響應(yīng)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(); // 不含主機(jī)和端口號 if(requestURI.contains("/login")){ filterChain.doFilter(request,response); return; } // 得到請求頭的信息(accessToken) String token = request.getHeader("accessToken"); if(!StringUtils.hasText(token)){ //響應(yīng)前端錯誤的消息提示 ResponseModel responseModel = new ResponseModel(500,"failure","令牌缺失!"); ResponseUtil.write(responseModel,response); return; } // 解析Token信息 try { JwtUtil.parseToken(token); }catch (Exception e){ //響應(yīng)前端錯誤的消息提示 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);// 設(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); } }
測試請求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">請求受保護(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 請求攔截器和響應(yīng)攔截器
import axios from "axios"; //axios請求攔截器 axios.interceptors.request.use( config=>{// 正確的請求攔截器 let token = null; let url = config.url // url.indexOf('/newToken')==-1 如果是刷新Token的請求 不用在攔截器里面加accessToken 這個請求已經(jīng)在請求頭中設(shè)置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響應(yīng)攔截器 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 代表請求的所有參數(shù)(這里是上一次請求的所有參數(shù)),包括url和攜帶的所有數(shù)據(jù) } } return res // 將重新請求的響應(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 因為后端過濾器里面獲取的是accessToken,所以要寫這個,不然過濾器通不過過濾器 }
效果展示
1.登錄頁面
2.輸入用戶名和密碼點擊【登錄】
3.點擊【請求受保護(hù)的資源】按鈕
3.等待10秒,accessToken過期,但refreshToken未過期時,點擊【請求受保護(hù)的資源】按鈕
4.等待30秒后,refreshToken和accessToken都過期,再次點擊【請求受保護(hù)的資源】按鈕
到此這篇關(guān)于springboot+vue實現(xiàn)Token自動續(xù)期(雙Token方案)的文章就介紹到這了,更多相關(guān)springboot Token自動續(xù)期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java使用泛型實現(xiàn)棧結(jié)構(gòu)的示例代碼
泛型是JAVA重要的特性,使用泛型編程,可以使代碼復(fù)用率提高。本文將利用泛型實現(xiàn)簡單的棧結(jié)構(gòu),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-08-08基于java中的null類型---有關(guān)null的9件事
這篇文章主要介紹了java中的null類型---有關(guān)null的9件事,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08學(xué)習(xí)Java HashMap,看這篇就夠了
這篇文章主要介紹了Java HashMap的相關(guān)資料,文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07JAVA實現(xiàn)遍歷文件夾下的所有文件(遞歸調(diào)用和非遞歸調(diào)用)
本篇文章主要介紹了JAVA 遍歷文件夾下的所有文件(遞歸調(diào)用和非遞歸調(diào)用) ,具有一定的參考價值,有興趣的可以了解一下。2017-01-01Java實現(xiàn)基礎(chǔ)銀行ATM系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)基礎(chǔ)銀行ATM系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05