From b7ed804af2750beefdf6911fdf3e17a7a3a940a8 Mon Sep 17 00:00:00 2001 From: zichun Date: Thu, 7 May 2026 09:13:04 +0800 Subject: [PATCH] feat(usr): JwtTokenProvider sign/parse REQ-USR-004 --- backend/src/main/java/com/xly/erp/module/usr/security/JwtTokenProvider.java | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ backend/src/test/java/com/xly/erp/module/usr/security/JwtTokenProviderTest.java | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/com/xly/erp/module/usr/security/JwtTokenProvider.java create mode 100644 backend/src/test/java/com/xly/erp/module/usr/security/JwtTokenProviderTest.java diff --git a/backend/src/main/java/com/xly/erp/module/usr/security/JwtTokenProvider.java b/backend/src/main/java/com/xly/erp/module/usr/security/JwtTokenProvider.java new file mode 100644 index 0000000..1faedc2 --- /dev/null +++ b/backend/src/main/java/com/xly/erp/module/usr/security/JwtTokenProvider.java @@ -0,0 +1,50 @@ +package com.xly.erp.module.usr.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; + +/** REQ-USR-004 JWT 签发 / 校验封装。HS256,secret 来自 .env.local JWT_SECRET。 */ +@Component +public class JwtTokenProvider { + + private final SecretKey key; + private final long expiresInSeconds; + + public JwtTokenProvider(@Value("${erp.jwt.secret}") String secret, + @Value("${erp.jwt.expires-in-seconds}") long expiresInSeconds) { + this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.expiresInSeconds = expiresInSeconds; + } + + public long getExpiresInSeconds() { + return expiresInSeconds; + } + + public String sign(int uid, String username, String userType) { + Instant now = Instant.now(); + return Jwts.builder() + .subject(username) + .claim("uid", uid) + .claim("type", userType) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(expiresInSeconds))) + .signWith(key) + .compact(); + } + + public Claims parse(String token) { + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} diff --git a/backend/src/test/java/com/xly/erp/module/usr/security/JwtTokenProviderTest.java b/backend/src/test/java/com/xly/erp/module/usr/security/JwtTokenProviderTest.java new file mode 100644 index 0000000..ddc0041 --- /dev/null +++ b/backend/src/test/java/com/xly/erp/module/usr/security/JwtTokenProviderTest.java @@ -0,0 +1,53 @@ +package com.xly.erp.module.usr.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.junit.jupiter.api.Test; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JwtTokenProviderTest { + + private static final String SECRET = "f8d4be76bff13bf32fa33ca0b14a4b152ad01ca5719f57df18ec4ecf2370b235"; + private static final long EXPIRES_IN = 7200L; + + @Test + void signAndParse_returnsClaims() { + JwtTokenProvider provider = new JwtTokenProvider(SECRET, EXPIRES_IN); + + String token = provider.sign(42, "alice", "普通用户"); + + Claims claims = provider.parse(token); + assertThat(claims.getSubject()).isEqualTo("alice"); + assertThat(claims.get("uid", Integer.class)).isEqualTo(42); + assertThat(claims.get("type", String.class)).isEqualTo("普通用户"); + assertThat(claims.getExpiration().getTime() - claims.getIssuedAt().getTime()) + .isEqualTo(EXPIRES_IN * 1000L); + } + + @Test + void parseExpiredToken_throwsExpiredJwtException() { + // 用同一 secret 手工签一个已过期 token + SecretKey key = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)); + Instant now = Instant.now(); + String expired = Jwts.builder() + .subject("alice") + .issuedAt(Date.from(now.minusSeconds(7200))) + .expiration(Date.from(now.minusSeconds(60))) + .signWith(key) + .compact(); + + JwtTokenProvider provider = new JwtTokenProvider(SECRET, EXPIRES_IN); + + assertThatThrownBy(() -> provider.parse(expired)) + .isInstanceOf(ExpiredJwtException.class); + } +} -- libgit2 0.22.2