欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Springboot實(shí)現(xiàn)Shiro整合JWT的示例代碼

 更新時(shí)間:2020年12月17日 11:25:26   作者:一個(gè)JavaBean  
這篇文章主要介紹了Springboot實(shí)現(xiàn)Shiro整合JWT的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

寫(xiě)在前面

之前想嘗試把JWT和Shiro結(jié)合到一起,但是在網(wǎng)上查了些博客,也沒(méi)太有看懂,所以就自己重新研究了一下Shiro的工作機(jī)制,然后自己想了個(gè)(傻逼)辦法把JWT和Shiro整合到一起了

另外接下來(lái)還會(huì)涉及到JWT相關(guān)的內(nèi)容,我之前寫(xiě)過(guò)一篇博客,可以看這里:Springboot實(shí)現(xiàn)JWT認(rèn)證

Shiro的Session機(jī)制

由于我的方法是改變了Shiro的默認(rèn)的Session機(jī)制,所以這里先簡(jiǎn)單講一下Shiro的機(jī)制,簡(jiǎn)單了解Shiro是怎么確定每次訪問(wèn)的是哪個(gè)用戶(hù)的

Servlet的Session機(jī)制

Shiro在JavaWeb中使用到的就是默認(rèn)的Servlet的Session機(jī)制,大致流程如下:

image-20200227163238464

1.用戶(hù)首次發(fā)請(qǐng)求

2.服務(wù)器接收到請(qǐng)求之后,無(wú)論你有沒(méi)有權(quán)限訪問(wèn)到資源,在返回響應(yīng)的時(shí)候,服務(wù)器都會(huì)生成一個(gè)Session用來(lái)儲(chǔ)存該用戶(hù)的信息,然后生成SessionId作為對(duì)應(yīng)的Key

3.服務(wù)器會(huì)在響應(yīng)中,用jsessionId這個(gè)名字,把這個(gè)SessionId以Cookie的方式發(fā)給客戶(hù)(就是Set-Cookie響應(yīng)頭)

4.由于已經(jīng)設(shè)置了Cookie,下次訪問(wèn)的時(shí)候,服務(wù)器會(huì)自動(dòng)識(shí)別到這個(gè)SessionId然后找到你上次對(duì)應(yīng)的Session

Shiro帶來(lái)的變化

而結(jié)合Shiro之后,上面的第二步和第三步會(huì)發(fā)生小變化:

2.—>服務(wù)器不但會(huì)創(chuàng)建Session,還會(huì)創(chuàng)建一個(gè)Subject對(duì)象(就是Shiro中用來(lái)代表當(dāng)前用戶(hù)的類(lèi)),也用這個(gè)SessionId作為Key綁定

3.—>第二次接受到請(qǐng)求的時(shí)候,Shiro會(huì)從請(qǐng)求頭中找到SessionId,然后去尋找對(duì)應(yīng)的Subject然后綁定到當(dāng)前上下文,這時(shí)候Shiro就能知道來(lái)訪的是誰(shuí)了

我的思路

由于這個(gè)是我自己想出來(lái)的,所以可能會(huì)存在一定的問(wèn)題,還請(qǐng)大佬指點(diǎn)

主要思想是:用JWT Token來(lái)代替Shiro原本返回的Session

image-20200227151055913

工作流程:

  • 用戶(hù)登錄
  • 若成功則shiro會(huì)默認(rèn)生成一個(gè)SessionId用來(lái)匹配當(dāng)前Subject對(duì)象,則我們將這個(gè)SessionId放入JWT中
  • 返回JWT
  • 用戶(hù)第二次攜帶JWT來(lái)訪問(wèn)接口
  • 服務(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里寫(xiě)入

<!-- 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里寫(xiě)入

compile group: 'com.auth0', name: 'java-jwt', version: '3.8.3'

如果你是其他玩家:

maven中央倉(cāng)庫(kù)地址點(diǎn)這里

JWT工具類(lèi)

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è)用戶(hù)的id
   簽發(fā)時(shí)間:現(xiàn)在
   有效時(shí)間:30分鐘
   載荷內(nèi)容:暫時(shí)設(shè)計(jì)為:這個(gè)人的名字,這個(gè)人的昵稱(chēng)
   加密密鑰:這個(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)  //載荷,隨便寫(xiě)幾個(gè)都可以
        .withClaim("realName", realName)
        .sign(Algorithm.HMAC256(userId+"HelloLehr"));  //加密
  }

  /**
   * 檢驗(yàn)合法性,其中secret參數(shù)就應(yīng)該傳入的是用戶(hù)的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è)異常,你也可以寫(xiě)成別的
      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;
  }


  /**
  * 通過(guò)載荷名字獲取載荷的值
  */
  public static Claim getClaimByName(String token, String name){
    return JWT.decode(token).getClaim(name);
  }
}

