package handler import ( "net/http" "time" "github.com/freefire/jiang13-bbs/middleware" "github.com/freefire/jiang13-bbs/service" "github.com/gin-gonic/gin" ) var ( accessCookieMaxAge = int(service.AccessTokenTTL.Seconds()) // 15 分钟 refreshCookieMaxAge = int(service.RefreshTokenTTL.Seconds()) // 7 天 ) // setAuthCookies 设置认证 cookie: // - j13_token: access token(HttpOnly,15min) // - j13_refresh: refresh token(HttpOnly,7天,仅 /api/auth 路径) // - j13_csrf: CSRF token(可读,7天,与 refresh 同生命周期,确保 refresh 流程可用) func setAuthCookies(c *gin.Context, accessToken, refreshToken string, secure bool) { csrfToken := service.GenerateCSRFToken() // Access token cookie http.SetCookie(c.Writer, &http.Cookie{ Name: service.CookieName, Value: accessToken, Path: "/", MaxAge: accessCookieMaxAge, HttpOnly: true, Secure: secure, SameSite: http.SameSiteStrictMode, }) // Refresh token cookie(仅 /api/auth 路径使用,缩小攻击面) http.SetCookie(c.Writer, &http.Cookie{ Name: service.RefreshCookieName, Value: refreshToken, Path: "/api/auth", MaxAge: refreshCookieMaxAge, HttpOnly: true, Secure: secure, SameSite: http.SameSiteStrictMode, }) // CSRF cookie(前端可读,放入 X-CSRF-Token header;与 refresh 同寿命以支持 refresh 流程) http.SetCookie(c.Writer, &http.Cookie{ Name: service.CSRFCookieName, Value: csrfToken, Path: "/", MaxAge: refreshCookieMaxAge, HttpOnly: false, Secure: secure, SameSite: http.SameSiteStrictMode, }) } // clearAuthCookies 清除所有认证 cookie func clearAuthCookies(c *gin.Context) { cookies := []struct{ name, path string }{ {service.CookieName, "/"}, {service.RefreshCookieName, "/api/auth"}, {service.CSRFCookieName, "/"}, } for _, ck := range cookies { http.SetCookie(c.Writer, &http.Cookie{ Name: ck.name, Value: "", Path: ck.path, MaxAge: -1, HttpOnly: true, Secure: false, SameSite: http.SameSiteStrictMode, Expires: time.Unix(0, 0), }) } } // RegisterRequest 注册请求 type RegisterRequest struct { Username string `json:"username" binding:"required,min=3,max=32"` Email string `json:"email" binding:"omitempty,email"` Password string `json:"password" binding:"required,min=6,max=64"` } // LoginRequest 登录请求 type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // Register 用户注册 func (h *Handlers) Register(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.Auth.Register(req.Username, req.Email, req.Password) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "id": user.ID, "username": user.Username, }) } // Login 用户登录:access + refresh token 写入 HttpOnly cookie,CSRF 写入可读 cookie func (h *Handlers) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } accessToken, refreshToken, user, err := h.Auth.Login(req.Username, req.Password) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } // dev 模式不设 Secure,生产环境需 HTTPS setAuthCookies(c, accessToken, refreshToken, !h.Cfg.DevMode) c.JSON(http.StatusOK, gin.H{ "user": gin.H{ "id": user.ID, "username": user.Username, "nickname": user.Nickname, "avatar": user.Avatar, "role": user.Role, }, }) } // Refresh 刷新 access token:验证 refresh token,轮转后签发新 access + refresh func (h *Handlers) Refresh(c *gin.Context) { refreshToken, err := c.Cookie(service.RefreshCookieName) if err != nil || refreshToken == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"}) return } accessToken, newRefresh, user, err := h.Auth.RotateRefreshToken(refreshToken) if err != nil { clearAuthCookies(c) c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"}) return } setAuthCookies(c, accessToken, newRefresh, !h.Cfg.DevMode) c.JSON(http.StatusOK, gin.H{ "user": gin.H{ "id": user.ID, "username": user.Username, "nickname": user.Nickname, "avatar": user.Avatar, "role": user.Role, }, }) } // Logout 登出:撤销 refresh token,清除所有认证 cookie func (h *Handlers) Logout(c *gin.Context) { claims := middleware.CurrentUser(c) if claims != nil { // 撤销该用户所有 refresh token(防止 refresh token 被盗用) h.Auth.RevokeAllUserRefreshTokens(claims.ID) } clearAuthCookies(c) c.JSON(http.StatusOK, gin.H{"message": "已登出"}) } // ChangePasswordRequest 修改密码请求 type ChangePasswordRequest struct { OldPassword string `json:"old_password" binding:"required"` NewPassword string `json:"new_password" binding:"required,min=6,max=64"` } // ChangePassword 修改密码:校验旧密码,更新新密码,清除所有登录态 func (h *Handlers) ChangePassword(c *gin.Context) { claims := middleware.CurrentUser(c) var req ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.Auth.ChangePassword(claims.ID, req.OldPassword, req.NewPassword); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 密码已改,token_version 已递增,旧 token 全部失效,清除 cookie 要求重新登录 clearAuthCookies(c) c.JSON(http.StatusOK, gin.H{"message": "密码修改成功,请重新登录"}) } // Me 获取当前用户信息 func (h *Handlers) Me(c *gin.Context) { claims := middleware.CurrentUser(c) if claims == nil { c.JSON(http.StatusOK, gin.H{"user": nil}) return } user, err := h.Auth.GetUserByID(claims.ID) if err != nil { c.JSON(http.StatusOK, gin.H{"user": nil}) return } c.JSON(http.StatusOK, gin.H{ "user": gin.H{ "id": user.ID, "username": user.Username, "nickname": user.Nickname, "avatar": user.Avatar, "role": user.Role, }, }) }