jwtauthMiddleware.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. package middleware
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "perms-system-server/internal/consts"
  8. "perms-system-server/internal/loaders"
  9. "perms-system-server/internal/response"
  10. "github.com/golang-jwt/jwt/v4"
  11. "github.com/zeromicro/go-zero/rest/httpx"
  12. )
  13. type contextKey string
  14. const (
  15. ctxKeyUserDetails contextKey = "userDetails"
  16. )
  17. // Claims JWT access token 的 Claims 结构。
  18. type Claims struct {
  19. TokenType string `json:"tokenType"`
  20. UserId int64 `json:"userId"`
  21. Username string `json:"username"`
  22. ProductCode string `json:"productCode"`
  23. MemberType string `json:"memberType"`
  24. TokenVersion int64 `json:"tokenVersion"`
  25. jwt.RegisteredClaims
  26. }
  27. // ParseWithHMAC 所有 JWT 解析点(HTTP 中间件 / gRPC VerifyToken / RefreshToken 等)
  28. // 的统一入口。必须显式断言 token.Method 为 *jwt.SigningMethodHMAC,避免未来迁移到 RSA/ECDSA
  29. // 非对称密钥时把公钥当成 HMAC 共享密钥伪造 token(jwt-go 历史 CVE-2016-10555 同类问题,
  30. // OWASP JWT Cheat Sheet / RFC 8725 强制要求 alg 白名单,见审计 H-4 / L-N1)。
  31. //
  32. // 函数放在 middleware 包是为了避免 auth → middleware 的循环依赖(auth 包已经引用
  33. // middleware.Claims)。所有历史 inline keyfunc 调用点都应统一替换为本 helper,
  34. // 把"算法混淆防御"的审计覆盖矩阵收敛到一个函数。
  35. func ParseWithHMAC(tokenStr, secret string, claims jwt.Claims) (*jwt.Token, error) {
  36. return jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
  37. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  38. return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
  39. }
  40. return []byte(secret), nil
  41. })
  42. }
  43. type JwtAuthMiddleware struct {
  44. accessSecret string
  45. loader *loaders.UserDetailsLoader
  46. }
  47. func NewJwtAuthMiddleware(accessSecret string, loader *loaders.UserDetailsLoader) *JwtAuthMiddleware {
  48. return &JwtAuthMiddleware{
  49. accessSecret: accessSecret,
  50. loader: loader,
  51. }
  52. }
  53. func (m *JwtAuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
  54. return func(w http.ResponseWriter, r *http.Request) {
  55. authHeader := r.Header.Get("Authorization")
  56. if authHeader == "" {
  57. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "未登录"))
  58. return
  59. }
  60. tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
  61. if tokenStr == authHeader {
  62. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "token格式错误"))
  63. return
  64. }
  65. token, err := ParseWithHMAC(tokenStr, m.accessSecret, &Claims{})
  66. if err != nil || !token.Valid {
  67. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "token无效或已过期"))
  68. return
  69. }
  70. claims, ok := token.Claims.(*Claims)
  71. if !ok || claims.TokenType != consts.TokenTypeAccess {
  72. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "token无效或类型错误"))
  73. return
  74. }
  75. ud, err := m.loader.Load(r.Context(), claims.UserId, claims.ProductCode)
  76. if err != nil {
  77. // DB / Redis 短时不可用;与"用户不存在(Username=="")"严格区分,避免把一次 DB 抖动同化
  78. // 成"全站用户被删除"把客户端集体 kick 掉形成雪崩(见审计 M-1)。返回 503 让客户端按
  79. // 临时故障重试策略处理,token 不作废。
  80. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(503, "服务暂时不可用,请稍后重试"))
  81. return
  82. }
  83. if ud.Username == "" {
  84. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "用户不存在或已被删除"))
  85. return
  86. }
  87. if ud.Status != consts.StatusEnabled {
  88. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(403, "账号已被冻结"))
  89. return
  90. }
  91. // 审计 H-R18-2:所在部门已被冻结(DeptStatus=Disabled)时硬拦截所有非超管请求,
  92. // 与 UpdateDeptLogic 的 normalDeptFrozen / devFullAccessRevoked 语义闭环——
  93. // 冻结部门 = 冻结部门所有成员所有活动,而不仅是"吊销一次 session"。
  94. // DeptId==0(超管或无部门的历史数据)不命中此分支,避免误伤。
  95. if !ud.IsSuperAdmin && ud.DeptId > 0 && ud.DeptStatus != consts.StatusEnabled {
  96. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(403, "所在部门已被冻结"))
  97. return
  98. }
  99. if claims.TokenVersion != ud.TokenVersion {
  100. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(401, "登录状态已失效,请重新登录"))
  101. return
  102. }
  103. if claims.ProductCode != "" && ud.ProductStatus != consts.StatusEnabled {
  104. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(403, "该产品已被禁用"))
  105. return
  106. }
  107. if claims.ProductCode != "" && !ud.IsSuperAdmin && ud.MemberType == "" {
  108. httpx.ErrorCtx(r.Context(), w, response.NewCodeError(403, "您已不是该产品的有效成员"))
  109. return
  110. }
  111. ctx := context.WithValue(r.Context(), ctxKeyUserDetails, ud)
  112. next(w, r.WithContext(ctx))
  113. }
  114. }
  115. // -------- context helpers --------
  116. func WithUserDetails(ctx context.Context, ud *loaders.UserDetails) context.Context {
  117. return context.WithValue(ctx, ctxKeyUserDetails, ud)
  118. }
  119. func GetUserDetails(ctx context.Context) *loaders.UserDetails {
  120. v, _ := ctx.Value(ctxKeyUserDetails).(*loaders.UserDetails)
  121. return v
  122. }
  123. func GetUserId(ctx context.Context) int64 {
  124. if ud := GetUserDetails(ctx); ud != nil {
  125. return ud.UserId
  126. }
  127. return 0
  128. }
  129. func GetProductCode(ctx context.Context) string {
  130. if ud := GetUserDetails(ctx); ud != nil {
  131. return ud.ProductCode
  132. }
  133. return ""
  134. }