feat: API Key 静默认证 + 多邮箱支持

核心改动:
- V3 迁移: accounts 加 api_key/api_secret + account_emails 多邮箱关联表
- HMAC 验签改用 api_secret 替代密码哈希(改密码不影响集成)
- findByAnyEmail 支持主邮箱 + 关联邮箱查找
- AdminController: API Key 生成/重置 + 邮箱绑定/解绑/列表
- 前端 Users.vue: API Key 列 + 生成按钮 + Secret 复制弹窗
- 现有邮箱自动迁移到 account_emails 表

认证流程:
  旧: X-Email → account.email → passwordHash 作 HMAC key
  新: X-Email → account_emails ∪ account.email → api_secret 作 HMAC key
This commit is contained in:
mediabot-pt
2026-06-29 16:30:53 +08:00
parent c4877f983a
commit 2d91552ea4
11 changed files with 349 additions and 13 deletions

View File

@@ -18,6 +18,8 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.List;
/**
* 管理员控制器
@@ -153,6 +155,72 @@ public class AdminController {
return ApiResponse.success();
}
/**
* 生成/重置 API Key
*/
@PostMapping("/accounts/{accountId}/api-key")
public ApiResponse<Map<String, String>> generateApiKey(
@PathVariable Long accountId,
HttpServletRequest request) {
checkAdmin(request);
var account = accountMapper.selectById(accountId);
if (account == null) {
return ApiResponse.error(404, "Account not found");
}
String[] keys = accountService.generateApiCredentials(accountId);
Map<String, String> result = new java.util.LinkedHashMap<>();
result.put("apiKey", keys[0]);
result.put("apiSecret", keys[1]);
// secret 只在生成时返回一次,前端需要立即保存
log.info("API key generated for account: id={}", accountId);
return ApiResponse.success(result);
}
/**
* 绑定额外邮箱
*/
@PostMapping("/accounts/{accountId}/emails")
public ApiResponse<Void> bindEmail(
@PathVariable Long accountId,
@RequestBody Map<String, String> payload,
HttpServletRequest request) {
checkAdmin(request);
String email = payload.get("email");
if (email == null || email.isBlank()) {
return ApiResponse.error(400, "email is required");
}
accountService.bindEmail(accountId, email);
log.info("Email bound to account: accountId={}, email={}", accountId, email);
return ApiResponse.success();
}
/**
* 解绑邮箱
*/
@DeleteMapping("/accounts/{accountId}/emails/{emailId}")
public ApiResponse<Void> unbindEmail(
@PathVariable Long accountId,
@PathVariable Long emailId,
HttpServletRequest request) {
checkAdmin(request);
accountService.unbindEmail(emailId);
log.info("Email unbound: accountId={}, emailId={}", accountId, emailId);
return ApiResponse.success();
}
/**
* 获取账户的绑定邮箱列表
*/
@GetMapping("/accounts/{accountId}/emails")
public ApiResponse<List<String>> listEmails(
@PathVariable Long accountId,
HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(accountService.listEmails(accountId));
}
private void checkAdmin(HttpServletRequest request) {
TrustLevel level = (TrustLevel) request.getAttribute("trustLevel");
if (level == null || level != TrustLevel.ADMIN) {