Files
tkcrawl-client/src/main/java/com/yupi/springbootinit/component/FeatureAuthComponent.java

175 lines
6.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}