Commit 7fbd1447e12d9b017f91dc05bbaf1066636e6e7b

Authored by zichun
1 parent 75fa00a6

feat(usr): JwtUtil generate + parse access/refresh token REQ-USR-004

- JwtProperties: @ConfigurationProperties("jwt") with secret/expiry
- JwtUtil: generateAccessToken/generateRefreshToken/parseAccessToken/parseRefreshToken
- parseRefreshToken validates type=refresh claim, throws 40103 if mismatch
- JwtUtilTest: 3 tests PASS
backend/src/main/java/com/example/erp/common/util/JwtUtil.java 0 → 100644
  1 +package com.example.erp.common.util;
  2 +
  3 +import com.example.erp.common.constants.AuthErrorCode;
  4 +import com.example.erp.common.exception.BizException;
  5 +import com.example.erp.config.JwtProperties;
  6 +import io.jsonwebtoken.Claims;
  7 +import io.jsonwebtoken.JwtException;
  8 +import io.jsonwebtoken.Jwts;
  9 +import io.jsonwebtoken.security.Keys;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.stereotype.Component;
  12 +
  13 +import javax.crypto.SecretKey;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.util.Date;
  16 +
  17 +@Component
  18 +@RequiredArgsConstructor
  19 +public class JwtUtil {
  20 +
  21 + private final JwtProperties properties;
  22 +
  23 + private SecretKey key() {
  24 + return Keys.hmacShaKeyFor(properties.getSecret().getBytes(StandardCharsets.UTF_8));
  25 + }
  26 +
  27 + public String generateAccessToken(String userId, String username, String userType, String brandId) {
  28 + long now = System.currentTimeMillis();
  29 + return Jwts.builder()
  30 + .subject(userId)
  31 + .claim("username", username)
  32 + .claim("userType", userType)
  33 + .claim("brandId", brandId)
  34 + .issuedAt(new Date(now))
  35 + .expiration(new Date(now + properties.getAccessTokenExpiry() * 1000))
  36 + .signWith(key(), Jwts.SIG.HS256)
  37 + .compact();
  38 + }
  39 +
  40 + public String generateRefreshToken(String userId, String brandId) {
  41 + long now = System.currentTimeMillis();
  42 + return Jwts.builder()
  43 + .subject(userId)
  44 + .claim("brandId", brandId)
  45 + .claim("type", "refresh")
  46 + .issuedAt(new Date(now))
  47 + .expiration(new Date(now + properties.getRefreshTokenExpiry() * 1000))
  48 + .signWith(key(), Jwts.SIG.HS256)
  49 + .compact();
  50 + }
  51 +
  52 + public Claims parseAccessToken(String token) {
  53 + return doParse(token);
  54 + }
  55 +
  56 + public Claims parseRefreshToken(String token) {
  57 + Claims claims = doParse(token);
  58 + if (!"refresh".equals(claims.get("type", String.class))) {
  59 + throw new BizException(AuthErrorCode.REFRESH_TOKEN_INVALID, "Refresh Token 已失效,请重新登录");
  60 + }
  61 + return claims;
  62 + }
  63 +
  64 + private Claims doParse(String token) {
  65 + try {
  66 + return Jwts.parser()
  67 + .verifyWith(key())
  68 + .build()
  69 + .parseSignedClaims(token)
  70 + .getPayload();
  71 + } catch (JwtException e) {
  72 + throw new BizException(AuthErrorCode.REFRESH_TOKEN_INVALID, "Token 已失效,请重新登录");
  73 + }
  74 + }
  75 +}
... ...
backend/src/main/java/com/example/erp/config/JwtProperties.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import lombok.Getter;
  4 +import lombok.Setter;
  5 +import org.springframework.boot.context.properties.ConfigurationProperties;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +@Getter
  9 +@Setter
  10 +@Component
  11 +@ConfigurationProperties(prefix = "jwt")
  12 +public class JwtProperties {
  13 + private String secret;
  14 + private long accessTokenExpiry;
  15 + private long refreshTokenExpiry;
  16 +}
... ...
backend/src/test/java/com/example/erp/common/JwtUtilTest.java 0 → 100644
  1 +package com.example.erp.common;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.common.util.JwtUtil;
  5 +import com.example.erp.config.JwtProperties;
  6 +import io.jsonwebtoken.Claims;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +
  10 +import static org.junit.jupiter.api.Assertions.*;
  11 +
  12 +class JwtUtilTest {
  13 +
  14 + private JwtUtil jwtUtil;
  15 +
  16 + @BeforeEach
  17 + void setUp() {
  18 + JwtProperties props = new JwtProperties();
  19 + props.setSecret("testSecretKey32CharactersMinimumXXXXX");
  20 + props.setAccessTokenExpiry(86400L);
  21 + props.setRefreshTokenExpiry(604800L);
  22 + jwtUtil = new JwtUtil(props);
  23 + }
  24 +
  25 + @Test
  26 + void generateAndParseAccessToken_containsAllClaims() {
  27 + String token = jwtUtil.generateAccessToken("u1", "admin", "超级管理员", "b1");
  28 + Claims claims = jwtUtil.parseAccessToken(token);
  29 +
  30 + assertEquals("u1", claims.getSubject());
  31 + assertEquals("admin", claims.get("username", String.class));
  32 + assertEquals("超级管理员", claims.get("userType", String.class));
  33 + assertEquals("b1", claims.get("brandId", String.class));
  34 + assertNotNull(claims.getExpiration());
  35 + }
  36 +
  37 + @Test
  38 + void parseRefreshToken_withAccessToken_throws40103() {
  39 + String accessToken = jwtUtil.generateAccessToken("u1", "admin", "普通用户", "b1");
  40 + BizException ex = assertThrows(BizException.class, () -> jwtUtil.parseRefreshToken(accessToken));
  41 + assertEquals(40103, ex.getCode());
  42 + }
  43 +
  44 + @Test
  45 + void parseAccessToken_withExpiredToken_throws40103() {
  46 + JwtProperties expiredProps = new JwtProperties();
  47 + expiredProps.setSecret("testSecretKey32CharactersMinimumXXXXX");
  48 + expiredProps.setAccessTokenExpiry(-1L);
  49 + expiredProps.setRefreshTokenExpiry(604800L);
  50 + JwtUtil expiredJwtUtil = new JwtUtil(expiredProps);
  51 +
  52 + String token = expiredJwtUtil.generateAccessToken("u1", "admin", "普通用户", "b1");
  53 + BizException ex = assertThrows(BizException.class, () -> jwtUtil.parseAccessToken(token));
  54 + assertEquals(40103, ex.getCode());
  55 + }
  56 +}
... ...