feat: Bearer Token 认证,修复 Web 管理后台 401 问题

- HmacAuthInterceptor 增加 Bearer Token 优先认证(Web 管理后台)
- AuthController.login 使用 hmac.secret 签发 24h 有效 token
- AccountDTO 新增 token 字段(@JsonInclude NON_NULL)
- 前端 Login.vue 存储登录返回的 token
- 原有 HMAC 签名认证不受影响(外部 API 客户端继续可用)
This commit is contained in:
mediabot-pt
2026-06-29 14:20:29 +08:00
parent d2c226c25f
commit c15016d57c
4 changed files with 80 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
package com.par.core.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.par.core.enums.TrustLevel;
import lombok.Data;
@@ -9,6 +10,7 @@ import java.time.LocalDateTime;
* 账户信息 DTO(响应用,不包含敏感字段)
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AccountDTO {
private Long id;
@@ -17,4 +19,7 @@ public class AccountDTO {
private TrustLevel trustLevel;
private Boolean isActive;
private LocalDateTime createdAt;
/** 登录 token(仅登录接口返回) */
private String token;
}

View File

@@ -5,35 +5,62 @@ import com.par.core.entity.Account;
import com.par.core.service.AccountService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.stream.Collectors;
/**
* HMAC 请求签名鉴权拦截器
* 拦截需要写入权限的 API 端点
* API 鉴权拦截器
* <p>
* 支持两种认证方式(按优先级):
* 1. Bearer Token — Web 管理后台使用,登录后由服务端签发
* 2. HMAC 请求签名 — 外部 API 客户端使用,以 X-Signature/X-Timestamp/X-Email 头携带
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HmacAuthInterceptor implements HandlerInterceptor {
private final AccountService accountService;
@Value("${hmac.secret}")
private String hmacSecret;
private static final String HEADER_SIGNATURE = "X-Signature";
private static final String HEADER_TIMESTAMP = "X-Timestamp";
private static final String HEADER_EMAIL = "X-Email";
private static final String HEADER_AUTHORIZATION = "Authorization";
/** Token 有效期(小时) */
private static final long TOKEN_EXPIRY_HOURS = 24;
/** HMAC 时间窗口(秒) */
private static final long HMAC_WINDOW_SECONDS = 60;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ======== 方式 1:Bearer Token 认证(Web 管理后台) ========
String authHeader = request.getHeader(HEADER_AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
String email = verifyBearerToken(token);
if (email != null) {
Account account = accountService.findByEmail(email);
if (account != null && Boolean.TRUE.equals(account.getIsActive())) {
request.setAttribute("accountId", account.getId());
request.setAttribute("trustLevel", account.getTrustLevel());
return true;
}
}
writeError(response, 401, "Invalid or expired token");
return false;
}
// ======== 方式 2:HMAC 签名认证(外部 API 客户端) ========
String signature = request.getHeader(HEADER_SIGNATURE);
String timestampStr = request.getHeader(HEADER_TIMESTAMP);
String email = request.getHeader(HEADER_EMAIL);
@@ -105,4 +132,41 @@ public class HmacAuthInterceptor implements HandlerInterceptor {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.format("{\"code\":%d,\"message\":\"%s\",\"data\":null}", status, message));
}
/**
* 生成登录 token(供 AuthController 调用)
* 格式: Base64(email:expiryTimestamp:HmacSHA256(email:expiry, hmacSecret))
*/
public String generateToken(String email) {
long expiry = System.currentTimeMillis() / 1000 + TOKEN_EXPIRY_HOURS * 3600;
String payload = email + ":" + expiry;
String sig = HmacUtil.sign(hmacSecret, payload);
String token = payload + ":" + sig;
return Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
}
/**
* 校验 Bearer token,返回 email(失败返回 null)
*/
private String verifyBearerToken(String token) {
try {
String decoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
String[] parts = decoded.split(":", 3);
if (parts.length != 3) return null;
String email = parts[0];
long expiry = Long.parseLong(parts[1]);
String sig = parts[2];
if (System.currentTimeMillis() / 1000 > expiry) return null;
String payload = email + ":" + expiry;
if (HmacUtil.verify(hmacSecret, payload, sig)) {
return email;
}
} catch (Exception e) {
log.debug("Bearer token validation failed: {}", e.getMessage());
}
return null;
}
}