Merge pull request #31 from MoLing-Dong/main

refactor: 更新刷新令牌处理并删除冗余令牌loginID映射
This commit is contained in:
llc-993
2025-12-17 15:39:53 +07:00
committed by GitHub
3 changed files with 140 additions and 10 deletions

View File

@@ -110,12 +110,6 @@ func (rtm *RefreshTokenManager) GenerateTokenPair(loginID, device string, access
}
}
// Save token-loginID mapping (符合 Java sa-token 设计) | 保存 Token-LoginID 映射
tokenKey := rtm.getTokenKey(accessToken)
if err := rtm.storage.Set(tokenKey, loginID, rtm.accessTTL); err != nil {
return nil, fmt.Errorf("failed to save token: %w", err)
}
// Generate refresh token | 生成刷新令牌
refreshTokenBytes := make([]byte, RefreshTokenLength)
if _, err := rand.Read(refreshTokenBytes); err != nil {
@@ -184,10 +178,14 @@ func (rtm *RefreshTokenManager) RefreshAccessToken(refreshToken string) (*Refres
// Update access token info | 更新访问令牌信息
oldInfo.AccessToken = newAccessToken
// Save token-loginID mapping (符合 Java sa-token 设计) | 保存 Token-LoginID 映射
tokenKey := rtm.getTokenKey(newAccessToken)
if err := rtm.storage.Set(tokenKey, oldInfo.LoginID, rtm.accessTTL); err != nil {
return nil, fmt.Errorf("failed to save token: %w", err)
// Copy original token storage value to new access token key, to keep JSON TokenInfo format
// 复制原 access token 的存储值到新的 access token 键,保持 JSON TokenInfo 格式,避免破坏 IsLogin/CheckLogin
oldTokenKey := rtm.getTokenKey(oldInfo.AccessToken)
if data, err := rtm.storage.Get(oldTokenKey); err == nil && data != nil {
newTokenKey := rtm.getTokenKey(newAccessToken)
if err := rtm.storage.Set(newTokenKey, data, rtm.accessTTL); err != nil {
return nil, fmt.Errorf("failed to save new access token: %w", err)
}
}
// Update storage | 更新存储

View File

@@ -23,6 +23,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/click33/sa-token-go/storage/memory v0.1.4/go.mod h1:nqyuEh23mNjcuG3aI/BqJFz71zkpsgjdStW1BC5lkB0=
github.com/click33/sa-token-go/storage/memory v0.1.5/go.mod h1:HxN2NVLq7lx+sOmq5RmV0h8xJjEUJLm4Xt1Mq+9PV2s=
github.com/click33/sa-token-go/storage/memory v0.1.6/go.mod h1:YNojcgyLC/uFrmReZLePCDQ5WK2fo2WWGRjRMvXVH74=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=

131
stputil/stputil_test.go Normal file
View File

@@ -0,0 +1,131 @@
package stputil
import (
"testing"
"time"
"github.com/click33/sa-token-go/core/config"
"github.com/click33/sa-token-go/core/manager"
"github.com/click33/sa-token-go/storage/memory"
"github.com/stretchr/testify/assert"
)
// setupTestManager 初始化内存存储和全局 Manager
func setupTestManager() {
storage := memory.NewStorage()
cfg := &config.Config{
TokenName: "satoken",
Timeout: 3600,
IsConcurrent: true,
IsShare: true,
MaxLoginCount: -1,
}
mgr := manager.NewManager(storage, cfg)
SetManager(mgr)
}
func TestLoginAndIsLogin(t *testing.T) {
setupTestManager()
token, err := Login("user1")
assert.NoError(t, err)
assert.NotEmpty(t, token)
assert.True(t, IsLogin(token))
loginID, err := GetLoginID(token)
assert.NoError(t, err)
assert.Equal(t, "user1", loginID)
}
func TestPermissionsHelpers(t *testing.T) {
setupTestManager()
token, err := Login("user2")
assert.NoError(t, err)
err = SetPermissions("user2", []string{"user.read", "user.write"})
assert.NoError(t, err)
// HasPermission / CheckPermission
assert.True(t, HasPermission("user2", "user.read"))
assert.NoError(t, CheckPermission(token, "user.read"))
// AND / OR helpers
assert.True(t, HasPermissionsAnd("user2", []string{"user.read", "user.write"}))
assert.True(t, HasPermissionsOr("user2", []string{"user.delete", "user.read"}))
// Permission list by token
perms, err := GetPermissionList(token)
assert.NoError(t, err)
assert.ElementsMatch(t, []string{"user.read", "user.write"}, perms)
}
func TestRoleHelpers(t *testing.T) {
setupTestManager()
token, err := Login("user3")
assert.NoError(t, err)
err = SetRoles("user3", []string{"Admin", "User"})
assert.NoError(t, err)
// HasRole / CheckRole
assert.True(t, HasRole("user3", "Admin"))
assert.NoError(t, CheckRole(token, "Admin"))
// AND / OR helpers
assert.True(t, HasRolesAnd("user3", []string{"Admin", "User"}))
assert.True(t, HasRolesOr("user3", []string{"Guest", "Admin"}))
// Role list by token
roles, err := GetRoleList(token)
assert.NoError(t, err)
assert.ElementsMatch(t, []string{"Admin", "User"}, roles)
}
func TestDisableAndCheckDisable(t *testing.T) {
setupTestManager()
token, err := Login("user4")
assert.NoError(t, err)
// 初始未封禁
assert.NoError(t, CheckDisable(token))
// 封禁账号
err = Disable("user4", time.Hour)
assert.NoError(t, err)
// 现在 CheckDisable 应返回错误(可能是“未登录”或“已封禁”等)
err = CheckDisable(token)
assert.Error(t, err)
disabled := IsDisable("user4")
assert.True(t, disabled)
}
func TestToStringHelpers(t *testing.T) {
assert.Equal(t, "123", toString(123))
assert.Equal(t, "-5", toString(int(-5)))
assert.Equal(t, "0", toString(int64(0)))
assert.Equal(t, "42", toString(uint(42)))
assert.Equal(t, "", toString(struct{}{}))
}
// TestLoginWithRefreshToken_IsLogin 验证双 Token 登录场景下access token 能正常通过 IsLogin/CheckLogin
func TestLoginWithRefreshToken_IsLogin(t *testing.T) {
setupTestManager()
// 使用双 token 登录
tokenInfo, err := LoginWithRefreshToken("user-refresh", "web")
assert.NoError(t, err)
assert.NotEmpty(t, tokenInfo.AccessToken)
assert.NotEmpty(t, tokenInfo.RefreshToken)
// 刚登录的 access token 应该是“已登录”
assert.True(t, IsLogin(tokenInfo.AccessToken))
assert.NoError(t, CheckLogin(tokenInfo.AccessToken))
}