系统凭证调整

This commit is contained in:
caozehui
2026-05-13 09:47:54 +08:00
parent e9f66ca0b2
commit c2bbdd865d

View File

@@ -1,8 +1,11 @@
package com.njcn.msgpush.module.push.service.credential;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import com.njcn.msgpush.framework.common.exception.ServiceException;
import com.njcn.msgpush.framework.common.exception.enums.ServiceErrorCodeRange;
@@ -12,8 +15,10 @@ import com.njcn.msgpush.module.push.dal.dataobject.credential.dto.CredentialReqD
import com.njcn.msgpush.module.push.dal.dataobject.credential.dto.CredentialRespDTO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@@ -28,186 +33,186 @@ import java.util.concurrent.TimeUnit;
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class CredentialServiceImpl implements ICredentialService {
/**
* 凭证加密密钥(生产环境应通过配置中心或环境变量注入)
*/
private String credentialSecretKey = "88888888888888888888888888888888"; // 32 字节
@Autowired
private ISystemSecretService systemSecretService;
private static final long CREDENTIAL_EXPIRE_HOURS = 24L;
private static final long LOCK_WAIT_SECONDS = 3L;
private static final long LOCK_LEASE_SECONDS = 5L;
private static final String SYSTEM_KEY_PREFIX = "credential:";
private static final String LOCK_KEY_PREFIX = "credential:lock:";
private String credentialSecretKey = "88888888888888888888888888888888"; // 32 瀛楄妭
private final ISystemSecretService systemSecretService;
private final StringRedisTemplate stringRedisTemplate;
public CredentialServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
private final RedissonClient redissonClient;
@Override
public CredentialRespDTO generateCredential(CredentialReqDTO reqDTO) {
String systemName = reqDTO.getSystemName();
boolean validatedSecretRes = validateSecret(systemName, reqDTO.getSecretKey());
if (!validatedSecretRes) {
throw new ServiceException(ServiceErrorCodeRange.VALIDATE_SYSTEM_SECRET_FAIL);
}
LocalDateTime expiresTime = LocalDateTime.now().plusHours(CREDENTIAL_EXPIRE_HOURS);
String credential = encryptCredential(systemName, expiresTime);
RLock lock = redissonClient.getLock(formatLockKey(systemName));
boolean locked = false;
try {
// 1. 验证系统密钥
boolean validatedSecretRes = validateSecret(reqDTO.getSystemName(), reqDTO.getSecretKey());
if (!validatedSecretRes) {
throw new ServiceException(ServiceErrorCodeRange.VALIDATE_SYSTEM_SECRET_FAIL);
locked = lock.tryLock(LOCK_WAIT_SECONDS, LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
if (!locked) {
log.warn("[generateCredential][systemName({}) failed to acquire lock within {} seconds]",
systemName, LOCK_WAIT_SECONDS);
throw new ServiceException(ServiceErrorCodeRange.GENERATE_CREDENTIAL_FAIL);
}
// 2. 创建凭证信息
CredentialInfo credentialInfo = new CredentialInfo(
reqDTO.getSystemName(),
LocalDateTime.now().plusHours(24)
);
// 3. 生成凭证 token加密后的 JSON
String token = encryptCredential(credentialInfo);
// 4. 缓存凭证信息到 Redis
cacheCredential(token, credentialInfo);
// 5. 构建响应
return new CredentialRespDTO()
.setCredentialToken(token)
.setSystemName(reqDTO.getSystemName())
.setExpiresTime(credentialInfo.getExpiresTime());
} catch (Exception e) {
deleteCredential(systemName);
CredentialInfo credentialInfo = new CredentialInfo(systemName, expiresTime, credential);
cacheCredential(systemName, credentialInfo);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("[generateCredential][systemName({}) interrupted while waiting for lock]", systemName, e);
throw new ServiceException(ServiceErrorCodeRange.GENERATE_CREDENTIAL_FAIL);
}
}
public CredentialInfo verifyCredential(String token) {
if (StrUtil.isEmpty(token)) {
return null;
}
try {
// 1. 从 Redis 获取凭证信息
CredentialInfo credentialInfo = getCredentialFromRedis(token);
if (credentialInfo == null) {
return null;
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
// 2. 检查是否过期
if (credentialInfo.getExpiresTime().isBefore(LocalDateTime.now())) {
// 删除过期的凭证
deleteCredential(token);
return null;
}
return credentialInfo;
} catch (Exception e) {
log.warn("[verifyCredential] 验证凭证失败token={}", token, e);
return null;
}
return new CredentialRespDTO()
.setCredentialToken(systemName + StrUtil.COLON + credential)
.setSystemName(systemName)
.setExpiresTime(expiresTime);
}
/**
* 验证系统密钥
* 验证凭证
*
* @param systemName 系统名称
* @param secretKey 密钥
* @param credentialToken 凭证 credentialToken
* @return
*/
@Override
public CredentialInfo verifyCredential(String credentialToken) {
if (StrUtil.isEmpty(credentialToken)) {
return null;
}
String[] split = credentialToken.split(StrUtil.COLON);
if (split.length != 2) {
return null;
}
String systemName = split[0];
CredentialInfo credentialInfo = getCredentialFromRedis(systemName);
if (ObjectUtil.isNull(credentialInfo)) {
return null;
}
if (credentialInfo.getExpiresTime().isBefore(LocalDateTime.now()) || !credentialInfo.getCredential().equals(split[1])) {
return null;
}
return credentialInfo;
}
/**
* 校验系统secretKey
*
* @param systemName
* @param secretKey
* @return
*/
private boolean validateSecret(String systemName, String secretKey) {
SystemSecretDO systemSecretDO = systemSecretService.getBySystemName(systemName);
String storedSecret = systemSecretDO.getSecret();
if (!storedSecret.equals(secretKey)) {
return false;
} else {
return true;
}
return systemSecretDO != null && StrUtil.equals(systemSecretDO.getSecret(), secretKey);
}
/**
* 加密凭证信息
* 获取加密后的凭证credential
*
* @param info 凭证信息
* @return 加密后的 token
* @param systemName 系统名称
* @param expiresTime 过期时间
* @return
*/
private String encryptCredential(CredentialInfo info) {
AES aes = SecureUtil.aes(credentialSecretKey.getBytes(StandardCharsets.UTF_8));
String json = JsonUtils.toJsonString(info);
byte[] encrypted = aes.encrypt(json.getBytes(StandardCharsets.UTF_8));
private String encryptCredential(String systemName, LocalDateTime expiresTime) {
// 生成随机 IV
byte[] iv = RandomUtil.randomBytes(16);
// 创建 AES 实例
AES aes = new AES(
Mode.CBC,
Padding.PKCS5Padding,
credentialSecretKey.getBytes(StandardCharsets.UTF_8),
iv
);
String origin = systemName + StrUtil.C_COLON + expiresTime.getNano();
byte[] encrypted = aes.encrypt(origin.getBytes(StandardCharsets.UTF_8));
return Base64.encode(encrypted);
}
/**
* 解密凭证信息
* 缓存系统凭证credential
*
* @param token 凭证 token
* @return 凭证信息
* @param systemName
* @param info
*/
private CredentialInfo decryptCredential(String token) {
try {
AES aes = SecureUtil.aes(credentialSecretKey.getBytes(StandardCharsets.UTF_8));
byte[] decrypted = aes.decrypt(Base64.decode(token));
String json = new String(decrypted, StandardCharsets.UTF_8);
return JsonUtils.parseObject(json, CredentialInfo.class);
} catch (Exception e) {
log.error("[decryptCredential] 解密凭证失败", e);
return null;
}
}
/**
* 缓存凭证信息到 Redis
*
* @param token 凭证 token
* @param info 凭证信息
*/
private void cacheCredential(String token, CredentialInfo info) {
String redisKey = formatRedisKey(token);
private void cacheCredential(String systemName, CredentialInfo info) {
String credentialKey = formatCredentialKey(systemName);
String jsonValue = JsonUtils.toJsonString(info);
// 计算剩余过期时间(秒)
long remainingSeconds = Duration.between(LocalDateTime.now(), info.getExpiresTime()).getSeconds();
if (remainingSeconds > 0) {
stringRedisTemplate.opsForValue().set(redisKey, jsonValue, remainingSeconds, TimeUnit.SECONDS);
stringRedisTemplate.opsForValue().set(credentialKey, jsonValue, remainingSeconds, TimeUnit.SECONDS);
}
}
/**
* 从 Redis 获取凭证信息
* 从redis获取系统凭证信息
*
* @param token 凭证 token
* @return 凭证信息
* @param systemName
* @return
*/
private CredentialInfo getCredentialFromRedis(String token) {
String redisKey = formatRedisKey(token);
String jsonValue = stringRedisTemplate.opsForValue().get(redisKey);
private CredentialInfo getCredentialFromRedis(String systemName) {
String credentialKey = formatCredentialKey(systemName);
String jsonValue = stringRedisTemplate.opsForValue().get(credentialKey);
if (StrUtil.isEmpty(jsonValue)) {
return null;
}
return JsonUtils.parseObject(jsonValue, CredentialInfo.class);
}
/**
* 删除凭证
*
* @param token 凭证 token
*/
private void deleteCredential(String token) {
String redisKey = formatRedisKey(token);
stringRedisTemplate.delete(redisKey);
private void deleteCredential(String systemName) {
String credentialKey = formatCredentialKey(systemName);
String oldToken = stringRedisTemplate.opsForValue().get(credentialKey);
if (StrUtil.isNotEmpty(oldToken)) {
stringRedisTemplate.delete(credentialKey);
}
}
private String formatCredentialKey(String systemName) {
return SYSTEM_KEY_PREFIX + systemName;
}
/**
* 格式化 Redis Key
* 获取锁的key用于针对同一时刻多个请求保证多个请求只对同一个key进行加锁
*
* @param token 凭证 token
* @return Redis Key
* @param systemName
* @return
*/
private String formatRedisKey(String token) {
return "credential:token:" + token;
private String formatLockKey(String systemName) {
return LOCK_KEY_PREFIX + systemName;
}
/**
* 凭证信息内部类
*/
@Data
@AllArgsConstructor
public static class CredentialInfo {
private String systemName;
private LocalDateTime expiresTime;
private String credential;
}
}