一點(diǎn)小說(shuō)明:

關(guān)于jwt生成時(shí)的加密和驗(yàn)證方法:

jwt的驗(yàn)證其實(shí)就是驗(yàn)證jwt最后那一部分(簽名部分)。這里在指定簽名的加密方式的時(shí)候,還傳入了一個(gè)字符串來(lái)加密,所以驗(yàn)證的時(shí)候不但需要知道加密算法,還需要獲得這個(gè)字符串才能成功解密,提高了安全性。我這里用的是id來(lái),比較簡(jiǎn)單,如果你想更安全一點(diǎn),可以把用戶(hù)密碼作為這個(gè)加密字符串,這樣就算是這段業(yè)務(wù)代碼泄露了,也不會(huì)引發(fā)太大的安全問(wèn)題(畢竟我的id是誰(shuí)都知道的,這樣令牌就可以被偽造,但是如果換成密碼,只要數(shù)據(jù)庫(kù)沒(méi)事那就沒(méi)人知道)

關(guān)于獲得載荷的方法:

可能有人會(huì)覺(jué)得奇怪,為什么不需要解密不需要verify就能夠獲取到載荷里的內(nèi)容呢?原因是,本來(lái)載荷就只是用Base64處理了,就沒(méi)有加密性,所以能直接獲取到它的值,但是至于可不可以相信這個(gè)值的真實(shí)性,就是要看能不能通過(guò)驗(yàn)證了,因?yàn)樽詈蟮暮灻糠质呛颓懊骖^部和載荷的內(nèi)容有關(guān)聯(lián)的,所以一旦簽名驗(yàn)證過(guò)了,那就說(shuō)明前面的載荷是沒(méi)有被改過(guò)的。

