Jwt的新手入門教程

Alickx 發表於 2021-09-21

Jwt的新手入門教程

1.Jwt究竟是什麼東東?

​ 先貼官網地址:JSON Web Tokens - jwt.io

image-20210921201459885

​ 再貼官方的定義:

What is JSON Web Token?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

​ 我的理解總結:Jwt全稱是Json Web Token,顧名思義就是一種通過Json方式來傳輸的令牌,並且他最大的特點就是signed tokens,簽發者可以使用各種的加密方式對資訊進行簽名,更重要的是Jwt還能驗證token是否被篡改或者token是否正確。當然了,這種方式註定是不安全的。我們很容易地就可以在官網得到這一結論。

image-20210921203154437

​ 可以看到,只要我們獲得token字串,就可以獲取到裡面的大部分資訊,除了簽名的金鑰。

所以Jwt簡單來說,就是簡單地儲存部分不那麼重要的資訊,通過Json,對客戶端進行驗證的一種方式。

2.Jwt的組成

​ 從官網的Debugger介面,我們可以得知,Jwt由三部分組成。

image-20210921203716689

  • 第一部分:Header,Header通常由令牌的型別和加密的演算法組成,也就是
{
  "alg": "HS256",
  "typ": "JWT"
}

這個Header的含義就是"alg"--Algorithm(演算法) 是HS256。

  • 第二部分: Payload,這部分主要是記錄我們所儲存的簡單且不重要的資訊。例如:使用者名稱,過期時間,使用者id等等。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

但是要注意的是,Payload記錄的這些資訊是完全公開的,所以千萬不能把使用者或者系統的敏感資料放到Payload中,Payload只是負責記錄簡單資訊,並不具備加密的功能。

  • 第三部分:Signature, 簽名裡是由三部分組成,Header的Base64編碼,Payload的Base64編碼,還有secret,然後通過指定的加密方式,例如HS256,進行加密後得出的字串。
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

可以說這部分是Jwt的重點,因為他承擔著兩個作用,第一個:驗證JwtToken在傳輸過程中沒有受到篡改 第二個:驗證簽發人的身份

​ 詳細說一下這兩個作用。

驗證JwtToken在傳輸過程中沒有受到篡改,這個原理就比較好理解,因為Signature中有一個字串,也就是secret,這個secret是由我們來設定的,相當於私鑰,只有我們自己才知道,別人是不知道的。那麼在後續驗證過程中,只要我們自己生產的Token與客戶端傳來的Token進行比對,如果一致那就證明該Token沒有受到篡改,反之則證明客戶端傳來的Token是非法的。

驗證簽發人的身份,其實第二個作用是從第一個作用中展現而來的,因為如果能證明Token在傳輸過程中沒有受到篡改,那就更加說明服務端是這個簽名的簽發者,因為只有簽發者才知道私鑰

​ 其實secret我們也可以認為是鹽(salt),我們知道如果單純地在資料庫中儲存明文密碼,或者是隻經過一重MD5加密的密碼,是非常的不安全,因為就算是經過MD5加密,仍然可以通過暴力窮舉的方式來進行破解,可是如果在使用者密碼的基礎上,加上我們生成的隨機或固定的字串,然後再進行加密,那麼安全程度會大大提升。

​ 如果不知道鹽(salt)的童鞋,可以去bind搜尋一下相關資料,其實理解起來就是使用者密碼+我們設定的隨機或固定字串再進行多重加密。

3.編碼實現

​ 我們來建立這幾個類。

image-20210921215324634

配置類:InterceptorConfig

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }

    @Bean
    public JwtInterceptor authenticationInterceptor() {
        return new JwtInterceptor();
    }

}

控制類: UserController

@RestController
public class UserController {


    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody User user) {

        Map<String, Object> map = new HashMap<>();
        String username = user.getUsername();
        String password = user.getPassword();
        // 省略 賬號密碼驗證
        // 驗證成功後傳送token
        String token = JwtUtil.sign(username, password);
        if (token != null) {
            map.put("code", "200");
            map.put("message", "認證成功");
            map.put("token", token);
            return map;
        }
        map.put("code", "403");
        map.put("message", "認證失敗");
        return map;
    }
    
    @GetMapping(value = "/api/test")
    public String get(){
        System.out.println("執行了get請求");
        return "success";
    }
}

服務類:UserService

@Service
public class UserService {

    public String getPassword(){

        return "admin";
    }
}

實體類 User

@Data
public class User {

    private String username;
    private String password;

}

自定義攔截器類 JwtIntercepter

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 從 http 請求頭中取出 token
        String token = request.getHeader("token");
        // 如果請求不是對映到方法直接通過
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        if (token != null){
            String username = JwtUtil.getUserNameByToken(request);
            // 這邊拿到的 使用者名稱 應該去資料庫查詢獲得密碼,簡略,步驟在service直接獲取密碼
            boolean result = JwtUtil.verify(token,username,userService.getPassword());
            if(result){
                System.out.println("通過攔截器");
                return true;
            }
        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

Jwt工具類 JwtUtil

public class JwtUtil {

    // Token一天後過期
    public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;


    //檢驗Token是否正確
    public static boolean verify(String token, String username, String secret) {
        try {
            // 設定加密演算法
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            // 效驗TOKEN
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }


    public static String sign(String username, String secret) {
        //現在系統的時間 + 一天
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //對密碼進行加密
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附帶username資訊
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
    }
    
    public static String getUserNameByToken(HttpServletRequest request)  {
        String token = request.getHeader("token");
        DecodedJWT jwt = JWT.decode(token);
        return jwt.getClaim("username")
                .asString();
    }
    
}

具體程式碼,我上傳到github上:Alickx/JwtTokenDemo: JwtDemo程式碼 (github.com)

這裡程式碼的secret則是使用者的密碼。

我們先通過PostMan向/login介面傳送我們的賬號密碼,得到Jwt根據我們的賬號密碼生成的token。

{
    "code": "200",
    "message": "認證成功",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzIzMTg1NjksInVzZXJuYW1lIjoiYWRtaW4ifQ.c_m3z11UOcFcS_hZN9KNlidzZ2j6y_Ugkb9awHQ3FGY"
}

image-20210921221458979

這裡認證成功後,伺服器如果不經過我們其他的儲存操作,是不會對生成的token進行持久化或其他控制的,所以一旦簽發出去,這個token串就會變成無狀態了。

​ 接著,我們訪問一下我們的api測試介面,因為我們在config裡配置了全域性攔截器,且重寫了HandlerInterceptor類,除了/login路徑,其他全路徑都需要請求的Header(請求頭)中帶有token欄位,且需驗證token成功後才會允許訪問,否則進行攔截。

image-20210921222244047

4.管理JwtToken的狀態

​ 要做到管理JwtToken的狀態,我們可以通過把token儲存到Redis資料庫中,通過設定key的過期時間,就可以做到對Jwt的過期操作,同時也能夠對Token進行續簽,失效等等操作。這部分先不去仔細探究,有個思路就可以了。具體的編碼實現我相信也不難。

本文中所有的程式碼均已上傳到github上,如有需要請下載。

Alickx/JwtTokenDemo: JwtDemo程式碼 (github.com)

JwtTokenDemo (gitee.com)