Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
2004230b
Commit
2004230b
authored
Dec 30, 2025
by
shaw
Browse files
Merge branch 'fix/token-invalidation-on-password-change'
parents
0026e871
19d0ee13
Changes
5
Show whitespace changes
Inline
Side-by-side
backend/internal/repository/user_repo.go
View file @
2004230b
...
...
@@ -198,6 +198,7 @@ type userModel struct {
Concurrency
int
`gorm:"default:5;not null"`
Status
string
`gorm:"size:20;default:active;not null"`
AllowedGroups
pq
.
Int64Array
`gorm:"type:bigint[]"`
TokenVersion
int64
`gorm:"default:0;not null"`
// Incremented on password change
CreatedAt
time
.
Time
`gorm:"not null"`
UpdatedAt
time
.
Time
`gorm:"not null"`
DeletedAt
gorm
.
DeletedAt
`gorm:"index"`
...
...
@@ -221,6 +222,7 @@ func userModelToService(m *userModel) *service.User {
Concurrency
:
m
.
Concurrency
,
Status
:
m
.
Status
,
AllowedGroups
:
[]
int64
(
m
.
AllowedGroups
),
TokenVersion
:
m
.
TokenVersion
,
CreatedAt
:
m
.
CreatedAt
,
UpdatedAt
:
m
.
UpdatedAt
,
}
...
...
@@ -242,6 +244,7 @@ func userModelFromService(u *service.User) *userModel {
Concurrency
:
u
.
Concurrency
,
Status
:
u
.
Status
,
AllowedGroups
:
pq
.
Int64Array
(
u
.
AllowedGroups
),
TokenVersion
:
u
.
TokenVersion
,
CreatedAt
:
u
.
CreatedAt
,
UpdatedAt
:
u
.
UpdatedAt
,
}
...
...
@@ -252,6 +255,7 @@ func applyUserModelToService(dst *service.User, src *userModel) {
return
}
dst
.
ID
=
src
.
ID
dst
.
TokenVersion
=
src
.
TokenVersion
dst
.
CreatedAt
=
src
.
CreatedAt
dst
.
UpdatedAt
=
src
.
UpdatedAt
}
backend/internal/server/middleware/jwt_auth.go
View file @
2004230b
...
...
@@ -61,6 +61,13 @@ func jwtAuth(authService *service.AuthService, userService *service.UserService)
return
}
// Security: Validate TokenVersion to ensure token hasn't been invalidated
// This check ensures tokens issued before a password change are rejected
if
claims
.
TokenVersion
!=
user
.
TokenVersion
{
AbortWithError
(
c
,
401
,
"TOKEN_REVOKED"
,
"Token has been revoked (password changed)"
)
return
}
c
.
Set
(
string
(
ContextKeyUser
),
AuthSubject
{
UserID
:
user
.
ID
,
Concurrency
:
user
.
Concurrency
,
...
...
backend/internal/service/auth_service.go
View file @
2004230b
...
...
@@ -20,6 +20,7 @@ var (
ErrEmailExists
=
infraerrors
.
Conflict
(
"EMAIL_EXISTS"
,
"email already exists"
)
ErrInvalidToken
=
infraerrors
.
Unauthorized
(
"INVALID_TOKEN"
,
"invalid token"
)
ErrTokenExpired
=
infraerrors
.
Unauthorized
(
"TOKEN_EXPIRED"
,
"token has expired"
)
ErrTokenRevoked
=
infraerrors
.
Unauthorized
(
"TOKEN_REVOKED"
,
"token has been revoked"
)
ErrEmailVerifyRequired
=
infraerrors
.
BadRequest
(
"EMAIL_VERIFY_REQUIRED"
,
"email verification is required"
)
ErrRegDisabled
=
infraerrors
.
Forbidden
(
"REGISTRATION_DISABLED"
,
"registration is currently disabled"
)
ErrServiceUnavailable
=
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
)
...
...
@@ -30,6 +31,7 @@ type JWTClaims struct {
UserID
int64
`json:"user_id"`
Email
string
`json:"email"`
Role
string
`json:"role"`
TokenVersion
int64
`json:"token_version"`
// Used to invalidate tokens on password change
jwt
.
RegisteredClaims
}
...
...
@@ -314,6 +316,7 @@ func (s *AuthService) GenerateToken(user *User) (string, error) {
UserID
:
user
.
ID
,
Email
:
user
.
Email
,
Role
:
user
.
Role
,
TokenVersion
:
user
.
TokenVersion
,
RegisteredClaims
:
jwt
.
RegisteredClaims
{
ExpiresAt
:
jwt
.
NewNumericDate
(
expiresAt
),
IssuedAt
:
jwt
.
NewNumericDate
(
now
),
...
...
@@ -368,6 +371,12 @@ func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (
return
""
,
ErrUserNotActive
}
// Security: Check TokenVersion to prevent refreshing revoked tokens
// This ensures tokens issued before a password change cannot be refreshed
if
claims
.
TokenVersion
!=
user
.
TokenVersion
{
return
""
,
ErrTokenRevoked
}
// 生成新token
return
s
.
GenerateToken
(
user
)
}
backend/internal/service/user.go
View file @
2004230b
...
...
@@ -18,6 +18,7 @@ type User struct {
Concurrency
int
Status
string
AllowedGroups
[]
int64
TokenVersion
int64
// Incremented on password change to invalidate existing tokens
CreatedAt
time
.
Time
UpdatedAt
time
.
Time
...
...
backend/internal/service/user_service.go
View file @
2004230b
...
...
@@ -116,6 +116,7 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat
}
// ChangePassword 修改密码
// Security: Increments TokenVersion to invalidate all existing JWT tokens
func
(
s
*
UserService
)
ChangePassword
(
ctx
context
.
Context
,
userID
int64
,
req
ChangePasswordRequest
)
error
{
user
,
err
:=
s
.
userRepo
.
GetByID
(
ctx
,
userID
)
if
err
!=
nil
{
...
...
@@ -131,6 +132,10 @@ func (s *UserService) ChangePassword(ctx context.Context, userID int64, req Chan
return
fmt
.
Errorf
(
"set password: %w"
,
err
)
}
// Increment TokenVersion to invalidate all existing tokens
// This ensures that any tokens issued before the password change become invalid
user
.
TokenVersion
++
if
err
:=
s
.
userRepo
.
Update
(
ctx
,
user
);
err
!=
nil
{
return
fmt
.
Errorf
(
"update user: %w"
,
err
)
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment