feat: Mediabot PSK 签名恢复 + 注册自动提权

- par.mediabot.psk 配置项(环境变量 PAR_MEDIABOT_PSK)
- HmacUtil.verifyMediabotPsk() PSK 签名验证
- POST /auth/recover: PSK 签名 → 返回/重建 apiKey/apiSecret → 自动 TRUSTED
- POST /auth/register: 携带 PSK 头 → 自动提升为 TRUSTED
- AccountService 新增 updateAccount

mediabot 端只需:
  const PSK = '内置密钥'
  sig = HMAC('email:timestamp', PSK)
  POST /auth/recover {email} + X-Medibot-* 头 → 拿回凭据
This commit is contained in:
mediabot-pt
2026-06-29 16:44:59 +08:00
parent aed8035995
commit 11d0840e42
5 changed files with 110 additions and 7 deletions

View File

@@ -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<Map<String, Object>> register(@Valid @RequestBody RegisterRequest request) {
public ApiResponse<Map<String, Object>> 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<String, Object> result = new java.util.LinkedHashMap<>();
Map<String, Object> 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<Map<String, Object>> recover(
@RequestBody Map<String, String> 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<String, Object> 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;
}
}
}

View File

@@ -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:

View File

@@ -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
*/

View File

@@ -43,6 +43,11 @@ public interface AccountService {
*/
void registerByInterceptor(Account account);
/**
* 更新账户
*/
void updateAccount(Account account);
/**
* 转换为 DTO(脱敏)
*/

View File

@@ -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;