系统凭证调整
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user