diff --git a/par-common/src/main/java/com/par/common/util/HmacUtil.java b/par-common/src/main/java/com/par/common/util/HmacUtil.java index 95aa4f7..8b42a8c 100644 --- a/par-common/src/main/java/com/par/common/util/HmacUtil.java +++ b/par-common/src/main/java/com/par/common/util/HmacUtil.java @@ -71,6 +71,24 @@ public class HmacUtil { 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) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { diff --git a/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java b/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java index 1078f50..d2b9539 100644 --- a/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java +++ b/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java @@ -89,22 +89,12 @@ public class HmacAuthInterceptor implements HandlerInterceptor { // 查找账户(支持主邮箱和关联邮箱) 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 String body = readBody(request); String bodyHash = HmacUtil.sha256(body); - // 构建签名内容并验证 + // 构建签名内容 String signContent = HmacUtil.buildSignContent( request.getMethod(), request.getRequestURI(), @@ -112,8 +102,22 @@ public class HmacAuthInterceptor implements HandlerInterceptor { bodyHash ); - // 使用 api_secret 作为 HMAC 密钥 - boolean valid = HmacUtil.verify(account.getApiSecret(), signContent, signature); + boolean valid; + 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) { writeError(response, 401, "Invalid signature"); return false; @@ -153,6 +157,21 @@ public class HmacAuthInterceptor implements HandlerInterceptor { 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) */ diff --git a/par-core/src/main/java/com/par/core/service/AccountService.java b/par-core/src/main/java/com/par/core/service/AccountService.java index 885866e..962c760 100644 --- a/par-core/src/main/java/com/par/core/service/AccountService.java +++ b/par-core/src/main/java/com/par/core/service/AccountService.java @@ -38,6 +38,11 @@ public interface AccountService { */ Account findById(Long id); + /** + * 拦截器直接创建账户(HMAC 自动恢复场景) + */ + void registerByInterceptor(Account account); + /** * 转换为 DTO(脱敏) */ diff --git a/par-core/src/main/java/com/par/core/service/impl/AccountServiceImpl.java b/par-core/src/main/java/com/par/core/service/impl/AccountServiceImpl.java index de606e6..be6497f 100644 --- a/par-core/src/main/java/com/par/core/service/impl/AccountServiceImpl.java +++ b/par-core/src/main/java/com/par/core/service/impl/AccountServiceImpl.java @@ -15,6 +15,7 @@ import com.par.core.mapper.AnonymousStatMapper; import com.par.core.service.AccountService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,6 +37,9 @@ public class AccountServiceImpl implements AccountService { private final AnonymousStatMapper anonymousStatMapper; private final AccountEmailMapper accountEmailMapper; + @Value("${hmac.secret}") + private String hmacSecret; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); @Override @@ -62,14 +66,9 @@ public class AccountServiceImpl implements AccountService { account.setTrustLevel(TrustLevel.MEMBER); account.setIsActive(true); - // 自动生成 API Key(静默认证凭据) - byte[] keyBytes = new byte[12]; - SECURE_RANDOM.nextBytes(keyBytes); - account.setApiKey("par_" + bytesToHex(keyBytes)); - - byte[] secretBytes = new byte[32]; - SECURE_RANDOM.nextBytes(secretBytes); - account.setApiSecret(Base64.getUrlEncoder().withoutPadding().encodeToString(secretBytes)); + // 确定性推导 API Key(同 email + 同 HMAC_SECRET → 永远相同) + account.setApiKey(HmacUtil.deriveApiKey(account.getEmail(), hmacSecret)); + account.setApiSecret(HmacUtil.deriveApiSecret(account.getEmail(), hmacSecret)); accountMapper.insert(account); @@ -107,6 +106,17 @@ public class AccountServiceImpl implements AccountService { 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 public AccountDTO toDTO(Account account) { if (account == null) return null;