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
65c0d8b5
Commit
65c0d8b5
authored
Feb 07, 2026
by
yangjianbo
Browse files
fix(middleware): 管理员JWT增加TokenVersion校验
管理员改密后旧JWT会被拒绝,并补充单元测试覆盖。
parent
a9e256ce
Changes
4
Hide whitespace changes
Inline
Side-by-side
backend/internal/server/api_contract_test.go
View file @
65c0d8b5
...
...
@@ -598,7 +598,7 @@ func newContractDeps(t *testing.T) *contractDeps {
usageRepo
:=
newStubUsageLogRepo
()
usageService
:=
service
.
NewUsageService
(
usageRepo
,
userRepo
,
nil
,
nil
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepo
,
userSubRepo
,
nil
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepo
,
userSubRepo
,
nil
,
cfg
)
subscriptionHandler
:=
handler
.
NewSubscriptionHandler
(
subscriptionService
)
redeemService
:=
service
.
NewRedeemService
(
redeemRepo
,
userRepo
,
subscriptionService
,
nil
,
nil
,
nil
,
nil
)
...
...
backend/internal/server/middleware/admin_auth.go
View file @
65c0d8b5
...
...
@@ -176,6 +176,12 @@ func validateJWTForAdmin(
return
false
}
// 校验 TokenVersion,确保管理员改密后旧 token 失效
if
claims
.
TokenVersion
!=
user
.
TokenVersion
{
AbortWithError
(
c
,
401
,
"TOKEN_REVOKED"
,
"Token has been revoked (password changed)"
)
return
false
}
// 检查管理员权限
if
!
user
.
IsAdmin
()
{
AbortWithError
(
c
,
403
,
"FORBIDDEN"
,
"Admin access required"
)
...
...
backend/internal/server/middleware/admin_auth_test.go
0 → 100644
View file @
65c0d8b5
//go:build unit
package
middleware
import
(
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func
TestAdminAuthJWTValidatesTokenVersion
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
cfg
:=
&
config
.
Config
{
JWT
:
config
.
JWTConfig
{
Secret
:
"test-secret"
,
ExpireHour
:
1
}}
authService
:=
service
.
NewAuthService
(
nil
,
nil
,
nil
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
)
admin
:=
&
service
.
User
{
ID
:
1
,
Email
:
"admin@example.com"
,
Role
:
service
.
RoleAdmin
,
Status
:
service
.
StatusActive
,
TokenVersion
:
2
,
Concurrency
:
1
,
}
userRepo
:=
&
stubUserRepo
{
getByID
:
func
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
User
,
error
)
{
if
id
!=
admin
.
ID
{
return
nil
,
service
.
ErrUserNotFound
}
clone
:=
*
admin
return
&
clone
,
nil
},
}
userService
:=
service
.
NewUserService
(
userRepo
,
nil
)
router
:=
gin
.
New
()
router
.
Use
(
gin
.
HandlerFunc
(
NewAdminAuthMiddleware
(
authService
,
userService
,
nil
)))
router
.
GET
(
"/t"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"ok"
:
true
})
})
t
.
Run
(
"token_version_mismatch_rejected"
,
func
(
t
*
testing
.
T
)
{
token
,
err
:=
authService
.
GenerateToken
(
&
service
.
User
{
ID
:
admin
.
ID
,
Email
:
admin
.
Email
,
Role
:
admin
.
Role
,
TokenVersion
:
admin
.
TokenVersion
-
1
,
})
require
.
NoError
(
t
,
err
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
token
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
w
.
Code
)
require
.
Contains
(
t
,
w
.
Body
.
String
(),
"TOKEN_REVOKED"
)
})
t
.
Run
(
"token_version_match_allows"
,
func
(
t
*
testing
.
T
)
{
token
,
err
:=
authService
.
GenerateToken
(
&
service
.
User
{
ID
:
admin
.
ID
,
Email
:
admin
.
Email
,
Role
:
admin
.
Role
,
TokenVersion
:
admin
.
TokenVersion
,
})
require
.
NoError
(
t
,
err
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
token
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
w
.
Code
)
})
t
.
Run
(
"websocket_token_version_mismatch_rejected"
,
func
(
t
*
testing
.
T
)
{
token
,
err
:=
authService
.
GenerateToken
(
&
service
.
User
{
ID
:
admin
.
ID
,
Email
:
admin
.
Email
,
Role
:
admin
.
Role
,
TokenVersion
:
admin
.
TokenVersion
-
1
,
})
require
.
NoError
(
t
,
err
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"Upgrade"
,
"websocket"
)
req
.
Header
.
Set
(
"Connection"
,
"Upgrade"
)
req
.
Header
.
Set
(
"Sec-WebSocket-Protocol"
,
"sub2api-admin, jwt."
+
token
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
w
.
Code
)
require
.
Contains
(
t
,
w
.
Body
.
String
(),
"TOKEN_REVOKED"
)
})
t
.
Run
(
"websocket_token_version_match_allows"
,
func
(
t
*
testing
.
T
)
{
token
,
err
:=
authService
.
GenerateToken
(
&
service
.
User
{
ID
:
admin
.
ID
,
Email
:
admin
.
Email
,
Role
:
admin
.
Role
,
TokenVersion
:
admin
.
TokenVersion
,
})
require
.
NoError
(
t
,
err
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"Upgrade"
,
"websocket"
)
req
.
Header
.
Set
(
"Connection"
,
"Upgrade"
)
req
.
Header
.
Set
(
"Sec-WebSocket-Protocol"
,
"sub2api-admin, jwt."
+
token
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
w
.
Code
)
})
}
type
stubUserRepo
struct
{
getByID
func
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
User
,
error
)
}
func
(
s
*
stubUserRepo
)
Create
(
ctx
context
.
Context
,
user
*
service
.
User
)
error
{
panic
(
"unexpected Create call"
)
}
func
(
s
*
stubUserRepo
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
User
,
error
)
{
if
s
.
getByID
==
nil
{
panic
(
"GetByID not stubbed"
)
}
return
s
.
getByID
(
ctx
,
id
)
}
func
(
s
*
stubUserRepo
)
GetByEmail
(
ctx
context
.
Context
,
email
string
)
(
*
service
.
User
,
error
)
{
panic
(
"unexpected GetByEmail call"
)
}
func
(
s
*
stubUserRepo
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
service
.
User
,
error
)
{
panic
(
"unexpected GetFirstAdmin call"
)
}
func
(
s
*
stubUserRepo
)
Update
(
ctx
context
.
Context
,
user
*
service
.
User
)
error
{
panic
(
"unexpected Update call"
)
}
func
(
s
*
stubUserRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
panic
(
"unexpected Delete call"
)
}
func
(
s
*
stubUserRepo
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
service
.
User
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected List call"
)
}
func
(
s
*
stubUserRepo
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
filters
service
.
UserListFilters
)
([]
service
.
User
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
}
func
(
s
*
stubUserRepo
)
UpdateBalance
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
error
{
panic
(
"unexpected UpdateBalance call"
)
}
func
(
s
*
stubUserRepo
)
DeductBalance
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
error
{
panic
(
"unexpected DeductBalance call"
)
}
func
(
s
*
stubUserRepo
)
UpdateConcurrency
(
ctx
context
.
Context
,
id
int64
,
amount
int
)
error
{
panic
(
"unexpected UpdateConcurrency call"
)
}
func
(
s
*
stubUserRepo
)
ExistsByEmail
(
ctx
context
.
Context
,
email
string
)
(
bool
,
error
)
{
panic
(
"unexpected ExistsByEmail call"
)
}
func
(
s
*
stubUserRepo
)
RemoveGroupFromAllowedGroups
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected RemoveGroupFromAllowedGroups call"
)
}
func
(
s
*
stubUserRepo
)
UpdateTotpSecret
(
ctx
context
.
Context
,
userID
int64
,
encryptedSecret
*
string
)
error
{
panic
(
"unexpected UpdateTotpSecret call"
)
}
func
(
s
*
stubUserRepo
)
EnableTotp
(
ctx
context
.
Context
,
userID
int64
)
error
{
panic
(
"unexpected EnableTotp call"
)
}
func
(
s
*
stubUserRepo
)
DisableTotp
(
ctx
context
.
Context
,
userID
int64
)
error
{
panic
(
"unexpected DisableTotp call"
)
}
backend/internal/server/middleware/api_key_auth_test.go
View file @
65c0d8b5
...
...
@@ -60,7 +60,7 @@ func TestSimpleModeBypassesQuotaCheck(t *testing.T) {
t
.
Run
(
"simple_mode_bypasses_quota_check"
,
func
(
t
*
testing
.
T
)
{
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeSimple
}
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepo
,
nil
,
nil
,
nil
,
nil
,
nil
,
cfg
)
subscriptionService
:=
service
.
NewSubscriptionService
(
nil
,
&
stubUserSubscriptionRepo
{},
nil
)
subscriptionService
:=
service
.
NewSubscriptionService
(
nil
,
&
stubUserSubscriptionRepo
{},
nil
,
cfg
)
router
:=
newAuthTestRouter
(
apiKeyService
,
subscriptionService
,
cfg
)
w
:=
httptest
.
NewRecorder
()
...
...
@@ -99,7 +99,7 @@ func TestSimpleModeBypassesQuotaCheck(t *testing.T) {
resetWeekly
:
func
(
ctx
context
.
Context
,
id
int64
,
start
time
.
Time
)
error
{
return
nil
},
resetMonthly
:
func
(
ctx
context
.
Context
,
id
int64
,
start
time
.
Time
)
error
{
return
nil
},
}
subscriptionService
:=
service
.
NewSubscriptionService
(
nil
,
subscriptionRepo
,
nil
)
subscriptionService
:=
service
.
NewSubscriptionService
(
nil
,
subscriptionRepo
,
nil
,
cfg
)
router
:=
newAuthTestRouter
(
apiKeyService
,
subscriptionService
,
cfg
)
w
:=
httptest
.
NewRecorder
()
...
...
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