Springboot實(shí)現(xiàn)Shiro整合JWT的示例代碼
寫在前面
之前想嘗試把JWT和Shiro結(jié)合到一起,但是在網(wǎng)上查了些博客,也沒太有看懂,所以就自己重新研究了一下Shiro的工作機(jī)制,然后自己想了個(gè)(傻逼)辦法把JWT和Shiro整合到一起了
另外接下來還會(huì)涉及到JWT相關(guān)的內(nèi)容,我之前寫過一篇博客,可以看這里:Springboot實(shí)現(xiàn)JWT認(rèn)證
Shiro的Session機(jī)制
由于我的方法是改變了Shiro的默認(rèn)的Session機(jī)制,所以這里先簡單講一下Shiro的機(jī)制,簡單了解Shiro是怎么確定每次訪問的是哪個(gè)用戶的
Servlet的Session機(jī)制
Shiro在JavaWeb中使用到的就是默認(rèn)的Servlet的Session機(jī)制,大致流程如下:
1.用戶首次發(fā)請(qǐng)求
2.服務(wù)器接收到請(qǐng)求之后,無論你有沒有權(quán)限訪問到資源,在返回響應(yīng)的時(shí)候,服務(wù)器都會(huì)生成一個(gè)Session用來儲(chǔ)存該用戶的信息,然后生成SessionId作為對(duì)應(yīng)的Key
3.服務(wù)器會(huì)在響應(yīng)中,用jsessionId這個(gè)名字,把這個(gè)SessionId以Cookie的方式發(fā)給客戶(就是Set-Cookie響應(yīng)頭)
4.由于已經(jīng)設(shè)置了Cookie,下次訪問的時(shí)候,服務(wù)器會(huì)自動(dòng)識(shí)別到這個(gè)SessionId然后找到你上次對(duì)應(yīng)的Session
Shiro帶來的變化
而結(jié)合Shiro之后,上面的第二步和第三步會(huì)發(fā)生小變化:
2.—>服務(wù)器不但會(huì)創(chuàng)建Session,還會(huì)創(chuàng)建一個(gè)Subject對(duì)象(就是Shiro中用來代表當(dāng)前用戶的類),也用這個(gè)SessionId作為Key綁定
3.—>第二次接受到請(qǐng)求的時(shí)候,Shiro會(huì)從請(qǐng)求頭中找到SessionId,然后去尋找對(duì)應(yīng)的Subject然后綁定到當(dāng)前上下文,這時(shí)候Shiro就能知道來訪的是誰了
我的思路
由于這個(gè)是我自己想出來的,所以可能會(huì)存在一定的問題,還請(qǐng)大佬指點(diǎn)
主要思想是:用JWT Token來代替Shiro原本返回的Session
工作流程:
- 用戶登錄
- 若成功則shiro會(huì)默認(rèn)生成一個(gè)SessionId用來匹配當(dāng)前Subject對(duì)象,則我們將這個(gè)SessionId放入JWT中
- 返回JWT
- 用戶第二次攜帶JWT來訪問接口
- 服務(wù)器解析JWT,獲得SessionId
- 服務(wù)器把SessionId交給Shiro執(zhí)行相關(guān)認(rèn)證
代碼實(shí)現(xiàn)
導(dǎo)入JWT相關(guān)包
導(dǎo)入java-jwt
包:
這個(gè)包里實(shí)現(xiàn)了一系列jwt操作的api(包括上面講到的怎么校驗(yàn),怎么生成jwt等等)
如果你是Maven玩家:
pom.xml里寫入
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency>
如果你是Gradle玩家:
build.gradle里寫入
compile group: 'com.auth0', name: 'java-jwt', version: '3.8.3'
如果你是其他玩家:
maven中央倉庫地址點(diǎn)這里
JWT工具類
JwtUtils,代碼如下:
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import java.io.Serializable; import java.util.Calendar; import java.util.Date; /** * @author Lehr * @create: 2020-02-04 */ public class JwtUtils { /** 簽發(fā)對(duì)象:這個(gè)用戶的id 簽發(fā)時(shí)間:現(xiàn)在 有效時(shí)間:30分鐘 載荷內(nèi)容:暫時(shí)設(shè)計(jì)為:這個(gè)人的名字,這個(gè)人的昵稱 加密密鑰:這個(gè)人的id加上一串字符串 */ public static String createToken(String userId,String realName, String userName) { Calendar nowTime = Calendar.getInstance(); nowTime.add(Calendar.MINUTE,30); Date expiresDate = nowTime.getTime(); return JWT.create().withAudience(userId) //簽發(fā)對(duì)象 .withIssuedAt(new Date()) //發(fā)行時(shí)間 .withExpiresAt(expiresDate) //有效時(shí)間 .withClaim("userName", userName) //載荷,隨便寫幾個(gè)都可以 .withClaim("realName", realName) .sign(Algorithm.HMAC256(userId+"HelloLehr")); //加密 } /** * 檢驗(yàn)合法性,其中secret參數(shù)就應(yīng)該傳入的是用戶的id * @param token * @throws TokenUnavailable */ public static void verifyToken(String token, String secret) throws TokenUnavailable { DecodedJWT jwt = null; try { JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"HelloLehr")).build(); jwt = verifier.verify(token); } catch (Exception e) { //效驗(yàn)失敗 //這里拋出的異常是我自定義的一個(gè)異常,你也可以寫成別的 throw new TokenUnavailable(); } } /** * 獲取簽發(fā)對(duì)象 */ public static String getAudience(String token) throws TokenUnavailable { String audience = null; try { audience = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { //這里是token解析失敗 throw new TokenUnavailable(); } return audience; } /** * 通過載荷名字獲取載荷的值 */ public static Claim getClaimByName(String token, String name){ return JWT.decode(token).getClaim(name); } }
一點(diǎn)小說明:
關(guān)于jwt生成時(shí)的加密和驗(yàn)證方法:
jwt的驗(yàn)證其實(shí)就是驗(yàn)證jwt最后那一部分(簽名部分)。這里在指定簽名的加密方式的時(shí)候,還傳入了一個(gè)字符串來加密,所以驗(yàn)證的時(shí)候不但需要知道加密算法,還需要獲得這個(gè)字符串才能成功解密,提高了安全性。我這里用的是id來,比較簡單,如果你想更安全一點(diǎn),可以把用戶密碼作為這個(gè)加密字符串,這樣就算是這段業(yè)務(wù)代碼泄露了,也不會(huì)引發(fā)太大的安全問題(畢竟我的id是誰都知道的,這樣令牌就可以被偽造,但是如果換成密碼,只要數(shù)據(jù)庫沒事那就沒人知道)
關(guān)于獲得載荷的方法:
可能有人會(huì)覺得奇怪,為什么不需要解密不需要verify就能夠獲取到載荷里的內(nèi)容呢?原因是,本來載荷就只是用Base64處理了,就沒有加密性,所以能直接獲取到它的值,但是至于可不可以相信這個(gè)值的真實(shí)性,就是要看能不能通過驗(yàn)證了,因?yàn)樽詈蟮暮灻糠质呛颓懊骖^部和載荷的內(nèi)容有關(guān)聯(lián)的,所以一旦簽名驗(yàn)證過了,那就說明前面的載荷是沒有被改過的。
Controller層
登錄邏輯
/** * 用戶登錄 * @param userName * @param password * @param req * @return * @throws Exception */ @SneakyThrows @PostMapping(value = "/login") public AccountVO login(String userName, String password, HttpServletRequest req){ //嘗試登錄 Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(userName, password)); } catch (Exception e) { throw new LoginFailed(); } AccountVO account = accountService.getAccountByUserName(userName); String id = account.getId(); //生成jwtToken String jwtToken = JwtUtils.createToken(id, account.getRealName(),account.getUserName(), subject.getSession().getId().toString()); //設(shè)置好token,后來會(huì)在全局處理的時(shí)候放入響應(yīng)里 req.setAttribute("token", jwtToken); return account; }
主要是:在登錄成功之后把這個(gè)Subject的SessionId放入JWT然后生成token:
String jwtToken = JwtUtils.createToken(id,account.getRealName(),account.getUserName(),subject.getSession().getId().toString());
以后我們就可以通過解析JWT來獲取SessionId了,而不是每次把SessionId作為Cookie返回
退出邏輯
首先,由于JWT令牌本身就會(huì)失效,所以如果JWT令牌失效,也就相當(dāng)與退出了
然后我們還可以同樣實(shí)現(xiàn)Shiro中傳統(tǒng)的手動(dòng)登出:
public String logout(HttpServletRequest req) { SecurityUtils.getSubject().logout(); return "用戶已經(jīng)安全登出"; }
這樣的話Realm中的用戶狀態(tài)就變成未認(rèn)證了,就算JWT沒過期也需要重新登錄了
自定義SessionManager
先上代碼:
package com.imlehr.internship.shiroJwt; import com.imlehr.internship.exception.TokenUnavailable; import lombok.SneakyThrows; import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.Serializable; import java.util.UUID; /** * @author Lehr * @create: 2020-02-10 */ public class CustomSessionManager extends DefaultWebSessionManager { //這里我為了省事用了lombok的標(biāo)簽 @SneakyThrows @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String token = WebUtils.toHttp(request).getHeader("token"); System.out.println("會(huì)話管理器得到的token是:" + token); if (token == null || token.length()<1) { return UUID.randomUUID().toString(); } //在這里驗(yàn)證一下jwt了,雖然我知道這樣不好 String userId = JwtUtils.getAudience(token); JwtUtils.verifyToken(token, userId); String sessionId = JwtUtils.getClaimByName(token, "sessionId").asString(); if (sessionId == null) { return new TokenUnavailable(); } request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header"); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled()); return sessionId; } }
之前的Session的獲取,就是在DefaultWebSessionManager里實(shí)現(xiàn)的,所以我們現(xiàn)在只需要重寫這個(gè)類,把我們?nèi)绾潍@取Session的邏輯寫進(jìn)去就好了
這里說兩個(gè)方法:
getSessionId(SessionKey key)
這個(gè)方法是在DefaultWebSessionManager的,這里并沒有重寫,我們上面重寫的是后面第二個(gè)同名方法,只是想在這里談?wù)?,讀者可以直接跳過這段也不影響
源碼邏輯
在Shiro想要獲取SessionId的時(shí)候,首先會(huì)調(diào)用的就是這個(gè)方法,而不是那個(gè)傳入httpRequest的方法
在DefaultWebSessionManager中,他是這樣做的
@Override public Serializable getSessionId(SessionKey key) { Serializable id = super.getSessionId(key); if (id == null && WebUtils.isWeb(key)) { ServletRequest request = WebUtils.getRequest(key); ServletResponse response = WebUtils.getResponse(key); //調(diào)用第二個(gè)同名方法 id = getSessionId(request, response); } return id; }
- 如果沒能找到id,就調(diào)用第二個(gè)同名方法
- 如果有,就返回
這里需要注意的是,這個(gè)方法會(huì)在整個(gè)驗(yàn)證過程中多次被反復(fù)調(diào)用,而在服務(wù)器接受到用戶請(qǐng)求的時(shí)候,只會(huì)調(diào)用一次的方法是下面這個(gè),也就是我們重寫的這個(gè)
getSessionId(ServletRequest request, ServletResponse response)
這個(gè)才是真正涉及到服務(wù)器接受到請(qǐng)求的時(shí)候獲取Session邏輯,從用戶的請(qǐng)求報(bào)文中獲取SessionId
所以我們要重寫的就是這一步
原版中的邏輯是:從Cookie里找到sessionId的值
我們只需要把邏輯該為:從Header中找出JWT(也就是從請(qǐng)求頭的'token'頭中找),然后解析JWT,獲取到我們存放在其中的SessionId屬性即可
ShiroConfiguration
我們只需要把自己寫的SessionManager配置進(jìn)去就好了
首先配好:
public DefaultWebSessionManager sessionManager() { CustomSessionManager customSessionManager = new CustomSessionManager(); return customSessionManager; }
然后放入SecurityManager
public SecurityManager securityManager(MyRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); return securityManager; }
完成🎉
測(cè)試
登錄
我們獲取到了JWT,JWT里面就帶有SessionId
后續(xù)請(qǐng)求不帶token
顯然,沒過認(rèn)證,我們看下后臺(tái):
因?yàn)椴荒塬@得token所以無法得到該用戶對(duì)應(yīng)的sessionId,所以被授權(quán)攔截了
后面那個(gè)JSESSIONID是因?yàn)闆]得到sessionId新生成的,所以對(duì)應(yīng)了一個(gè)沒有登錄的用戶,自然就會(huì)被拒絕
只有帶上之前的token,shiro才會(huì)認(rèn)為我們是之前那個(gè)已經(jīng)登錄過的用戶
后續(xù)請(qǐng)求帶token
后臺(tái):
成功!
另外,因?yàn)镴WT本身就適合RESTful API服務(wù),所以,如果把Shiro和Redis整合起來做成分布式的,那么效果會(huì)更好
到此這篇關(guān)于Springboot實(shí)現(xiàn)Shiro整合JWT的示例代碼的文章就介紹到這了,更多相關(guān)Springboot Shiro整合JWT內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis Plus Wrapper查詢某幾列的方法實(shí)現(xiàn)
MybatisPlus中,使用Wrapper的select和notSelect方法可以精確控制查詢的字段,本文就來介紹一下Mybatis Plus Wrapper查詢某幾列的方法實(shí)現(xiàn),感興趣的可以了解一下2024-10-10Springboot pom項(xiàng)目間接依賴包版本與預(yù)期不符原因解決分析
這篇文章主要介紹了Springboot pom項(xiàng)目間接依賴包版本與預(yù)期不符原因解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08Java實(shí)現(xiàn)Token工具類進(jìn)行登錄和攔截
在應(yīng)用的登錄時(shí)需要生成token進(jìn)行驗(yàn)證,并放入信息,之后的話可以直接使用瀏覽器的session進(jìn)行登錄,本文就來利用java編寫一個(gè)token工具類,可以很方便的生成和解析token,感興趣的可以了解下2023-12-12Java設(shè)計(jì)模式中的策略(Strategy)模式解讀
這篇文章主要介紹了Java設(shè)計(jì)模式中的策略(Strategy)模式解讀,對(duì)象的某個(gè)行為,在不同場(chǎng)景有不同實(shí)現(xiàn)方式,可以將這些行為的具體實(shí)現(xiàn)定義為一組策略,每個(gè)實(shí)現(xiàn)類實(shí)現(xiàn)一種策略,在不同場(chǎng)景使用不同的實(shí)現(xiàn),并且可以自由切換策略,需要的朋友可以參考下2023-10-10Spring+Mybatis+Mysql搭建分布式數(shù)據(jù)庫訪問框架的方法
這篇文章主要介紹了Spring+Mybatis+Mysql搭建分布式數(shù)據(jù)庫訪問框架的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-03-03