diff --git a/par-api/src/main/java/com/par/api/controller/AuthController.java b/par-api/src/main/java/com/par/api/controller/AuthController.java index c97aadb..aa00b49 100644 --- a/par-api/src/main/java/com/par/api/controller/AuthController.java +++ b/par-api/src/main/java/com/par/api/controller/AuthController.java @@ -5,6 +5,7 @@ import com.par.core.dto.ApiResponse; import com.par.core.dto.LoginRequest; 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.validation.Valid; import lombok.RequiredArgsConstructor; @@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.*; public class AuthController { private final AccountService accountService; + private final HmacAuthInterceptor hmacAuthInterceptor; /** * 用户注册 @@ -31,11 +33,13 @@ public class AuthController { } /** - * 用户登录(仅验证密码,返回账户信息) + * 用户登录,返回 Bearer token(有效期 24 小时) */ @PostMapping("/login") public ApiResponse login(@Valid @RequestBody LoginRequest request) { Account account = accountService.login(request); - return ApiResponse.success(accountService.toDTO(account)); + AccountDTO dto = accountService.toDTO(account); + dto.setToken(hmacAuthInterceptor.generateToken(account.getEmail())); + return ApiResponse.success(dto); } } diff --git a/par-core/src/main/java/com/par/core/dto/AccountDTO.java b/par-core/src/main/java/com/par/core/dto/AccountDTO.java index 63710f6..85a2c7b 100644 --- a/par-core/src/main/java/com/par/core/dto/AccountDTO.java +++ b/par-core/src/main/java/com/par/core/dto/AccountDTO.java @@ -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; } diff --git a/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java b/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java index 7c8eaa8..f6e1ec0 100644 --- a/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java +++ b/par-core/src/main/java/com/par/core/interceptor/HmacAuthInterceptor.java @@ -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 鉴权拦截器 + *

+ * 支持两种认证方式(按优先级): + * 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; + } } diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 7a31612..6aa1ce3 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -40,7 +40,7 @@ const handleLogin = async () => { try { const res = await api.auth.login({ email: form.email, password: form.password }) if (res.code === 200) { - localStorage.setItem('par_token', res.data.email) + localStorage.setItem('par_token', res.data.token || res.data.email) ElMessage.success('登录成功') router.push('/') } else {