springboot結(jié)合JWT實(shí)現(xiàn)單點(diǎn)登錄的示例
JWT實(shí)現(xiàn)單點(diǎn)登錄
- 登錄流程:
校驗(yàn)用戶名密碼->生成隨機(jī)JWT Token->返回給前端。之后前端發(fā)請(qǐng)求攜帶該Token就能驗(yàn)證是哪個(gè)用戶了。 - 校驗(yàn)流程:
從前端請(qǐng)求的header獲取JWT Token->根據(jù)工具包校驗(yàn)JWT Token->校驗(yàn)成功或失敗
JWT 簡(jiǎn)介
結(jié)構(gòu)
Header 頭部信息,主要聲明了JWT的簽名算法等信息
Payload 載荷信息,主要承載了各種聲明并傳遞明文數(shù)據(jù)
Signature 簽名,擁有該部分的JWT被稱為JWS,也就是簽了名的JWT,用于校驗(yàn)數(shù)據(jù)
整體結(jié)構(gòu)是:
header.payload.signature
參考文檔:https://doc.hutool.cn/pages/jwt/
存在問題及解決方案
token被解密:如工具包被獲取??赏ㄟ^增加“鹽值”來(lái)解決。
token被拿到第三方使用:如被包裝到第三方使用(ChatGPT工具),可以通過限流來(lái)解決。
登錄流程
后端程序?qū)崿F(xiàn)
封裝hutool工具類:
public class JwtUtil { private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class); /** * 鹽值很重要,不能泄漏,且每個(gè)項(xiàng)目都應(yīng)該不一樣,可以放到配置文件中 */ private static final String key = "xxx"; public static String createToken(Long id, String mobile) { LOG.info("開始生成JWT token,id:{},mobile:{}", id, mobile); GlobalBouncyCastleProvider.setUseBouncyCastle(false); DateTime now = DateTime.now(); DateTime expTime = now.offsetNew(DateField.HOUR, 24); // DateTime expTime = now.offsetNew(DateField.SECOND, 10); Map<String, Object> payload = new HashMap<>(); // 簽發(fā)時(shí)間 payload.put(JWTPayload.ISSUED_AT, now); // 過期時(shí)間 payload.put(JWTPayload.EXPIRES_AT, expTime); // 生效時(shí)間 payload.put(JWTPayload.NOT_BEFORE, now); // 內(nèi)容 payload.put("id", id); payload.put("mobile", mobile); String token = JWTUtil.createToken(payload, key.getBytes()); LOG.info("生成JWT token:{}", token); return token; } public static boolean validate(String token) { LOG.info("開始JWT token校驗(yàn),token:{}", token); GlobalBouncyCastleProvider.setUseBouncyCastle(false); JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes()); // validate包含了verify boolean validate = jwt.validate(0); LOG.info("JWT token校驗(yàn)結(jié)果:{}", validate); return validate; } public static JSONObject getJSONObject(String token) { GlobalBouncyCastleProvider.setUseBouncyCastle(false); JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes()); JSONObject payloads = jwt.getPayloads(); payloads.remove(JWTPayload.ISSUED_AT); payloads.remove(JWTPayload.EXPIRES_AT); payloads.remove(JWTPayload.NOT_BEFORE); LOG.info("根據(jù)token獲取原始內(nèi)容:{}", payloads); return payloads; } public static void main(String[] args) { createToken(1L, "123"); String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3MzY0ODczMDQsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MzY1NzM3MDQsImlhdCI6MTczNjQ4NzMwNH0.Bui7guCvPEF557eqxRLwmt5tO-W-3oVLnn37H4qOVfA"; validate(token); getJSONObject(token); } }
后端定義登錄業(yè)務(wù):
public MemberLoginResp login(MemberLoginReq memberLoginReq){ String mobile = memberLoginReq.getMobile(); String code = memberLoginReq.getCode(); Member memberDB = selectByMobile(mobile); if (ObjectUtil.isEmpty(memberDB)){ throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST); } if(!code.equals("8888")){ throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR); } MemberLoginResp memberLoginResp = new MemberLoginResp(); memberLoginResp.setId(memberDB.getId()); memberLoginResp.setMobile(mobile); String token = JwtUtil.createToken(memberDB.getId(), memberDB.getMobile()); memberLoginResp.setToken(token); return memberLoginResp; }
通過調(diào)用封裝的JwtUtil生成token并返回前端
成功返回Token結(jié)果
前端保存Token
Vuex全局保存Token到store中
import { createStore } from 'vuex' const MEMBER = "MEMBER"; export default createStore({ state: { member: {} }, getters: { }, mutations: { setMember (state, _member) { state.member = _member; } }, actions: { }, modules: { } })
const login = () => { axios.post("/member/member/login", loginForm).then((response) => { let data = response.data; if (data.success) { notification.success({ description: '登錄成功!' }); // 登錄成功,跳到控臺(tái)主頁(yè) router.push("/welcome"); store.commit("setMember", data.content); } else { notification.error({ description: data.message }); } }) };
store存放信息的缺點(diǎn)及解決
store存放用戶信息后,如果刷新頁(yè)面,那么信息也會(huì)消失!store可以理解為緩存,一旦重新加載,則緩存全都沒了。
解決方法:
- step1. 新增session-storage.js,封裝會(huì)話緩存sessionStorage
// 所有的session key都在這里統(tǒng)一定義,可以避免多個(gè)功能使用同一個(gè)key SESSION_ORDER = "SESSION_ORDER"; SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS"; SessionStorage = { get: function (key) { var v = sessionStorage.getItem(key); if (v && typeof(v) !== "undefined" && v !== "undefined") { return JSON.parse(v); } }, set: function (key, data) { sessionStorage.setItem(key, JSON.stringify(data)); }, remove: function (key) { sessionStorage.removeItem(key); }, clearAll: function () { sessionStorage.clear(); } };
- step2. 在index.html中引入該js
<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico" rel="external nofollow" > <!-- 引入js --> <script src="<%= BASE_URL %>js/session-storage.js"></script> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
- step3. 修改store的index.js
const MEMBER = "MEMBER"; export default createStore({ state: { member: window.SessionStorage.get(MEMBER) || {} # 讀取 }, getters: { }, mutations: { setMember (state, _member) { state.member = _member; window.SessionStorage.set(MEMBER, _member); # 設(shè)置 } },
不再是把member定義為{},而是首先在緩存中獲取,如果沒有則設(shè)置為{}。同時(shí)避免空指針
同時(shí)在用戶登錄后設(shè)置MEMBER緩存
校驗(yàn)流程:為gateway增加登錄校驗(yàn)攔截器
- 添加依賴
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.10</version> </dependency>
- 攔截器類
@Component public class LoginMemberFilter implements Ordered, GlobalFilter { private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); // 排除不需要攔截的請(qǐng)求 if (path.contains("/admin") || path.contains("/redis") || path.contains("/test") || path.contains("/member/member/login") || path.contains("/member/member/send-code")) { LOG.info("不需要登錄驗(yàn)證:{}", path); return chain.filter(exchange); } else { LOG.info("需要登錄驗(yàn)證:{}", path); } // 獲取header的token參數(shù) String token = exchange.getRequest().getHeaders().getFirst("token"); LOG.info("會(huì)員登錄驗(yàn)證開始,token:{}", token); if (token == null || token.isEmpty()) { LOG.info( "token為空,請(qǐng)求被攔截" ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 校驗(yàn)token是否有效,包括token是否被改過,是否過期 boolean validate = JwtUtil.validate(token); if (validate) { LOG.info("token有效,放行該請(qǐng)求"); return chain.filter(exchange); } else { LOG.warn( "token無(wú)效,請(qǐng)求被攔截" ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } /** * 優(yōu)先級(jí)設(shè)置 值越小 優(yōu)先級(jí)越高 * * @return */ @Override public int getOrder() { return 0; } }
- 測(cè)試結(jié)果:
- 直接調(diào)用不需要驗(yàn)證登錄的接口
@RestController public class TestController { @GetMapping("/test") public String test(){ return "test"; } }
調(diào)用需要登錄的接口方法(未登錄)
同時(shí)服務(wù)器端沒有打印,表示請(qǐng)求已被攔截
調(diào)用login登陸后再次執(zhí)行上述請(qǐng)求
login打印日志:
調(diào)用請(qǐng)求打印日志:
可見成功校驗(yàn)token,并讀取登錄用戶信息,通過校驗(yàn)
另一種單點(diǎn)登錄方法:Token+Redis實(shí)現(xiàn)單點(diǎn)登錄
- 登錄流程:
校驗(yàn)用戶名密碼->生成隨機(jī)Token->將Token存放到Redis,并返回給前端。
之后前端發(fā)請(qǐng)求攜帶該Token就能驗(yàn)證是哪個(gè)用戶了。 - 校驗(yàn)流程:
從前端請(qǐng)求的header獲取Token->根據(jù)Token到Redis獲取用戶數(shù)據(jù)->若有數(shù)據(jù)則登錄校驗(yàn)通過,否則失敗
到此這篇關(guān)于springboot結(jié)合JWT實(shí)現(xiàn)單點(diǎn)登錄的示例的文章就介紹到這了,更多相關(guān)springboot JWT單點(diǎn)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合JDBC的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot整合JDBC的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01SpringBoot中實(shí)現(xiàn)訂單30分鐘自動(dòng)取消的三種方案分享
在電商和其他涉及到在線支付的應(yīng)用中,通常需要實(shí)現(xiàn)一個(gè)功能:如果用戶在生成訂單后的一定時(shí)間內(nèi)未完成支付,系統(tǒng)將自動(dòng)取消該訂單,本文將詳細(xì)介紹基于Spring Boot框架實(shí)現(xiàn)訂單30分鐘內(nèi)未支付自動(dòng)取消的幾種方案,并提供實(shí)例代碼,需要的朋友可以參考下2023-10-10springboot 在linux后臺(tái)運(yùn)行的方法
這篇文章主要介紹了springboot 在linux后臺(tái)運(yùn)行的方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06Mybatis原始執(zhí)行方式Executor代碼實(shí)例
這篇文章主要介紹了Mybatis原始執(zhí)行方式Executor代碼實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07Java基礎(chǔ)之FileInputStream和FileOutputStream流詳解
這篇文章主要介紹了Java基礎(chǔ)之FileInputStream和FileOutputStream流詳解,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java基礎(chǔ)的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04