diff --git a/par-api/src/main/java/com/par/api/controller/AuthController.java b/par-api/src/main/java/com/par/api/controller/AuthController.java index cc61313..ee89032 100644 --- a/par-api/src/main/java/com/par/api/controller/AuthController.java +++ b/par-api/src/main/java/com/par/api/controller/AuthController.java @@ -1,5 +1,6 @@ package com.par.api.controller; +import com.par.common.util.HmacUtil; import com.par.core.dto.AccountDTO; import com.par.core.dto.ApiResponse; import com.par.core.dto.LoginRequest; @@ -7,35 +8,98 @@ import com.par.core.dto.RegisterRequest; import com.par.core.entity.Account; import com.par.core.interceptor.HmacAuthInterceptor; import com.par.core.service.AccountService; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; +import java.util.LinkedHashMap; import java.util.Map; /** * 认证控制器 - * 注册、登录(读取公开,无需鉴权) */ +@Slf4j @RestController @RequestMapping("/api/v1/auth") -@RequiredArgsConstructor public class AuthController { private final AccountService accountService; private final HmacAuthInterceptor hmacAuthInterceptor; + @Value("${par.mediabot.psk:}") + private String mediabotPsk; + + public AuthController(AccountService accountService, HmacAuthInterceptor hmacAuthInterceptor) { + this.accountService = accountService; + this.hmacAuthInterceptor = hmacAuthInterceptor; + } + /** - * 用户注册(自动生成 API Key,apiSecret 仅此时返回一次) + * 用户注册(自动生成 API Key) + * 携带 PSK 签名头 → 自动提权为 TRUSTED */ @PostMapping("/register") - public ApiResponse> register(@Valid @RequestBody RegisterRequest request) { + public ApiResponse> register( + @Valid @RequestBody RegisterRequest request, + HttpServletRequest httpRequest) { Account account = accountService.register(request); + + // PSK 签名验证通过 → 自动提权 + if (verifyMediabotPsk(httpRequest, request.getEmail())) { + account.setTrustLevel(com.par.core.enums.TrustLevel.TRUSTED); + accountService.updateAccount(account); + log.info("Account auto-trusted via PSK: email={}", account.getEmail()); + } + AccountDTO dto = accountService.toDTO(account); - Map result = new java.util.LinkedHashMap<>(); + Map result = new LinkedHashMap<>(); result.put("account", dto); result.put("apiKey", account.getApiKey()); - result.put("apiSecret", account.getApiSecret()); // 仅注册时返回,请立即保存 + result.put("apiSecret", account.getApiSecret()); + return ApiResponse.success(result); + } + + /** + * 账户恢复(DB 重置后无需管理员介入) + * PSK 签名验证通过 → 返回/重建凭据 + */ + @PostMapping("/recover") + public ApiResponse> recover( + @RequestBody Map body, + HttpServletRequest httpRequest) { + + String email = body.get("email"); + if (email == null || email.isBlank()) { + return ApiResponse.error(400, "email is required"); + } + email = email.toLowerCase().trim(); + + if (!verifyMediabotPsk(httpRequest, email)) { + return ApiResponse.error(403, "Invalid PSK signature"); + } + + // 查找或创建账户 + Account account = accountService.findByAnyEmail(email); + if (account == null) { + // 自动创建 + 提权 + RegisterRequest req = new RegisterRequest(); + req.setEmail(email); + account = accountService.register(req); + } + // 确保是 TRUSTED + if (account.getTrustLevel().ordinal() < com.par.core.enums.TrustLevel.TRUSTED.ordinal()) { + account.setTrustLevel(com.par.core.enums.TrustLevel.TRUSTED); + accountService.updateAccount(account); + } + + AccountDTO dto = accountService.toDTO(account); + Map result = new LinkedHashMap<>(); + result.put("account", dto); + result.put("apiKey", account.getApiKey()); + result.put("apiSecret", account.getApiSecret()); + log.info("Account recovered via PSK: email={}", email); return ApiResponse.success(result); } @@ -50,4 +114,19 @@ public class AuthController { dto.setApiKey(account.getApiKey()); return ApiResponse.success(dto); } + + /** + * 验证 X-Medibot-Timestamp + X-Medibot-Signature + */ + private boolean verifyMediabotPsk(HttpServletRequest request, String email) { + String ts = request.getHeader("X-Medibot-Timestamp"); + String sig = request.getHeader("X-Medibot-Signature"); + if (ts == null || sig == null) return false; + try { + long timestamp = Long.parseLong(ts); + return HmacUtil.verifyMediabotPsk(email, timestamp, sig, mediabotPsk); + } catch (NumberFormatException e) { + return false; + } + } } diff --git a/par-api/src/main/resources/application.yml b/par-api/src/main/resources/application.yml index 3344e53..b11ba99 100644 --- a/par-api/src/main/resources/application.yml +++ b/par-api/src/main/resources/application.yml @@ -46,6 +46,8 @@ mybatis-plus: par: static: path: ${PAR_STATIC_PATH:./static/configs/} + mediabot: + psk: ${PAR_MEDIABOT_PSK:} admin: path: ${PAR_ADMIN_PATH:./static/admin/} auth: 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 8b42a8c..1c52682 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 @@ -80,6 +80,18 @@ public class HmacUtil { return sign(hmacSecret, input); } + /** + * 验证 medibot PSK 签名(用于账户恢复 / 自动提权) + * signContent = email + ":" + timestamp + */ + public static boolean verifyMediabotPsk(String email, long timestamp, String signature, String psk) { + if (psk == null || psk.isBlank()) return false; + long now = System.currentTimeMillis() / 1000; + if (Math.abs(now - timestamp) > 300) return false; // 5 分钟窗口 + String payload = email.toLowerCase().trim() + ":" + timestamp; + return verify(psk, payload, signature); + } + /** * 从 email + 服务端密钥确定性推导 api_key */ 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 962c760..fc66cb3 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 @@ -43,6 +43,11 @@ public interface AccountService { */ void registerByInterceptor(Account account); + /** + * 更新账户 + */ + void updateAccount(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 be6497f..95576ea 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 @@ -117,6 +117,11 @@ public class AccountServiceImpl implements AccountService { accountMapper.insert(account); } + @Override + public void updateAccount(Account account) { + accountMapper.updateById(account); + } + @Override public AccountDTO toDTO(Account account) { if (account == null) return null;