jwt_auth.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. package middleware
  2. import (
  3. "context"
  4. "errors"
  5. "strings"
  6. "github.com/Wei-Shaw/sub2api/internal/service"
  7. "github.com/gin-gonic/gin"
  8. )
  9. // NewJWTAuthMiddleware 创建 JWT 认证中间件
  10. func NewJWTAuthMiddleware(
  11. authService *service.AuthService,
  12. userService *service.UserService,
  13. settingService *service.SettingService,
  14. auditService *service.AuditLogService,
  15. ) JWTAuthMiddleware {
  16. return JWTAuthMiddleware(jwtAuth(authService, userService, userService, settingService, auditService))
  17. }
  18. type jwtUserReader interface {
  19. GetByID(ctx context.Context, id int64) (*service.User, error)
  20. }
  21. type userActivityToucher interface {
  22. TouchLastActiveForUser(ctx context.Context, user *service.User)
  23. }
  24. // jwtAuth JWT认证中间件实现
  25. func jwtAuth(
  26. authService *service.AuthService,
  27. userService jwtUserReader,
  28. activityToucher userActivityToucher,
  29. settingService *service.SettingService,
  30. auditService *service.AuditLogService,
  31. ) gin.HandlerFunc {
  32. return func(c *gin.Context) {
  33. // 从Authorization header中提取token
  34. authHeader := c.GetHeader("Authorization")
  35. if authHeader == "" {
  36. AbortWithError(c, 401, "UNAUTHORIZED", "Authorization header is required")
  37. return
  38. }
  39. // 验证Bearer scheme
  40. parts := strings.SplitN(authHeader, " ", 2)
  41. if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
  42. AbortWithError(c, 401, "INVALID_AUTH_HEADER", "Authorization header format must be 'Bearer {token}'")
  43. return
  44. }
  45. tokenString := strings.TrimSpace(parts[1])
  46. if tokenString == "" {
  47. AbortWithError(c, 401, "EMPTY_TOKEN", "Token cannot be empty")
  48. return
  49. }
  50. // 验证token
  51. claims, err := authService.ValidateToken(tokenString)
  52. if err != nil {
  53. if errors.Is(err, service.ErrTokenExpired) {
  54. AbortWithError(c, 401, "TOKEN_EXPIRED", "Token has expired")
  55. return
  56. }
  57. AbortWithError(c, 401, "INVALID_TOKEN", "Invalid token")
  58. return
  59. }
  60. // 从数据库获取最新的用户信息
  61. user, err := userService.GetByID(c.Request.Context(), claims.UserID)
  62. if err != nil {
  63. AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
  64. return
  65. }
  66. // 检查用户状态
  67. if !user.IsActive() {
  68. AbortWithError(c, 401, "USER_INACTIVE", "User account is not active")
  69. return
  70. }
  71. // Security: Validate TokenVersion to ensure token hasn't been invalidated
  72. // This check ensures tokens issued before a password change are rejected
  73. if claims.TokenVersion != user.TokenVersion {
  74. AbortWithError(c, 401, "TOKEN_REVOKED", "Token has been revoked (password changed)")
  75. return
  76. }
  77. // 会话绑定校验:IP/UA 任一变化即撤销会话(功能可在系统设置中关闭)
  78. if !enforceSessionBinding(c, authService, settingService, auditService, claims) {
  79. return
  80. }
  81. c.Set(string(ContextKeyUser), AuthSubject{
  82. UserID: user.ID,
  83. Concurrency: user.Concurrency,
  84. })
  85. c.Set(string(ContextKeyUserRole), user.Role)
  86. c.Set(ContextKeyAuthEmail, user.Email)
  87. c.Set(ContextKeySessionID, claims.SessionID)
  88. if activityToucher != nil {
  89. activityToucher.TouchLastActiveForUser(c.Request.Context(), user)
  90. }
  91. c.Next()
  92. }
  93. }
  94. // Deprecated: prefer GetAuthSubjectFromContext in auth_subject.go.