From c2bbdd865d20e5ece3657b768626fe423083e874 Mon Sep 17 00:00:00 2001 From: caozehui <2427765068@qq.com> Date: Wed, 13 May 2026 09:47:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=87=AD=E8=AF=81=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../credential/CredentialServiceImpl.java | 253 +++++++++--------- 1 file changed, 129 insertions(+), 124 deletions(-) diff --git a/msgpush-module-push/msgpush-module-push-server/src/main/java/com/njcn/msgpush/module/push/service/credential/CredentialServiceImpl.java b/msgpush-module-push/msgpush-module-push-server/src/main/java/com/njcn/msgpush/module/push/service/credential/CredentialServiceImpl.java index 74bcde5..71df512 100644 --- a/msgpush-module-push/msgpush-module-push-server/src/main/java/com/njcn/msgpush/module/push/service/credential/CredentialServiceImpl.java +++ b/msgpush-module-push/msgpush-module-push-server/src/main/java/com/njcn/msgpush/module/push/service/credential/CredentialServiceImpl.java @@ -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; } }