本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

149 lines
4.8 KiB

package com.wok.supportbot.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
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.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* SDK 专用 JWT 令牌提供者
* 独立于管理后台 JwtTokenProvider,使用独立密钥签发和验证 SDK Token。
* <p>
* Token 中包含:
* - sub: apiKeyId(API Key 标识)
* - ak: 脱敏后的 API Key
* - rids: 允许使用的客服角色 ID 列表
*/
@Slf4j
@Component
public class SdkJwtTokenProvider {
private final SecretKey key;
private final long defaultExpiration;
/** 24 小时上限 */
private static final long MAX_EXPIRATION = 86400000L;
/** 5 分钟下限 */
private static final long MIN_EXPIRATION = 300000L;
/** 默认密钥标识(禁止使用) */
private static final String DEFAULT_SECRET = "support-bot-sdk-jwt-secret-2026-please-change";
public SdkJwtTokenProvider(
@Value("${jwt.sdk-secret:support-bot-sdk-jwt-secret-2026-please-change}") String sdkSecret,
@Value("${jwt.sdk-expiration:7200000}") long defaultExpiration) {
if (DEFAULT_SECRET.equals(sdkSecret)) {
log.warn("⚠️ SDK JWT 使用了默认密钥,存在安全风险!请在 application.yml 中配置 jwt.sdk-secret 为强随机字符串");
}
this.key = Keys.hmacShaKeyFor(sdkSecret.getBytes(StandardCharsets.UTF_8));
this.defaultExpiration = defaultExpiration;
}
/**
* 签发 SDK JWT Token
*
* @param apiKeyId API Key 标识
* @param maskedApiKey 脱敏后的 API Key
* @param roleIds 允许使用的客服角色 ID 列表
* @param expirationMs 过期时间(毫秒),会被钳制到 [5min, 24h]
* @return JWT Token 字符串
*/
public String generateToken(String apiKeyId, String maskedApiKey, List<Long> roleIds, long expirationMs) {
long clampedExpiration = clampExpiration(expirationMs);
Date now = new Date();
return Jwts.builder()
.subject(apiKeyId)
.claim("ak", maskedApiKey)
.claim("rids", roleIds)
.issuedAt(now)
.expiration(new Date(now.getTime() + clampedExpiration))
.signWith(key)
.compact();
}
/**
* 使用默认过期时间签发 SDK JWT Token
*/
public String generateToken(String apiKeyId, String maskedApiKey, List<Long> roleIds) {
return generateToken(apiKeyId, maskedApiKey, roleIds, defaultExpiration);
}
/**
* 验证并解析 Token,返回 Claims
*
* @param token JWT Token 字符串
* @return Claims 对象
* @throws ExpiredJwtException Token 已过期
* @throws JwtException Token 签名无效或格式错误
*/
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 验证 Token 是否有效(签名正确且未过期)
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 从 Claims 中提取 apiKeyId(subject)
*/
public String getApiKeyId(Claims claims) {
return claims.getSubject();
}
/**
* 从 Claims 中提取允许的角色 ID 集合
* JWT 中 rids 为 List&lt;Integer&gt;,需转换为 Set&lt;Long&gt;
*/
@SuppressWarnings("unchecked")
public Set<Long> getAllowedRoleIds(Claims claims) {
List<?> rids = claims.get("rids", List.class);
if (rids == null) {
return Set.of();
}
return rids.stream()
.map(r -> ((Number) r).longValue())
.collect(Collectors.toSet());
}
/**
* 脱敏 API Key: sk_ab****xyz0
* 保留前 4 位和后 4 位,中间用 **** 替代
*/
public static String maskApiKey(String keyValue) {
if (keyValue == null || keyValue.length() < 12) {
return "****";
}
return keyValue.substring(0, 4) + "****" + keyValue.substring(keyValue.length() - 4);
}
/**
* 将过期时间钳制到 [5min, 24h] 范围内
*/
private long clampExpiration(long expirationMs) {
return Math.max(MIN_EXPIRATION, Math.min(expirationMs, MAX_EXPIRATION));
}
}