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;
|
package com.par.api.controller;
|
||||||
|
|
||||||
|
import com.par.common.util.HmacUtil;
|
||||||
import com.par.core.dto.AccountDTO;
|
import com.par.core.dto.AccountDTO;
|
||||||
import com.par.core.dto.ApiResponse;
|
import com.par.core.dto.ApiResponse;
|
||||||
import com.par.core.dto.LoginRequest;
|
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.entity.Account;
|
||||||
import com.par.core.interceptor.HmacAuthInterceptor;
|
import com.par.core.interceptor.HmacAuthInterceptor;
|
||||||
import com.par.core.service.AccountService;
|
import com.par.core.service.AccountService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
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 org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证控制器
|
* 认证控制器
|
||||||
* 注册、登录(读取公开,无需鉴权)
|
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final AccountService accountService;
|
private final AccountService accountService;
|
||||||
private final HmacAuthInterceptor hmacAuthInterceptor;
|
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")
|
@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);
|
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);
|
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("account", dto);
|
||||||
result.put("apiKey", account.getApiKey());
|
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);
|
return ApiResponse.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +114,19 @@ public class AuthController {
|
|||||||
dto.setApiKey(account.getApiKey());
|
dto.setApiKey(account.getApiKey());
|
||||||
return ApiResponse.success(dto);
|
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:
|
par:
|
||||||
static:
|
static:
|
||||||
path: ${PAR_STATIC_PATH:./static/configs/}
|
path: ${PAR_STATIC_PATH:./static/configs/}
|
||||||
|
mediabot:
|
||||||
|
psk: ${PAR_MEDIABOT_PSK:}
|
||||||
admin:
|
admin:
|
||||||
path: ${PAR_ADMIN_PATH:./static/admin/}
|
path: ${PAR_ADMIN_PATH:./static/admin/}
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
@@ -80,6 +80,18 @@ public class HmacUtil {
|
|||||||
return sign(hmacSecret, input);
|
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
|
* 从 email + 服务端密钥确定性推导 api_key
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ public interface AccountService {
|
|||||||
*/
|
*/
|
||||||
void registerByInterceptor(Account account);
|
void registerByInterceptor(Account account);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新账户
|
||||||
|
*/
|
||||||
|
void updateAccount(Account account);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换为 DTO(脱敏)
|
* 转换为 DTO(脱敏)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ public class AccountServiceImpl implements AccountService {
|
|||||||
accountMapper.insert(account);
|
accountMapper.insert(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAccount(Account account) {
|
||||||
|
accountMapper.updateById(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