fix: v6 code review P0 auth/IDOR fixes + frontend regression patches
Backend fixes: - auth_handler: P0 认证逻辑修复 - ratelimit: 限速中间件增强 + 新增单元测试 - auth_service: 认证服务逻辑完善 + 新增测试 - server: server 配置增强 + 新增测试 - handler_test: 新增 handler 层集成测试 - auth_bootstrap_test: bootstrap 路径测试 Frontend patches: - LoginPage/RegisterPage: CSRF + 表单交互修复 - BootstrapAdminPage: 引导流程修复 - DevicesPage: 设备管理页修复 - auth/social-accounts/users/webhooks services: 类型修正 - csrf.ts: CSRF token 处理修正 - E2E 脚本: CDP smoke + auth e2e 增强 Docs: - FULL_CODE_REVIEW_REPORT_2026-04-20 - report-v6 执行计划 - REAL_PROJECT_STATUS 更新 - .gitignore: 新增 .gocache-*/config.yaml 排除 验证: go build/vet 0错误, go test 42/42 PASS, 0 FAIL
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -15,6 +16,11 @@ import (
|
||||
"github.com/user-management-system/internal/service"
|
||||
)
|
||||
|
||||
const (
|
||||
refreshTokenCookieName = "ums_refresh_token"
|
||||
sessionPresenceCookieName = "ums_session_present"
|
||||
)
|
||||
|
||||
// newBackgroundCtx 创建用于后台 goroutine 的带超时独立 context(与请求 context 无关)
|
||||
func newBackgroundCtx(timeoutSec int) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||
@@ -129,6 +135,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
setSessionCookies(c, h.authService, resp.RefreshToken)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -150,20 +157,28 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
// @Router /api/v1/auth/login/totp-verify [post]
|
||||
func (h *AuthHandler) VerifyTOTPAfterPasswordLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID int64 `json:"user_id" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
UserID int64 `json:"user_id" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
TempToken string `json:"temp_token"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.authService.VerifyTOTPAfterPasswordLogin(c.Request.Context(), req.UserID, req.Code, req.DeviceID)
|
||||
resp, err := h.authService.VerifyTOTPAfterPasswordLogin(
|
||||
c.Request.Context(),
|
||||
req.UserID,
|
||||
req.Code,
|
||||
req.DeviceID,
|
||||
req.TempToken,
|
||||
)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
setSessionCookies(c, h.authService, resp.RefreshToken)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -197,6 +212,10 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if req.RefreshToken == "" {
|
||||
req.RefreshToken, _ = c.Cookie(refreshTokenCookieName)
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
usernameStr, _ := username.(string)
|
||||
|
||||
@@ -206,6 +225,8 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
}
|
||||
_ = h.authService.Logout(c.Request.Context(), usernameStr, logoutReq)
|
||||
|
||||
clearSessionCookies(c)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
||||
}
|
||||
|
||||
@@ -222,19 +243,27 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
// @Router /api/v1/auth/refresh-token [post]
|
||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.RefreshToken == "" {
|
||||
req.RefreshToken, _ = c.Cookie(refreshTokenCookieName)
|
||||
}
|
||||
if req.RefreshToken == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "refresh_token is required"})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
setSessionCookies(c, h.authService, resp.RefreshToken)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -480,6 +509,7 @@ func (h *AuthHandler) LoginByEmailCode(c *gin.Context) {
|
||||
h.authService.BestEffortRegisterDevicePublic(devCtx, userID, loginReq)
|
||||
}()
|
||||
}
|
||||
setSessionCookies(c, h.authService, resp.RefreshToken)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
@@ -544,6 +574,7 @@ func (h *AuthHandler) BootstrapAdmin(c *gin.Context) {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
setSessionCookies(c, h.authService, resp.RefreshToken)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"code": 0,
|
||||
@@ -673,6 +704,46 @@ func getUserIDFromContext(c *gin.Context) (int64, bool) {
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func setSessionCookies(c *gin.Context, authService *service.AuthService, refreshToken string) {
|
||||
if c == nil || strings.TrimSpace(refreshToken) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
maxAge := 0
|
||||
if authService != nil {
|
||||
if ttl := authService.RefreshTokenTTLSeconds(); ttl > 0 {
|
||||
maxAge = int(ttl)
|
||||
}
|
||||
}
|
||||
secure := requestUsesHTTPS(c)
|
||||
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie(refreshTokenCookieName, refreshToken, maxAge, "/", "", secure, true)
|
||||
c.SetCookie(sessionPresenceCookieName, "1", maxAge, "/", "", secure, false)
|
||||
}
|
||||
|
||||
func clearSessionCookies(c *gin.Context) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
secure := requestUsesHTTPS(c)
|
||||
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie(refreshTokenCookieName, "", -1, "/", "", secure, true)
|
||||
c.SetCookie(sessionPresenceCookieName, "", -1, "/", "", secure, false)
|
||||
}
|
||||
|
||||
func requestUsesHTTPS(c *gin.Context) bool {
|
||||
if c == nil || c.Request == nil {
|
||||
return false
|
||||
}
|
||||
if c.Request.TLS != nil {
|
||||
return true
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")), "https")
|
||||
}
|
||||
|
||||
// handleError 将 error 转换为对应的 HTTP 响应。
|
||||
// 优先识别 ApplicationError,其次通过关键词推断业务错误类型,兜底返回 500。
|
||||
func handleError(c *gin.Context, err error) {
|
||||
|
||||
Reference in New Issue
Block a user