天天看點

SpringBoot內建JWT實作token驗證

JWT官網: https://jwt.io/ JWT(Java版)的github位址: https://github.com/jwtk/jjwt

什麼是JWT

Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基于

JSON

的開放标準((RFC 7519).定義了一種簡潔的,自包含的方法用于通信雙方之間以

JSON

對象的形式安全的傳遞資訊。因為數字簽名的存在,這些資訊是可信的,JWT可以使用

HMAC

算法或者是

RSA

的公私秘鑰對進行簽名。

JWT請求流程

image.png

1. 使用者使用賬号和面發出post請求;

2. 伺服器使用私鑰建立一個jwt;

3. 伺服器傳回這個jwt給浏覽器;

4. 浏覽器将該jwt串在請求頭中像伺服器發送請求;

5. 伺服器驗證該jwt;

6. 傳回響應的資源給浏覽器。

JWT的主要應用場景

身份認證在這種場景下,一旦使用者完成了登陸,在接下來的每個請求中包含JWT,可以用來驗證使用者身份以及對路由,服務和資源的通路權限進行驗證。由于它的開銷非常小,可以輕松的在不同域名的系統中傳遞,所有目前在單點登入(SSO)中比較廣泛的使用了該技術。 資訊交換在通信的雙方之間使用JWT對資料進行編碼是一種非常安全的方式,由于它的資訊是經過簽名的,可以確定發送者發送的資訊是沒有經過僞造的。

優點

1.簡潔(Compact): 可以通過

URL

POST

參數或者在

HTTP header

發送,因為資料量小,傳輸速度也很快

2.自包含(Self-contained):負載中包含了所有使用者所需要的資訊,避免了多次查詢資料庫

3.因為

Token

是以

JSON

加密的形式儲存在用戶端的,是以

JWT

是跨語言的,原則上任何web形式都支援。

4.不需要在服務端儲存會話資訊,特别适用于分布式微服務。

`

JWT的結構

JWT是由三段資訊構成的,将這三段資訊文本用.連結一起就構成了JWT字元串。

就像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT包含了三部分:

Header 頭部(标題包含了令牌的中繼資料,并且包含簽名和/或加密算法的類型)

Payload 負載 (類似于飛機上承載的物品)

Signature 簽名/簽證

Header

JWT的頭部承載兩部分資訊:token類型和采用的加密算法。

{ 
  "alg": "HS256",
   "typ": "JWT"
} 
           

聲明類型:這裡是jwt

聲明加密的算法:通常直接使用 HMAC SHA256

加密算法是單向函數雜湊演算法,常見的有MD5、SHA、HAMC。

MD5(message-digest algorithm 5) (資訊-摘要算法)縮寫,廣泛用于加密和解密技術,常用于檔案校驗。校驗?不管檔案多大,經過MD5後都能生成唯一的MD5值

SHA (Secure Hash Algorithm,安全雜湊演算法),數字簽名等密碼學應用中重要的工具,安全性高于MD5

HMAC (Hash Message Authentication Code),散列消息驗證碼,基于密鑰的Hash算法的認證協定。用公開函數和密鑰産生一個固定長度的值作為認證辨別,用這個辨別鑒别消息的完整性。常用于接口簽名驗證

Payload

載荷就是存放有效資訊的地方。

有效資訊包含三個部分

1.标準中注冊的聲明

2.公共的聲明

3.私有的聲明

标準中注冊的聲明 (建議但不強制使用) :

iss

: jwt簽發者

sub

: 面向的使用者(jwt所面向的使用者)

aud

: 接收jwt的一方

exp

: 過期時間戳(jwt的過期時間,這個過期時間必須要大于簽發時間)

nbf

: 定義在什麼時間之前,該jwt都是不可用的.

iat

: jwt的簽發時間

jti

: jwt的唯一身份辨別,主要用來作為一次性

token

,進而回避重播攻擊。

公共的聲明 :

公共的聲明可以添加任何的資訊,一般添加使用者的相關資訊或其他業務需要的必要資訊.但不建議添加敏感資訊,因為該部分在用戶端可解密.

私有的聲明 :

私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感資訊,因為

base64

是對稱解密的,意味着該部分資訊可以歸類為明文資訊。

Signature

jwt的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:

header (base64後的)

payload (base64後的)

secret

這個部分需要

base64

加密後的

header

base64

payload

使用

.

連接配接組成的字元串,然後通過

header

中聲明的加密方式進行加鹽

secret

組合加密,然後就構成了

jwt

的第三部分。

密鑰

secret

是儲存在服務端的,服務端會根據這個密鑰進行生成

token

和進行驗證,是以需要保護好。

下面來進行SpringBoot和JWT的內建

引入

JWT

依賴,由于是基于

Java

,是以需要的是

java-jwt

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

需要自定義兩個注解

用來跳過驗證的

PassToken

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}
           

需要登入才能進行操作的注解

UserLoginToken

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}
           

@Target

:注解的作用目标

@Target(ElementType.TYPE)

——接口、類、枚舉、注解

@Target(ElementType.FIELD)

——字段、枚舉的常量

@Target(ElementType.METHOD)

——方法

@Target(ElementType.PARAMETER)

——方法參數

@Target(ElementType.CONSTRUCTOR)

——構造函數

@Target(ElementType.LOCAL_VARIABLE)

——局部變量

@Target(ElementType.ANNOTATION_TYPE)

——注解

@Target(ElementType.PACKAGE)

——包

@Retention

:注解的保留位置

RetentionPolicy.SOURCE

:這種類型的

Annotations

隻在源代碼級别保留,編譯時就會被忽略,在

class

位元組碼檔案中不包含。

RetentionPolicy.CLASS

Annotations

編譯時被保留,預設的保留政策,在

class

檔案中存在,但

JVM

将會忽略,運作時無法獲得。

RetentionPolicy.RUNTIME

Annotations

将被

JVM

保留,是以他們能在運作時被

JVM

或其他使用反射機制的代碼所讀取和使用。

@Document

:說明該注解将被包含在

javadoc

@Inherited

:說明子類可以繼承父類中的該注解

簡單自定義一個實體類

User

,使用

lombok

簡化實體類的編寫

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String Id;
    String username;
    String password;
}
           

需要寫

token

的生成方法

public String getToken(User user) {
        String token="";
        token= JWT.create().withAudience(user.getId())
                .sign(Algorithm.HMAC256(user.getPassword()));
        return token;
    }
           

Algorithm.HMAC256()

:使用

HS256

生成

token

,密鑰則是使用者的密碼,唯一密鑰的話可以儲存在服務端。

withAudience()

存入需要儲存在

token

的資訊,這裡我把使用者

ID

存入

token

接下來需要寫一個攔截器去擷取

token

并驗證

token

public class AuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    UserService userService;
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("token");// 從 http 請求頭中取出 token
        // 如果不是映射到方法直接通過
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //檢查是否有passtoken注釋,有則跳過認證
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //檢查有沒有需要使用者權限的注解
        if (method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                // 執行認證
                if (token == null) {
                    throw new RuntimeException("無token,請重新登入");
                }
                // 擷取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("401");
                }
                User user = userService.findUserById(userId);
                if (user == null) {
                    throw new RuntimeException("使用者不存在,請重新登入");
                }
                // 驗證 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
                    throw new RuntimeException("401");
                }
                return true;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, 
                                  HttpServletResponse httpServletResponse, 
                            Object o, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, 
                                          HttpServletResponse httpServletResponse, 
                                          Object o, Exception e) throws Exception {
    }
           

實作一個攔截器就需要實作

HandlerInterceptor

接口

HandlerInterceptor

接口主要定義了三個方法

1.

boolean preHandle ()

預處理回調方法,實作處理器的預處理,第三個參數為響應的處理器,自定義

Controller

,傳回值為

true

表示繼續流程(如調用下一個攔截器或處理器)或者接着執行

postHandle()

afterCompletion()

false

表示流程中斷,不會繼續調用其他的攔截器或處理器,中斷執行。

2.

void postHandle()

後處理回調方法,實作處理器的後處理(

DispatcherServlet

進行視圖傳回渲染之前進行調用),此時我們可以通過

modelAndView

(模型和視圖對象)對模型資料進行處理或對視圖進行處理,

modelAndView

也可能為

null

3.

void afterCompletion()

:

整個請求處理完畢回調方法,該方法也是需要目前對應的

Interceptor

preHandle()

的傳回值為true時才會執行,也就是在

DispatcherServlet

渲染了對應的視圖之後執行。用于進行資源清理。整個請求處理完畢回調方法。如性能監控中我們可以在此記錄結束時間并輸出消耗時間,還可以進行一些資源清理,類似于

try-catch-finally

中的

finally

,但僅調用處理器執行鍊中

主要流程:

1.從

http

請求頭中取出

token

2.判斷是否映射到方法

3.檢查是否有

passtoken

注釋,有則跳過認證

4.檢查有沒有需要使用者登入的注解,有則需要取出并驗證

5.認證通過則可以通路,不通過會報相關錯誤資訊

配置攔截器

在配置類上添加了注解

@Configuration

,标明了該類是一個配置類并且會将該類作為一個

SpringBean

添加到

IOC

容器内

@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");    // 攔截所有請求,通過判斷是否有 @LoginRequired 注解 決定是否需要登入
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}
           

WebMvcConfigurerAdapter

該抽象類其實裡面沒有任何的方法實作,隻是空實作了接口

WebMvcConfigurer

内的全部方法,并沒有給出任何的業務邏輯處理,這一點設計恰到好處的讓我們不必去實作那些我們不用的方法,都交由

WebMvcConfigurerAdapter

抽象類空實作,如果我們需要針對具體的某一個方法做出邏輯處理,僅僅需要在

WebMvcConfigurerAdapter

子類中

@Override

對應方法就可以了。

注:

SpringBoot2.0

Spring 5.0

WebMvcConfigurerAdapter

已被廢棄

網上有說改為繼承

WebMvcConfigurationSupport()

,不過試了下,還是過期的

解決方法:

直接實作

WebMvcConfigurer

(官方推薦)

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");   
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}
           

InterceptorRegistry

内的

addInterceptor

需要一個實作

HandlerInterceptor

接口的攔截器執行個體,

addPathPatterns

方法用于設定攔截器的過濾路徑規則。

這裡我攔截所有請求,通過判斷是否有

@LoginRequired

注解 決定是否需要登入

在資料通路接口中加入登入操作注解

@RestController
@RequestMapping("api")
public class UserApi {
    @Autowired
    UserService userService;
    @Autowired
    TokenService tokenService;
    //登入
    @PostMapping("/login")
    public Object login(@RequestBody User user){
        JSONObject jsonObject=new JSONObject();
        User userForBase=userService.findByUsername(user);
        if(userForBase==null){
            jsonObject.put("message","登入失敗,使用者不存在");
            return jsonObject;
        }else {
            if (!userForBase.getPassword().equals(user.getPassword())){
                jsonObject.put("message","登入失敗,密碼錯誤");
                return jsonObject;
            }else {
                String token = tokenService.getToken(userForBase);
                jsonObject.put("token", token);
                jsonObject.put("user", userForBase);
                return jsonObject;
            }
        }
    }
    @UserLoginToken
    @GetMapping("/getMessage")
    public String getMessage(){
        return "你已認證驗證";
    }
}
           

不加注解的話預設不驗證,登入接口一般是不驗證的。在

getMessage()

中我加上了登入注解,說明該接口必須登入擷取

token

後,在請求頭中加上

token

并通過驗證才可以通路

下面進行測試,啟動項目,使用postman測試接口

在沒

token

的情況下通路

api/getMessage

我這裡使用了統一異常處理,是以隻看到錯誤

message

下面進行登入,進而擷取

token

登入操作我沒加驗證注解,是以可以直接通路

token

加在請求頭中,再次通路

api/getMessage

注意:這裡的

key

一定不能錯,因為在攔截器中是取關鍵字

token

的值

String token = httpServletRequest.getHeader("token");

加上

token

之後就可以順利通過驗證和進行接口通路了

github項目源碼位址: https://github.com/JinBinPeng/springboot-jwt