iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0

今天第22天來補足JWT驗證的邏輯,順便將控制器內產生JWT的邏輯進行封裝到專門的模組

為什麼要將邏輯分層

昨日文章在結尾時有提到,控制器內實作產生JWT的邏輯違反單一職責原則,居然知道違反甚麼原則,那麼問題就很清晰在於「職責」的定義,那麼如何對職責進行切割,首先來看MVC架構中控制器關注的重點,就是在於接收請求,並做相應邏輯處理,在來看現在的情境是開設API,用來核發和驗證JWT兩種需求,直接在控制器中撰寫邏輯能滿足需求

這時候反問一個問題,若有其他API端點一樣要用到JWT的使用,但只是修改部分自定義的PAYLOAD,這時候將整個程式碼複製貼上改相應邏輯,只會產生更多重複的程式碼段落,除此程式碼重複之外,基於某些因素要更改JWT的相關配置,就要去改N個控制器類別,這時的情況是本來只接收請求的控制器,多了另一個管理JWT邏輯的職責,為了改變這個問題,將JWT處理邏輯封裝到一個模組內,可以讓職責更清晰,也能避免重複的程式碼邏輯,帶來的負面影響

建立JWT工具類

這是通用的工具,工具類就放在utils資料夾下,在下面新增一個 JwtUtil的類別(記得檢查有沒有安裝依賴項目)

 // 添加 Component 注釋表示這個元件,會由IoC容器管理
@Component
public class JwtUtil {

    @Value("${jwt.expire_time}")
    private int expireTime;

    @Value("${jwt.secret}")
    private String secret;
}

JwtUtil 實作產生 JWT Token的方法

實作產生 JWT Token 的方法,邏輯大致沒變化,透過方法參數傳入相應資料即可

    /**
     * 初始化 Token 方法
     * 
     * @param Map<String, Object> claims JWT要附加的屬性
     * @param String      subject 識別主體值
     *
     * @return String
     */
    public String generateToken(
            Map<String, Object> payload,
            String subject) {

        Date expireDate = new Date(System.currentTimeMillis() + expireTime);
        // 產生隨機 UUDI 當作JWT ID
        String jwtIdentityId = UUID.randomUUID().toString();

        JwtBuilder jwtBuilder = Jwts.builder();
        if (payload != null) {
            Claims useClaims = Jwts.claims(payload);
            jwtBuilder.setClaims(useClaims);
        }

        return jwtBuilder.setSubject(subject)
                .setId(jwtIdentityId)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(expireDate)
                .signWith(generateKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    /**
         * 將 String 轉換成 Key物件
         *
         * @return io.jsonwebtoken.security.Keys
         */
    private SecretKey generateKey() {
            byte[] encodeKey = secret.getBytes();
            // Secret 轉換成符合 SHA規範的值
            return Keys.hmacShaKeyFor(encodeKey);
    }

打開 APIController 調整昨日使用JWT的實作


// 建立屬性,用來注入JWT工具類
@Autowired
private JwtUtil jwtUtil;

// 調整authJwt實作
@GetMapping("/jwt/generate")
public Map<String, Object> authJwt() {

    Map<String, Object> claims = new HashMap<>();
    claims.put("language", "java");

    String jwtToken = jwtUtil.generateToken(claims, "Java Programer");

    Map<String, Object> response = new HashMap<>();
    response.put("token", jwtToken);
    return response;
}

打開Postman測試API取得Token,並將結果貼到 jwt.io 查看與修改後的資料相符

https://ithelp.ithome.com.tw/upload/images/20231005/20161920WwljlTJuZO.png

用JwtUtil實現驗證方法

解析 Token 內容

/**
 * 
 * @param String token 要解析的 Token
 * 
 * @return Claims
 */
private Claims parseToken(String token) {
    return Jwts.parserBuilder().setSigningKey(generateKey())
            .build().parseClaimsJws(token).getBody();
}

封裝方法,套用Claims定義好Lambda函式取得特定欄位值

/**
 * @return <T> T 實際回傳型別依套用的 resovler為主
 */
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = parseToken(token);
    return claimsResolver.apply(claims);
}

使用Claims內建函示 getExpiration 驗證憑證是否到期

/**
 * 驗證 Tokem是否逾期
 *
 * @return Boolean
 */
public Boolean validateToken(String token) {
    final Date expiration = getClaimFromToken(token, Claims::getExpiration);
    return expiration.before(new Date()) == false;
}

用單元測試來跑JWT模組

為了方便講解,模組調整的部分用單元測試來實作,現在來安裝junit進行測試,

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

接著在tests資料夾下新增測試項目,測試類別就叫JwtUtilTest

// JwtUtilTest.java

@SpringBootTest
// 定義測試的環境設定套用 application-test.properties
@ActiveProfiles("test") 
public class JwtUtilTest {

    // 別忘了注入工具類
    @Autowired
    private JwtUtil jwtUtil;

    @Test
    void handleToken() {
        // 配置文件
        System.out.println("Run MyFirst Test");
        assertTrue(false);
    }
}

在 resource 目錄下新增測試環境配置 application-test.properties,變數名稱同 application.properties,套用值依開發者環境調整

執行 mvnw test -Dtest=JwtUtilTest#handleToken 試跑測試,有打印出文字表示

https://ithelp.ithome.com.tw/upload/images/20231005/20161920D4XgJa3Sku.png

測試產生JWT字串並執行Token驗證的邏輯

@Test
void handleToken() {
    // 配置文件
    Map<String, Object> claims = new HashMap<>();
    claims.put("created_by", "jtest");

    // 測試是否為 String
    String jwtToken = jwtUtil.generateToken(claims, "Java Programer");
    assertTrue(jwtToken instanceof String);

    // 測試 Token是否逾期或異常
    Boolean tokenIsValidated = jwtUtil.validateToken(jwtToken);
    assertTrue(tokenIsValidated);

    // 測試 解析不符合 Token 格式的字串
    assertThrows(MalformedJwtException.class, () -> {
        jwtUtil.validateToken("不符合Token的情況");
    });
}

執行 mvn test -Dtest=JwtUtilTest#tokenData 測試取得 Token 內容

@Test
void tokenData() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("founder", "jtest");
    String jwtToken = jwtUtil.generateToken(claims, "tokenData");
    Claims payload = jwtUtil.parseToken(jwtToken);

    // 驗證 founder 欄位資料
    String founder = payload.get("founder", String.class);
    assertEquals("jtest", founder);

    // 驗證 sub 資料
    String sub = payload.get("sub", String.class);
    assertEquals("tokenData", sub);
}

前面範例用到的 -Dtest選項,是用來指定要執行的類別,添加#方法名稱則就是對特定類別的方法進行測試,現在對tests目錄下所有測試項目,執行 mvn test執行所有測試

參考

  1. https://igouist.github.io/post/2021/10/newbie-5-3-layer-architecture
  2. https://zhuanlan.zhihu.com/p/381935767
  3. https://hackmd.io/@emisjerry/ryUn0FBEH/%2Fs%2Fui6Gi5UOTKeEnENybIQvUg

上一篇
第21日 來做有Jwt的API
下一篇
第23日 用單元測試看JWT驗證流程
系列文
掌握Java神器,駕馭SpringBoot猛獸30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言