feat: 确定性密钥推导 + HMAC 自动恢复账户
- HmacUtil.deriveApiSecret/deriveApiKey: email+HMAC_SECRET 确定性推导
- 注册时用推导值替代随机生成(同 email 永远同 key)
- HMAC 拦截器:未知邮箱但签名匹配 → 自动创建账户
- 重装后 mediabot 直接发 HMAC 请求即可恢复,无需重新注册
核心: apiSecret = HMAC(email + ':par-api-secret', HMAC_SECRET)
同一 email + 同一 HMAC_SECRET → 永久不变的凭据
This commit is contained in:
@@ -71,6 +71,24 @@ public class HmacUtil {
|
|||||||
return hash.substring(0, 16);
|
return hash.substring(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 email + 服务端密钥确定性推导 api_secret
|
||||||
|
* 同一 email + 同一 HMAC_SECRET → 永远返回相同值
|
||||||
|
*/
|
||||||
|
public static String deriveApiSecret(String email, String hmacSecret) {
|
||||||
|
String input = email.toLowerCase().trim() + ":par-api-secret";
|
||||||
|
return sign(hmacSecret, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 email + 服务端密钥确定性推导 api_key
|
||||||
|
*/
|
||||||
|
public static String deriveApiKey(String email, String hmacSecret) {
|
||||||
|
String input = email.toLowerCase().trim() + ":par-api-key";
|
||||||
|
String hash = sign(hmacSecret, input);
|
||||||
|
return "par_" + sha256(hash).substring(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
private static String bytesToHex(byte[] bytes) {
|
private static String bytesToHex(byte[] bytes) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (byte b : bytes) {
|
for (byte b : bytes) {
|
||||||
|
|||||||
@@ -89,22 +89,12 @@ public class HmacAuthInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
// 查找账户(支持主邮箱和关联邮箱)
|
// 查找账户(支持主邮箱和关联邮箱)
|
||||||
Account account = accountService.findByAnyEmail(email);
|
Account account = accountService.findByAnyEmail(email);
|
||||||
if (account == null || !Boolean.TRUE.equals(account.getIsActive())) {
|
|
||||||
writeError(response, 401, "Invalid credentials");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有 API Secret
|
|
||||||
if (account.getApiSecret() == null || account.getApiSecret().isBlank()) {
|
|
||||||
writeError(response, 401, "API credentials not configured for this account");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算 body hash
|
// 计算 body hash
|
||||||
String body = readBody(request);
|
String body = readBody(request);
|
||||||
String bodyHash = HmacUtil.sha256(body);
|
String bodyHash = HmacUtil.sha256(body);
|
||||||
|
|
||||||
// 构建签名内容并验证
|
// 构建签名内容
|
||||||
String signContent = HmacUtil.buildSignContent(
|
String signContent = HmacUtil.buildSignContent(
|
||||||
request.getMethod(),
|
request.getMethod(),
|
||||||
request.getRequestURI(),
|
request.getRequestURI(),
|
||||||
@@ -112,8 +102,22 @@ public class HmacAuthInterceptor implements HandlerInterceptor {
|
|||||||
bodyHash
|
bodyHash
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用 api_secret 作为 HMAC 密钥
|
boolean valid;
|
||||||
boolean valid = HmacUtil.verify(account.getApiSecret(), signContent, signature);
|
if (account != null && Boolean.TRUE.equals(account.getIsActive())) {
|
||||||
|
// 已有账户:用 api_secret 验签
|
||||||
|
if (account.getApiSecret() == null || account.getApiSecret().isBlank()) {
|
||||||
|
writeError(response, 401, "API credentials not configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
valid = HmacUtil.verify(account.getApiSecret(), signContent, signature);
|
||||||
|
} else {
|
||||||
|
// 未知邮箱:尝试用确定性推导的 secret 验签,匹配则自动创建账户
|
||||||
|
String derivedSecret = HmacUtil.deriveApiSecret(email, hmacSecret);
|
||||||
|
valid = HmacUtil.verify(derivedSecret, signContent, signature);
|
||||||
|
if (valid) {
|
||||||
|
account = autoCreateAccount(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
writeError(response, 401, "Invalid signature");
|
writeError(response, 401, "Invalid signature");
|
||||||
return false;
|
return false;
|
||||||
@@ -153,6 +157,21 @@ public class HmacAuthInterceptor implements HandlerInterceptor {
|
|||||||
return Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
|
return Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动创建账户(DB 重置后首次 HMAC 请求触发恢复)
|
||||||
|
*/
|
||||||
|
private Account autoCreateAccount(String email) {
|
||||||
|
var account = new Account();
|
||||||
|
account.setEmail(email.toLowerCase().trim());
|
||||||
|
account.setApiKey(HmacUtil.deriveApiKey(email, hmacSecret));
|
||||||
|
account.setApiSecret(HmacUtil.deriveApiSecret(email, hmacSecret));
|
||||||
|
account.setTrustLevel(com.par.core.enums.TrustLevel.MEMBER);
|
||||||
|
account.setIsActive(true);
|
||||||
|
accountService.registerByInterceptor(account);
|
||||||
|
log.info("Auto-created account via HMAC: email={}", email);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验 Bearer token,返回 email(失败返回 null)
|
* 校验 Bearer token,返回 email(失败返回 null)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ public interface AccountService {
|
|||||||
*/
|
*/
|
||||||
Account findById(Long id);
|
Account findById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截器直接创建账户(HMAC 自动恢复场景)
|
||||||
|
*/
|
||||||
|
void registerByInterceptor(Account account);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换为 DTO(脱敏)
|
* 转换为 DTO(脱敏)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.par.core.mapper.AnonymousStatMapper;
|
|||||||
import com.par.core.service.AccountService;
|
import com.par.core.service.AccountService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ public class AccountServiceImpl implements AccountService {
|
|||||||
private final AnonymousStatMapper anonymousStatMapper;
|
private final AnonymousStatMapper anonymousStatMapper;
|
||||||
private final AccountEmailMapper accountEmailMapper;
|
private final AccountEmailMapper accountEmailMapper;
|
||||||
|
|
||||||
|
@Value("${hmac.secret}")
|
||||||
|
private String hmacSecret;
|
||||||
|
|
||||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -62,14 +66,9 @@ public class AccountServiceImpl implements AccountService {
|
|||||||
account.setTrustLevel(TrustLevel.MEMBER);
|
account.setTrustLevel(TrustLevel.MEMBER);
|
||||||
account.setIsActive(true);
|
account.setIsActive(true);
|
||||||
|
|
||||||
// 自动生成 API Key(静默认证凭据)
|
// 确定性推导 API Key(同 email + 同 HMAC_SECRET → 永远相同)
|
||||||
byte[] keyBytes = new byte[12];
|
account.setApiKey(HmacUtil.deriveApiKey(account.getEmail(), hmacSecret));
|
||||||
SECURE_RANDOM.nextBytes(keyBytes);
|
account.setApiSecret(HmacUtil.deriveApiSecret(account.getEmail(), hmacSecret));
|
||||||
account.setApiKey("par_" + bytesToHex(keyBytes));
|
|
||||||
|
|
||||||
byte[] secretBytes = new byte[32];
|
|
||||||
SECURE_RANDOM.nextBytes(secretBytes);
|
|
||||||
account.setApiSecret(Base64.getUrlEncoder().withoutPadding().encodeToString(secretBytes));
|
|
||||||
|
|
||||||
accountMapper.insert(account);
|
accountMapper.insert(account);
|
||||||
|
|
||||||
@@ -107,6 +106,17 @@ public class AccountServiceImpl implements AccountService {
|
|||||||
return accountMapper.selectById(id);
|
return accountMapper.selectById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void registerByInterceptor(Account account) {
|
||||||
|
// 自动生成随机密码
|
||||||
|
byte[] pwdBytes = new byte[16];
|
||||||
|
SECURE_RANDOM.nextBytes(pwdBytes);
|
||||||
|
account.setPasswordHash(PasswordUtil.encode(
|
||||||
|
Base64.getUrlEncoder().withoutPadding().encodeToString(pwdBytes)));
|
||||||
|
accountMapper.insert(account);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountDTO toDTO(Account account) {
|
public AccountDTO toDTO(Account account) {
|
||||||
if (account == null) return null;
|
if (account == null) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user