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

175 lines
6.7 KiB
Java
Raw Normal View History

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);
}
}