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