系统凭证调整

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