| | |
| | | package com.vincent.rsf.openApi.service.impl; |
| | | |
| | | import com.vincent.rsf.openApi.entity.constant.Constants; |
| | | import com.vincent.rsf.openApi.security.service.AppAuthService; |
| | | import com.vincent.rsf.openApi.service.TokenService; |
| | | import io.jsonwebtoken.Claims; |
| | | import io.jsonwebtoken.JwtException; |
| | | import io.jsonwebtoken.Jwts; |
| | | import io.jsonwebtoken.SignatureAlgorithm; |
| | | import io.jsonwebtoken.security.Keys; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.crypto.SecretKey; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.security.MessageDigest; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** |
| | | * 对接 Token:JWT(密钥来自配置,重启后同一密钥仍可校验未过期 token;claims 仅含 appId) |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class TokenServiceImpl implements TokenService { |
| | | |
| | | private static final long EXPIRE_MS = 60 * 60 * 1000L; |
| | | private static final String CLAIM_APP_ID = "appId"; |
| | | |
| | | private final AppAuthService appAuthService; |
| | | private final SecretKey secretKey; |
| | | private final long expireMs; |
| | | |
| | | @Autowired |
| | | private AppAuthService appAuthService; |
| | | public TokenServiceImpl( |
| | | AppAuthService appAuthService, |
| | | @Value("${open-api.jwt.secret:}") String jwtSecret, |
| | | @Value("${open-api.jwt.expire-seconds:3600}") long expireSeconds) { |
| | | this.appAuthService = appAuthService; |
| | | if (!StringUtils.hasText(jwtSecret)) { |
| | | throw new IllegalStateException("请配置 open-api.jwt.secret(HS256,建议至少 32 字符或使用环境变量覆盖)"); |
| | | } |
| | | this.secretKey = hmacSha256Key(jwtSecret.trim()); |
| | | this.expireMs = expireSeconds * 1000L; |
| | | } |
| | | |
| | | private final Map<String, Long> tokenExpire = new ConcurrentHashMap<>(); |
| | | /** 短字符串用 SHA-256 派生 32 字节以满足 HS256 */ |
| | | private static SecretKey hmacSha256Key(String secret) { |
| | | byte[] bytes = secret.getBytes(StandardCharsets.UTF_8); |
| | | if (bytes.length < 32) { |
| | | try { |
| | | MessageDigest md = MessageDigest.getInstance("SHA-256"); |
| | | bytes = md.digest(bytes); |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException(e); |
| | | } |
| | | } |
| | | return Keys.hmacShaKeyFor(bytes); |
| | | } |
| | | |
| | | @Override |
| | | public String issueToken(String appId, String appSecret) { |
| | |
| | | if (!appAuthService.validateApp(appId, appSecret)) { |
| | | return null; |
| | | } |
| | | String token = UUID.randomUUID().toString().replace("-", ""); |
| | | tokenExpire.put(token, System.currentTimeMillis() + EXPIRE_MS); |
| | | evictExpired(); |
| | | return token; |
| | | long now = System.currentTimeMillis(); |
| | | Map<String, Object> claims = new HashMap<>(); |
| | | claims.put(CLAIM_APP_ID, appId); |
| | | String compact = Jwts.builder() |
| | | .setClaims(claims) |
| | | .setIssuedAt(new Date(now)) |
| | | .setExpiration(new Date(now + expireMs)) |
| | | .signWith(secretKey, SignatureAlgorithm.HS256) |
| | | .compact(); |
| | | return Constants.TOKEN_PREFIX + compact; |
| | | } |
| | | |
| | | @Override |
| | | public boolean validateToken(String token) { |
| | | if (!StringUtils.hasText(token)) { |
| | | return false; |
| | | } |
| | | Long expire = tokenExpire.get(token); |
| | | if (expire == null || System.currentTimeMillis() > expire) { |
| | | tokenExpire.remove(token); |
| | | return false; |
| | | } |
| | | return true; |
| | | return getAppIdIfValid(token) != null; |
| | | } |
| | | |
| | | private void evictExpired() { |
| | | long now = System.currentTimeMillis(); |
| | | tokenExpire.entrySet().removeIf(e -> e.getValue() < now); |
| | | @Override |
| | | public String getAppIdIfValid(String rawToken) { |
| | | if (!StringUtils.hasText(rawToken)) { |
| | | return null; |
| | | } |
| | | try { |
| | | Claims claims = Jwts.parserBuilder() |
| | | .setSigningKey(secretKey) |
| | | .build() |
| | | .parseClaimsJws(rawToken) |
| | | .getBody(); |
| | | Date exp = claims.getExpiration(); |
| | | if (exp == null || exp.before(new Date())) { |
| | | return null; |
| | | } |
| | | Object appId = claims.get(CLAIM_APP_ID); |
| | | return appId != null ? appId.toString() : null; |
| | | } catch (JwtException e) { |
| | | log.debug("JWT 校验失败: {}", e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | } |