package com.yoyuzh.auth; import com.yoyuzh.config.JwtProperties; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Date; import org.springframework.util.StringUtils; @Component public class JwtTokenProvider { private static final String DEFAULT_SECRET = "change-me-change-me-change-me-change-me"; private final JwtProperties jwtProperties; private SecretKey secretKey; public JwtTokenProvider(JwtProperties jwtProperties) { this.jwtProperties = jwtProperties; } @PostConstruct public void init() { String secret = jwtProperties.getSecret() == null ? "" : jwtProperties.getSecret().trim(); if (secret.isEmpty()) { throw new IllegalStateException("app.jwt.secret 未配置,请设置强密钥后再启动"); } if (DEFAULT_SECRET.equals(secret)) { throw new IllegalStateException("检测到默认 JWT 密钥,请替换 app.jwt.secret 后再启动"); } if (secret.getBytes(StandardCharsets.UTF_8).length < 32) { throw new IllegalStateException("JWT 密钥长度过短,至少需要 32 字节"); } secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateAccessToken(Long userId, String username, String sessionId) { return generateAccessToken(userId, username, sessionId, AuthClientType.DESKTOP); } public String generateAccessToken(Long userId, String username, String sessionId, AuthClientType clientType) { Instant now = Instant.now(); var builder = Jwts.builder() .subject(username) .claim("uid", userId) .claim("client", clientType.name()) .issuedAt(Date.from(now)) .expiration(Date.from(now.plusSeconds(jwtProperties.getAccessExpirationSeconds()))) .signWith(secretKey); if (StringUtils.hasText(sessionId)) { builder.claim("sid", sessionId); } return builder.compact(); } public boolean validateToken(String token) { try { Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); return true; } catch (Exception ex) { return false; } } public String getUsername(String token) { return parseClaims(token).getSubject(); } public Long getUserId(String token) { Object uid = parseClaims(token).get("uid"); return uid == null ? null : Long.parseLong(uid.toString()); } public Instant getIssuedAt(String token) { Date issuedAt = parseClaims(token).getIssuedAt(); return issuedAt == null ? null : issuedAt.toInstant(); } public String getSessionId(String token) { Object sessionId = parseClaims(token).get("sid"); return sessionId == null ? null : sessionId.toString(); } public AuthClientType getClientType(String token) { Object clientType = parseClaims(token).get("client"); return AuthClientType.fromHeader(clientType == null ? null : clientType.toString()); } public boolean hasMatchingSession(String token, String activeSessionId) { String tokenSessionId = getSessionId(token); if (!StringUtils.hasText(activeSessionId)) { return !StringUtils.hasText(tokenSessionId); } return activeSessionId.equals(tokenSessionId); } public boolean hasMatchingSession(String token, User user) { String expectedSessionId = switch (getClientType(token)) { case MOBILE -> user.getMobileActiveSessionId(); case DESKTOP -> StringUtils.hasText(user.getDesktopActiveSessionId()) ? user.getDesktopActiveSessionId() : user.getActiveSessionId(); }; return hasMatchingSession(token, expectedSessionId); } private Claims parseClaims(String token) { return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload(); } }