Controller層

 登錄邏輯

  /**
   * 用戶(hù)登錄
   * @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,后來(lái)會(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());

以后我們就可以通過(guò)解析JWT來(lái)獲取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 "用戶(hù)已經(jīng)安全登出";
  }

這樣的話Realm中的用戶(hù)狀態(tài)就變成未認(rèn)證了,就算JWT沒(méi)過(guò)期也需要重新登錄了

自定義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)在只需要重寫(xiě)這個(gè)類(lèi),把我們?nèi)绾潍@取Session的邏輯寫(xiě)進(jìn)去就好了

這里說(shuō)兩個(gè)方法:

getSessionId(SessionKey key)

這個(gè)方法是在DefaultWebSessionManager的,這里并沒(méi)有重寫(xiě),我們上面重寫(xiě)的是后面第二個(gè)同名方法,只是想在這里談?wù)?,讀者可以直接跳過(guò)這段也不影響

源碼邏輯

在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;
}
  • 如果沒(méi)能找到id,就調(diào)用第二個(gè)同名方法
  • 如果有,就返回

這里需要注意的是,這個(gè)方法會(huì)在整個(gè)驗(yàn)證過(guò)程中多次被反復(fù)調(diào)用,而在服務(wù)器接受到用戶(hù)請(qǐng)求的時(shí)候,只會(huì)調(diào)用一次的方法是下面這個(gè),也就是我們重寫(xiě)的這個(gè)

getSessionId(ServletRequest request, ServletResponse response)

這個(gè)才是真正涉及到服務(wù)器接受到請(qǐng)求的時(shí)候獲取Session邏輯,從用戶(hù)的請(qǐng)求報(bào)文中獲取SessionId

所以我們要重寫(xiě)的就是這一步

原版中的邏輯是:從Cookie里找到sessionId的值

我們只需要把邏輯該為:從Header中找出JWT(也就是從請(qǐng)求頭的'token'頭中找),然后解析JWT,獲取到我們存放在其中的SessionId屬性即可

ShiroConfiguration

我們只需要把自己寫(xiě)的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è)試

登錄

image-20200227161124575

我們獲取到了JWT,JWT里面就帶有SessionId

后續(xù)請(qǐng)求不帶token

image-20200227161223110

顯然,沒(méi)過(guò)認(rèn)證,我們看下后臺(tái):

image-20200227161319858

因?yàn)椴荒塬@得token所以無(wú)法得到該用戶(hù)對(duì)應(yīng)的sessionId,所以被授權(quán)攔截了

后面那個(gè)JSESSIONID是因?yàn)闆](méi)得到sessionId新生成的,所以對(duì)應(yīng)了一個(gè)沒(méi)有登錄的用戶(hù),自然就會(huì)被拒絕

只有帶上之前的token,shiro才會(huì)認(rèn)為我們是之前那個(gè)已經(jīng)登錄過(guò)的用戶(hù)

后續(xù)請(qǐng)求帶token

image-20200227161551066

后臺(tái):

image-20200227161616008

成功!

另外,因?yàn)镴WT本身就適合RESTful API服務(wù),所以,如果把Shiro和Redis整合起來(lái)做成分布式的,那么效果會(huì)更好

到此這篇關(guān)于Springboot實(shí)現(xiàn)Shiro整合JWT的示例代碼的文章就介紹到這了,更多相關(guān)Springboot Shiro整合JWT內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java 正則表達(dá)式詳細(xì)使用

    Java 正則表達(dá)式詳細(xì)使用

    這篇文章主要介紹了Java 正則表達(dá)式詳細(xì)使用,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧
    2017-10-10
  • SpringBoot登錄驗(yàn)證token攔截器的實(shí)現(xiàn)

    SpringBoot登錄驗(yàn)證token攔截器的實(shí)現(xiàn)

    本文主要介紹了SpringBoot登錄驗(yàn)證token攔截器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • java獲取用戶(hù)輸入的字符串方法

    java獲取用戶(hù)輸入的字符串方法

    今天小編就為大家分享一篇java獲取用戶(hù)輸入的字符串方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-07-07
  • JPA多條件復(fù)雜SQL動(dòng)態(tài)分頁(yè)查詢(xún)功能

    JPA多條件復(fù)雜SQL動(dòng)態(tài)分頁(yè)查詢(xún)功能

    這篇文章主要介紹了JPA多條件復(fù)雜SQL動(dòng)態(tài)分頁(yè)查詢(xún)功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-09-09
  • 基于Properties實(shí)現(xiàn)配置數(shù)據(jù)庫(kù)驅(qū)動(dòng)

    基于Properties實(shí)現(xiàn)配置數(shù)據(jù)庫(kù)驅(qū)動(dòng)

    這篇文章主要介紹了基于Properties實(shí)現(xiàn)配置數(shù)據(jù)庫(kù)驅(qū)動(dòng),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-05-05
  • SpringBoot 設(shè)置傳入?yún)?shù)非必要的操作

    SpringBoot 設(shè)置傳入?yún)?shù)非必要的操作

    這篇文章主要介紹了SpringBoot 設(shè)置傳入?yún)?shù)非必要的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-02-02
  • 詳解使用Spring?Data?repository進(jìn)行數(shù)據(jù)層的訪問(wèn)問(wèn)題

    詳解使用Spring?Data?repository進(jìn)行數(shù)據(jù)層的訪問(wèn)問(wèn)題

    這篇文章主要介紹了使用Spring?Data?repository進(jìn)行數(shù)據(jù)層的訪問(wèn),抽象出Spring Data repository是因?yàn)樵陂_(kāi)發(fā)過(guò)程中,常常會(huì)為了實(shí)現(xiàn)不同持久化存儲(chǔ)的數(shù)據(jù)訪問(wèn)層而寫(xiě)大量的大同小異的代碼,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2022-06-06
  • java算法之余弦相似度計(jì)算字符串相似率

    java算法之余弦相似度計(jì)算字符串相似率

    這篇文章主要介紹了java算法之余弦相似度計(jì)算字符串相似率,對(duì)算法感興趣的同學(xué),可以參考下
    2021-05-05
  • 重寫(xiě)hashCode()和equals()方法詳細(xì)介紹

    重寫(xiě)hashCode()和equals()方法詳細(xì)介紹

    這篇文章主要介紹了重寫(xiě)hashCode()和equals()方法詳細(xì)介紹,涉及重寫(xiě)equals()方法,重寫(xiě)hashCode()方法,重寫(xiě)equals()而不重寫(xiě)hashCode()的風(fēng)險(xiǎn)等相關(guān)內(nèi)容的介紹,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2018-01-01
  • java 使用異常的好處總結(jié)

    java 使用異常的好處總結(jié)

    這篇文章主要介紹了java 使用異常的好處總結(jié)的相關(guān)資料,需要的朋友可以參考下
    2017-03-03

最新評(píng)論