SpringBoot 使用 JWT


SpringBoot 使用 JWT

登錄方式對比

  1. 客戶端向服務器發送用戶名和密碼
  2. 服務器驗證通過,並把相關數據保存在 Session 中,例如登錄時間之類的
  3. 服務器返回給用戶一個 SessionId ,客戶端把這個 SessionId 寫入 Cookie
  4. 用戶每次請求都會通過 Cookie 提交 SessionId 到服務器
  5. 服務器收到 SessionId 后查找數據,就可以知道用戶身份

特點:

  • 數據存儲在服務器,安全性較強,但是占用服務器資源
  • 因為使用到了 Cookie ,所以會被偽造
  • 如果服務器較多,或者跨域訪問之類的操作,就要求共享 Session 資源,否則就需要用戶和服務器重復登錄驗證操作,或者記錄用戶登錄的服務器,對服務器和用戶體驗都不好。

JWT 方式登錄

  1. 客戶端向服務器發送用戶名和密碼
  2. 服務器驗證通過,對用戶數據進行加密,生成 Token 返回給客戶端
  3. 瀏覽器(客戶端)接收到 Token 后,將 Token 存儲在 Local Storage,需要使用 JavaScript 代碼獲取,而 Cookie 是自動攜帶
  4. 用戶每次請求都把 Token 提交到服務器
  5. 服務器對傳來的 Token 進行解密,再去查詢用戶數據,一次知道用戶身份

特點:

  • 存儲在客戶端,不占用服務器資源,但是同樣會被偽造
  • 前后端分離,帶上 Token 進行請求,不需要考慮用戶是在哪個服務器上登錄的,多服務器和跨域請求都沒有問題

建議:對數據庫的增刪改,必須加上 Token 驗證,查詢不加 Token ,這樣效率會比較高,同時查詢操作也無法獲取 Token ,更安全

如何強制token失效?
在數據庫里保存一份 Token ,驗證時再拿出來校驗,重新登錄就刷新覆蓋這個值

JWT

JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。

先看概念

官網:https://jwt.io/
這張圖來自官網

JWT 結構

JWT 分為三個部分

  • Header,算法和令牌類型
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload:數據,實際需要傳遞的 JSON 對象
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  • Verify Signature:簽名,用於防偽驗證
HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    your-256-bit-secret
)

加密之后的結果:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

終於可以上代碼了

代碼

新建一個 SpringBoot 項目

application.properties ,我只配置了最基本的

#配置程序端口,默認為8080
server.port= 8080
# 配置默認訪問路徑,默認為/
server.servlet.context-path=/jwt_demo

# 配置 Tomcat
# 配置Tomcat編碼,默認為UTF-8
server.tomcat.uri-encoding=UTF-8
# 配置最大線程數
server.tomcat.max-threads=1000

JWT 依賴庫

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

模型類

因為我要用 RESTful 所以可以用一個模型類

public class UserModel
{
    private String _username;
    private String _password;

    public String get_username()
    {
        return _username;
    }

    public void set_username(String _username)
    {
        this._username = _username;
    }

    public String get_password()
    {
        return _password;
    }

    public void set_password(String _password)
    {
        this._password = _password;
    }
}

控制器

DemoController ,用於測試訪問數據,Foo_01()不需要Token,Foo_02()需要 Token

@RestController
@RequestMapping("/v1/Demo")
public class DemoController
{
    @RequestMapping(value = "/Get1",method = RequestMethod.GET,produces = "application/json")
    public List<String> Foo_01()
    {
        List<String> list=new ArrayList<>();

        list.add("Foo_01");
        list.add("Test");

        return list;
    }

    @RequestMapping("/Get2")
    public List<String> Foo_02()
    {
        List<String> list=new ArrayList<>();

        list.add("Foo_02");
        list.add("Test");

        return list;
    }
}

LoginController ,用於登錄並獲取 Token

@RestController
@RequestMapping("/v1/Login")
public class LoginController
{
    @RequestMapping(value = "/Login", method = RequestMethod.POST, produces = "application/json")
    public String Login(@RequestBody UserModel user)
    {
        if (user.get_username().equals("abc") && user.get_password().equals("123456"))
        {
            Map<String, String> claimMap = new HashMap<>();

            claimMap.put("username", "abc");

            return TokenUtli.GenerateToken(claimMap);
        }

        return "登錄失敗";
    }
}

封裝 JWT 工具類

其實我封裝的很隨便啦,僅用於本案例

