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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user