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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -43,6 +43,11 @@ public interface AccountService {
|
||||
*/
|
||||
void registerByInterceptor(Account account);
|
||||
|
||||
/**
|
||||
* 更新账户
|
||||
*/
|
||||
void updateAccount(Account account);
|
||||
|
||||
/**
|
||||
* 转换为 DTO(脱敏)
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user