package middleware import ( "context" "errors" "net/http" "strings" "hfb_sys/backend/internal/logging" "hfb_sys/backend/internal/modules/auth" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" ) const ( ContextUserID = "user_id" ContextPhone = "phone" ContextAdminID = "admin_id" ContextUsername = "username" ContextPasswordMustChange = "password_must_change" ContextAuthFailureReason = "auth_failure_reason" ContextAuthTokenSource = "auth_token_source" ContextAuthTokenVersion = "auth_token_version" ContextAuthCurrentVersion = "auth_current_token_version" ContextAuthFailureDetail = "auth_failure_detail" AdminAccessCookieName = "hfb_admin_access" ) type AdminTokenContext struct { Username string PasswordMustChange bool } type AdminTokenValidatorFunc func(ctx context.Context, adminID uint64, tokenVersion int64) (AdminTokenContext, error) func extractBearerToken(c *gin.Context) string { header := c.GetHeader("Authorization") tokenText := strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")) if tokenText != "" && tokenText != header { return tokenText } return "" } func extractToken(c *gin.Context) string { if tokenText := extractBearerToken(c); tokenText != "" { return tokenText } return c.Query("token") } func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc { return func(c *gin.Context) { tokenText := extractToken(c) if tokenText == "" { response.Unauthorized(c, "缺少访问令牌") c.Abort() return } claims, err := jwtManager.ParseSubject(tokenText, "access", "user") if err != nil { response.Unauthorized(c, "访问令牌无效或已过期") c.Abort() return } c.Set(ContextUserID, claims.UserID) c.Set(ContextPhone, claims.Phone) c.Next() } } func AdminAuth(jwtManager *auth.JWTManager, validate AdminTokenValidatorFunc) gin.HandlerFunc { return func(c *gin.Context) { tokenText := extractBearerToken(c) tokenSource := "bearer" if tokenText == "" { if cookieToken, err := c.Cookie(AdminAccessCookieName); err == nil { tokenText = strings.TrimSpace(cookieToken) tokenSource = "cookie" } } if tokenText == "" { RecordAdminAuthFailure(c, "missing", "none", 0, 0) response.Unauthorized(c, "缺少后台访问令牌") c.Abort() return } claims, err := jwtManager.ParseSubject(tokenText, "access", "admin") if err != nil { RecordAdminAuthFailure(c, auth.TokenFailureReason(err), tokenSource, 0, 0) response.Unauthorized(c, "后台访问令牌无效或已过期") c.Abort() return } c.Set(ContextAdminID, claims.UserID) c.Request = c.Request.WithContext(logging.WithAdminID(c.Request.Context(), claims.UserID)) username := claims.Phone passwordMustChange := false if validate != nil { tokenContext, err := validate(c.Request.Context(), claims.UserID, claims.TokenVersion) if err != nil { reason, tokenVersion, currentVersion := AdminValidationFailure(err) RecordAdminAuthFailure(c, reason, tokenSource, tokenVersion, currentVersion, AdminValidationFailureDetail(err)) if AdminValidationUnavailable(err) { response.ServiceUnavailable(c, "后台认证服务暂时不可用") c.Abort() return } response.Unauthorized(c, "后台访问令牌无效或已过期") c.Abort() return } username = tokenContext.Username passwordMustChange = tokenContext.PasswordMustChange } c.Set(ContextUsername, username) c.Set(ContextPasswordMustChange, passwordMustChange) c.Next() } } type authFailureReasonCarrier interface { AuthFailureReason() string } type authFailureVersionCarrier interface { AuthFailureVersions() (int64, int64) } type authFailureDetailCarrier interface { AuthFailureDetail() string } type authFailureUnavailableCarrier interface { AuthFailureUnavailable() bool } // AdminValidationFailure 将认证包内部错误转换为安全、可观测的失败类别。 func AdminValidationFailure(err error) (string, int64, int64) { reason := "admin_validation_failed" var reasonCarrier authFailureReasonCarrier if errors.As(err, &reasonCarrier) && reasonCarrier.AuthFailureReason() != "" { reason = reasonCarrier.AuthFailureReason() } var versionCarrier authFailureVersionCarrier if errors.As(err, &versionCarrier) { tokenVersion, currentVersion := versionCarrier.AuthFailureVersions() return reason, tokenVersion, currentVersion } return reason, 0, 0 } // AdminValidationFailureDetail 返回仅供服务端日志记录的安全错误摘要。 func AdminValidationFailureDetail(err error) string { var detailCarrier authFailureDetailCarrier if errors.As(err, &detailCarrier) { return detailCarrier.AuthFailureDetail() } return "" } // AdminValidationUnavailable 判断失败是否由临时依赖故障引起。 func AdminValidationUnavailable(err error) bool { var unavailableCarrier authFailureUnavailableCarrier return errors.As(err, &unavailableCarrier) && unavailableCarrier.AuthFailureUnavailable() } // RecordAdminAuthFailure 把认证失败诊断字段写入请求上下文,供访问日志统一输出。 func RecordAdminAuthFailure(c *gin.Context, reason, source string, tokenVersion, currentVersion int64, detail ...string) { c.Set(ContextAuthFailureReason, reason) c.Set(ContextAuthTokenSource, source) if tokenVersion != 0 || currentVersion != 0 { c.Set(ContextAuthTokenVersion, tokenVersion) c.Set(ContextAuthCurrentVersion, currentVersion) } if len(detail) > 0 && detail[0] != "" { c.Set(ContextAuthFailureDetail, detail[0]) } } func RequireAdminPasswordChanged() gin.HandlerFunc { allowed := map[string]bool{ "/api/admin/me": true, "/api/admin/auth/logout": true, "/api/admin/admin-users/me/password": true, } return func(c *gin.Context) { value, ok := c.Get(ContextPasswordMustChange) if !ok { c.Next() return } mustChange, ok := value.(bool) if !ok || !mustChange || allowed[c.FullPath()] { c.Next() return } response.Error(c, http.StatusForbidden, "password_must_change", "请先修改初始密码") c.Abort() } }