public class TokenUtli
{
    //Issuer
    public static final String ISSUER = "Test.com";
    //Audience
    public static final String AUDIENCE = "Client";
    //密鑰
    public static final String KEY = "ThisIsMySecretKey";
    //算法
    public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY);
    //Header
    public static final Map<String, Object> HEADER_MAP = new HashMap<>()
    {
        {
            put("alg", "HS256");
            put("typ", "JWT");
        }
    };

    /**
     * 生成 Token 字符串
     *
     * @param claimMap claim 數據
     * @return Token 字符串
     */
    public static String GenerateToken(Map<String, String> claimMap)
    {
        Date nowDate = new Date();
        //120 分鍾過期
        Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);

        //Token 建造器
        JWTCreator.Builder tokenBuilder = JWT.create();

        for (Map.Entry<String, String> entry : claimMap.entrySet())
        {
            //Payload 部分,根據需求添加
            tokenBuilder.withClaim(entry.getKey(), entry.getValue());
        }

        //token 字符串
        String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP)//Header 部分
                .withIssuer(TokenUtli.ISSUER)//issuer
                .withAudience(TokenUtli.AUDIENCE)//audience
                .withIssuedAt(nowDate)//生效時間
                .withExpiresAt(expireDate)//過期時間
                .sign(TokenUtli.ALGORITHM);//簽名,算法加密

        return token;
    }

    /**
     * 時間加法
     *
     * @param date   當前時間
     * @param minute 持續時間(分鍾)
     * @return 時間加法結果
     */
    private static Date AddDate(Date date, Integer minute)
    {
        if (null == date)
        {
            date = new Date();
        }
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.add(Calendar.MINUTE, minute);

        return calendar.getTime();
    }
}

在此示例中,我們指定了必須考慮哪些參數才能將 JWT 視為有效。根據我們的代碼,以下項目認為令牌有效:

  • 驗證生成令牌的服務器 Issuer
  • 驗證令牌的接收者被授權接收 Audience
  • 檢查令牌是否未過期以及頒發者的簽名密鑰是否有效 Lifetime
  • 驗證令牌的簽名 IssuerSigningKey
  • 此外,我們指定Issuer、Audience、SigningKey的值。在本例中,我將這些值存儲在常量中。

驗證 Token ,Token 錯誤或者過期就拋出異常,這里我就只判斷了一個空字符串,其它驗證規則可以自己寫啦

/**
  * 驗證 Token
  *
  * @param webToken 前端傳遞的 Token 字符串
  * @return Token 字符串是否正確
  * @throws Exception 異常信息
  */
public static boolean VerifyJWTToken(String webToken) throws Exception
{
    String[] token = webToken.split(" ");

    if (token[1].equals(""))
    {
        throw new Exception("token錯誤");
    }


    //JWT驗證器
    JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();

    //解碼
    DecodedJWT jwt = verifier.verify(token[1]);

    //Audience
    List<String> audienceList = jwt.getAudience();
    String audience = audienceList.get(0);

    //Payload
    Map<String, Claim> claimMap = jwt.getClaims();
    for (Map.Entry<String, Claim> entry : claimMap.entrySet())
    {

    }

    //生效時間
    Date issueTime = jwt.getIssuedAt();
    //過期時間
    Date expiresTime = jwt.getExpiresAt();

    return true;
}

注冊攔截器

JWTInterceptor

public class JWTInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        //從請求頭內獲取token
        String token = request.getHeader("authorization");

        //驗證令牌,如果令牌不正確會出現異常會被全局異常處理
        return TokenUtli.VerifyJWTToken(token);
    }
}

注冊攔截器

@Configuration
public class InterceptorConfig implements WebMvcConfigurer
{
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(new JWTInterceptor()).addPathPatterns("/**")//全部路徑
                .excludePathPatterns("/v1/Demo/Get1")//排除不需要Token的路徑
                .excludePathPatterns("/v1/Login/Login");//開放登錄路徑
    }
}

測試

因為懶得寫前端頁面,所以使用 postman 調試

首先,不登錄去訪問 DemoController 里的兩個函數

報了 500錯誤

登錄,以及生成的 Token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJDbGllbnQiLCJpc3MiOiJUZXN0LmNvbSIsImV4cCI6MTYyNjY4OTczMiwiaWF0IjoxNjI2NjgyNTMyLCJ1c2VybmFtZSI6ImFiYyJ9.ZtSyoqmUVKgGY3_wFQD24_mOot7qnMUKh8xMPKOW2JQ

使用 Token 再去測試需要 Token 的函數

這次就訪問到數據了

SpringBoot 使用 JWT 結束

項目結構


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM