Commit 7fbd1447e12d9b017f91dc05bbaf1066636e6e7b
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
Showing
3 changed files
with
147 additions
and
0 deletions
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 | +} | ... | ... |