package com.xly.erp.common.security; import com.xly.erp.common.exception.BizException; import com.xly.erp.common.response.ErrorCode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * JWT 签发与验证工具。HS256,密钥来自 ${JWT_SECRET}。 * docs/04 § 1.6。 */ @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; private SecretKey key; @PostConstruct void init() { byte[] bytes = secret.getBytes(StandardCharsets.UTF_8); if (bytes.length < 32) { throw new IllegalStateException( "JWT_SECRET 长度不足 32 字节(HS256 要求),实际 " + bytes.length + " 字节。请在 .env.local 配置至少 256 位的随机字符串。"); } this.key = Keys.hmacShaKeyFor(bytes); } public String issue(Map claims, long ttlSec) { long now = System.currentTimeMillis(); Map all = new HashMap<>(claims); String sub = String.valueOf(all.remove("sub")); String jti = UUID.randomUUID().toString(); return Jwts.builder() .subject(sub) .claims(all) .id(jti) .issuedAt(new Date(now)) .expiration(new Date(now + ttlSec * 1000L)) .signWith(key) .compact(); } public Map parse(String token) { try { Claims claims = Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); Map out = new HashMap<>(claims); out.put("sub", claims.getSubject()); out.put("jti", claims.getId()); out.put("iat", claims.getIssuedAt() != null ? claims.getIssuedAt().getTime() / 1000 : null); out.put("exp", claims.getExpiration() != null ? claims.getExpiration().getTime() / 1000 : null); return out; } catch (JwtException e) { throw new BizException(ErrorCode.BAD_CREDENTIALS, "token 无效或已过期"); } } }