diff --git a/core/security/refresh_token.go b/core/security/refresh_token.go index 500761c..797eb66 100644 --- a/core/security/refresh_token.go +++ b/core/security/refresh_token.go @@ -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 | 更新存储 diff --git a/go.work.sum b/go.work.sum index ff4fc5a..b77dd20 100644 --- a/go.work.sum +++ b/go.work.sum @@ -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= diff --git a/stputil/stputil_test.go b/stputil/stputil_test.go new file mode 100644 index 0000000..d3343fe --- /dev/null +++ b/stputil/stputil_test.go @@ -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)) +} + +