package com.yupi.springbootinit.component; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.symmetric.AES; import com.yupi.springbootinit.common.ErrorCode; import com.yupi.springbootinit.config.FeatureTicketProperties; import com.yupi.springbootinit.exception.BusinessException; import com.yupi.springbootinit.model.dto.ticket.FeatureTicketPayload; import com.yupi.springbootinit.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.nio.charset.StandardCharsets; import java.util.Base64; /** * Feature Ticket 认证组件 * 负责 Ticket 的生成、加密、解密和校验 * * @author ziin */ @Component public class FeatureAuthComponent { private static final Logger log = LoggerFactory.getLogger(FeatureAuthComponent.class); @Resource private FeatureTicketProperties ticketProperties; private AES aes; @PostConstruct public void init() { // 使用配置的 AES 密钥初始化加密器 byte[] keyBytes = ticketProperties.getAesKey().getBytes(StandardCharsets.UTF_8); // 确保密钥长度为 16 字节(AES-128) if (keyBytes.length < 16) { byte[] paddedKey = new byte[16]; System.arraycopy(keyBytes, 0, paddedKey, 0, keyBytes.length); keyBytes = paddedKey; } else if (keyBytes.length > 16 && keyBytes.length < 24) { byte[] truncatedKey = new byte[16]; System.arraycopy(keyBytes, 0, truncatedKey, 0, 16); keyBytes = truncatedKey; } else if (keyBytes.length > 24 && keyBytes.length < 32) { byte[] truncatedKey = new byte[24]; System.arraycopy(keyBytes, 0, truncatedKey, 0, 24); keyBytes = truncatedKey; } else if (keyBytes.length > 32) { byte[] truncatedKey = new byte[32]; System.arraycopy(keyBytes, 0, truncatedKey, 0, 32); keyBytes = truncatedKey; } this.aes = SecureUtil.aes(keyBytes); log.info("FeatureAuthComponent 初始化完成,AES 密钥长度: {} 字节", keyBytes.length); } /** * 生成 Feature Ticket * * @param tenantId 租户 ID * @param userId 用户 ID * @param machineId 设备 ID * @param featureCode 功能代码 * @param expireSeconds 有效期(秒) * @return 加密后的 Base64 字符串 */ public String generateTicket(Long tenantId, Long userId, String machineId, String featureCode, Long expireSeconds) { if (tenantId == null || userId == null || StrUtil.isBlank(machineId) || StrUtil.isBlank(featureCode)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成 Ticket 参数不完整"); } // 计算过期时间 long expireTime = expireSeconds != null ? expireSeconds : ticketProperties.getDefaultExpireSeconds(); long expiryTimestamp = System.currentTimeMillis() + (expireTime * 1000); // 构建载荷 FeatureTicketPayload payload = FeatureTicketPayload.builder() .tenantId(tenantId) .userId(userId) .machineId(machineId) .featureCode(featureCode) .expiryTimestamp(expiryTimestamp) .build(); // 序列化为 JSON String jsonPayload = JsonUtils.toJsonString(payload); // AES 加密 byte[] encryptedBytes = aes.encrypt(jsonPayload.getBytes(StandardCharsets.UTF_8)); // Base64 编码(URL 安全) return Base64.getUrlEncoder().withoutPadding().encodeToString(encryptedBytes); } /** * 解密 Ticket * * @param encryptedTicket 加密的 Ticket 字符串 * @return 解密后的载荷 */ public FeatureTicketPayload decryptTicket(String encryptedTicket) { if (StrUtil.isBlank(encryptedTicket)) { throw new BusinessException(ErrorCode.TICKET_INVALID, "Ticket 不能为空"); } try { // Base64 解码 byte[] encryptedBytes = Base64.getUrlDecoder().decode(encryptedTicket); // AES 解密 byte[] decryptedBytes = aes.decrypt(encryptedBytes); String jsonPayload = new String(decryptedBytes, StandardCharsets.UTF_8); // 反序列化 return JsonUtils.parseObject(jsonPayload, FeatureTicketPayload.class); } catch (Exception e) { log.error("Ticket 解密失败: {}", e.getMessage()); throw new BusinessException(ErrorCode.TICKET_INVALID, "Ticket 无效或已损坏"); } } /** * 校验 Ticket * * @param encryptedTicket 加密的 Ticket * @param requestMachineId 请求中的设备 ID * @param requiredFeatureCode 需要的功能代码(可选,为 null 时不校验) * @return 校验通过的载荷 */ public FeatureTicketPayload validateTicket(String encryptedTicket, String requestMachineId, String requiredFeatureCode) { // 解密 Ticket FeatureTicketPayload payload = decryptTicket(encryptedTicket); // 校验过期时间 if (payload.getExpiryTimestamp() == null || System.currentTimeMillis() > payload.getExpiryTimestamp()) { throw new BusinessException(ErrorCode.TICKET_EXPIRED, "Ticket 已过期"); } // 校验设备 ID if (StrUtil.isBlank(requestMachineId)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求中缺少设备 ID"); } if (!requestMachineId.equals(payload.getMachineId())) { log.warn("设备 ID 不匹配,请求: {}, Ticket: {}", requestMachineId, payload.getMachineId()); throw new BusinessException(ErrorCode.TICKET_MACHINE_MISMATCH, "设备 ID 不匹配"); } // 校验功能代码(如果指定) if (StrUtil.isNotBlank(requiredFeatureCode) && !requiredFeatureCode.equals(payload.getFeatureCode())) { log.warn("功能代码不匹配,需要: {}, Ticket: {}", requiredFeatureCode, payload.getFeatureCode()); throw new BusinessException(ErrorCode.TICKET_FEATURE_MISMATCH, "功能代码不匹配"); } return payload; } /** * 简化校验(仅校验有效性和设备 ID) * * @param encryptedTicket 加密的 Ticket * @param requestMachineId 请求中的设备 ID * @return 校验通过的载荷 */ public FeatureTicketPayload validateTicket(String encryptedTicket, String requestMachineId) { return validateTicket(encryptedTicket, requestMachineId, null); } }