From 6c943326a6eae75777fb3ac833fb4d0335659cea Mon Sep 17 00:00:00 2001 From: Moling <1970115881@qq.com> Date: Sat, 25 Oct 2025 18:01:50 +0800 Subject: [PATCH] feat: introduce key prefix management for storage components, enhancing compatibility with Java sa-token and improving key handling across core functionalities --- core/builder/builder.go | 18 +++++++ core/config/config.go | 23 ++++++--- core/manager/manager.go | 16 +++++-- core/oauth2/oauth2.go | 17 ++++--- core/satoken.go | 14 +++--- core/security/nonce.go | 23 +++++---- core/security/refresh_token.go | 9 ++-- demo/java_compat_demo.go | 87 ++++++++++++++++++++++++++++++++++ demo/usage_example.go | 1 + examples/redis-example/go.sum | 9 ++++ examples/redis-example/main.go | 22 ++++++--- storage/redis/redis.go | 44 +++++------------ 12 files changed, 207 insertions(+), 76 deletions(-) create mode 100644 demo/java_compat_demo.go create mode 100644 demo/usage_example.go diff --git a/core/builder/builder.go b/core/builder/builder.go index a08fbd9..3fe67ce 100644 --- a/core/builder/builder.go +++ b/core/builder/builder.go @@ -2,6 +2,7 @@ package builder import ( "fmt" + "strings" "time" "github.com/click33/sa-token-go/core/adapter" @@ -29,6 +30,7 @@ type Builder struct { isReadCookie bool dataRefreshPeriod int64 tokenSessionCheckLogin bool + keyPrefix string cookieConfig *config.CookieConfig } @@ -50,6 +52,7 @@ func NewBuilder() *Builder { isReadCookie: false, dataRefreshPeriod: config.NoLimit, tokenSessionCheckLogin: true, + keyPrefix: "satoken:", cookieConfig: &config.CookieConfig{ Domain: "", Path: config.DefaultCookiePath, @@ -229,6 +232,20 @@ func (b *Builder) CookieConfig(cfg *config.CookieConfig) *Builder { return b } +// KeyPrefix sets storage key prefix | 设置存储键前缀 +// Automatically adds ":" suffix if not present (except for empty string) | 自动添加 ":" 后缀(空字符串除外) +// Examples: "satoken" -> "satoken:", "myapp" -> "myapp:", "" -> "" +// Use empty string "" for Java sa-token compatibility | 使用空字符串 "" 兼容 Java sa-token +func (b *Builder) KeyPrefix(prefix string) *Builder { + // 如果前缀不为空且不以 : 结尾,自动添加 : + if prefix != "" && !strings.HasSuffix(prefix, ":") { + b.keyPrefix = prefix + ":" + } else { + b.keyPrefix = prefix + } + return b +} + // NeverExpire sets token to never expire | 设置Token永不过期 func (b *Builder) NeverExpire() *Builder { b.timeout = config.NoLimit @@ -292,6 +309,7 @@ func (b *Builder) Build() *manager.Manager { JwtSecretKey: b.jwtSecretKey, IsLog: b.isLog, IsPrintBanner: b.isPrintBanner, + KeyPrefix: b.keyPrefix, CookieConfig: b.cookieConfig, } diff --git a/core/config/config.go b/core/config/config.go index 5c0320a..fcb2e90 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -40,11 +40,11 @@ const ( // Default configuration constants | 默认配置常量 const ( - DefaultTokenName = "satoken" - DefaultTimeout = 2592000 // 30 days in seconds | 30天(秒) - DefaultMaxLoginCount = 12 // Maximum concurrent logins | 最大并发登录数 - DefaultCookiePath = "/" - NoLimit = -1 // No limit flag | 不限制标志 + DefaultTokenName = "satoken" + DefaultTimeout = 2592000 // 30 days in seconds | 30天(秒) + DefaultMaxLoginCount = 12 // Maximum concurrent logins | 最大并发登录数 + DefaultCookiePath = "/" + NoLimit = -1 // No limit flag | 不限制标志 ) // IsValid checks if the TokenStyle is valid | 检查TokenStyle是否有效 @@ -109,6 +109,10 @@ type Config struct { // IsPrintBanner Print startup banner (default: true) | 是否打印启动 Banner(默认:true) IsPrintBanner bool + // KeyPrefix Storage key prefix for Redis isolation (default: "satoken:") | 存储键前缀,用于Redis隔离(默认:"satoken:") + // Set to empty "" to be compatible with Java sa-token default behavior | 设置为空""以兼容Java sa-token默认行为 + KeyPrefix string + // CookieConfig Cookie configuration | Cookie配置 CookieConfig *CookieConfig } @@ -153,6 +157,7 @@ func DefaultConfig() *Config { JwtSecretKey: "", IsLog: false, IsPrintBanner: true, + KeyPrefix: "satoken:", CookieConfig: &CookieConfig{ Domain: "", Path: DefaultCookiePath, @@ -310,8 +315,14 @@ func (c *Config) SetIsPrintBanner(isPrint bool) *Config { return c } +// SetKeyPrefix Set storage key prefix | 设置存储键前缀 +func (c *Config) SetKeyPrefix(prefix string) *Config { + c.KeyPrefix = prefix + return c +} + // SetCookieConfig Set cookie configuration | 设置Cookie配置 func (c *Config) SetCookieConfig(cookieConfig *CookieConfig) *Config { c.CookieConfig = cookieConfig return c -} \ No newline at end of file +} diff --git a/core/manager/manager.go b/core/manager/manager.go index a82b6ec..dc74ccf 100644 --- a/core/manager/manager.go +++ b/core/manager/manager.go @@ -17,7 +17,7 @@ import ( // Constants for storage keys and default values | 存储键和默认值常量 const ( DefaultDevice = "default" - DefaultPrefix = "satoken:" + DefaultPrefix = "satoken" DisableValue = "1" DefaultNonceTTL = 5 * time.Minute @@ -72,14 +72,20 @@ func NewManager(storage adapter.Storage, cfg *config.Config) *Manager { cfg = config.DefaultConfig() } + // Use configured prefix, fallback to default | 使用配置的前缀,回退到默认值 + prefix := cfg.KeyPrefix + if prefix == "" { + prefix = DefaultPrefix + } + return &Manager{ storage: storage, config: cfg, generator: token.NewGenerator(cfg), - prefix: DefaultPrefix, - nonceManager: security.NewNonceManager(storage, DefaultNonceTTL), - refreshManager: security.NewRefreshTokenManager(storage, cfg), - oauth2Server: oauth2.NewOAuth2Server(storage), + prefix: prefix, + nonceManager: security.NewNonceManager(storage, prefix, DefaultNonceTTL), + refreshManager: security.NewRefreshTokenManager(storage, prefix, cfg), + oauth2Server: oauth2.NewOAuth2Server(storage, prefix), } } diff --git a/core/oauth2/oauth2.go b/core/oauth2/oauth2.go index 609ec15..1751668 100644 --- a/core/oauth2/oauth2.go +++ b/core/oauth2/oauth2.go @@ -36,9 +36,9 @@ const ( AccessTokenLength = 32 // Access token byte length | 访问令牌字节长度 RefreshTokenLength = 32 // Refresh token byte length | 刷新令牌字节长度 - CodeKeyPrefix = "satoken:oauth2:code:" // Code storage key prefix | 授权码存储键前缀 - TokenKeyPrefix = "satoken:oauth2:token:" // Token storage key prefix | 令牌存储键前缀 - RefreshKeyPrefix = "satoken:oauth2:refresh:" // Refresh storage key prefix | 刷新令牌存储键前缀 + CodeKeySuffix = "oauth2:code:" // Code key suffix after prefix | 授权码键后缀 + TokenKeySuffix = "oauth2:token:" // Token key suffix after prefix | 令牌键后缀 + RefreshKeySuffix = "oauth2:refresh:" // Refresh key suffix after prefix | 刷新令牌键后缀 TokenTypeBearer = "Bearer" // Token type | 令牌类型 ) @@ -102,6 +102,7 @@ type AccessToken struct { // OAuth2Server OAuth2 authorization server | OAuth2授权服务器 type OAuth2Server struct { storage adapter.Storage + keyPrefix string // Configurable prefix | 可配置的前缀 clients map[string]*Client clientsMu sync.RWMutex // Clients map lock | 客户端映射锁 codeExpiration time.Duration // Authorization code expiration (10min) | 授权码过期时间(10分钟) @@ -109,9 +110,11 @@ type OAuth2Server struct { } // NewOAuth2Server Creates a new OAuth2 server | 创建新的OAuth2服务器 -func NewOAuth2Server(storage adapter.Storage) *OAuth2Server { +// prefix: key prefix (e.g., "satoken:" or "" for Java compatibility) | 键前缀(如:"satoken:" 或 "" 兼容Java) +func NewOAuth2Server(storage adapter.Storage, prefix string) *OAuth2Server { return &OAuth2Server{ storage: storage, + keyPrefix: prefix, clients: make(map[string]*Client), codeExpiration: DefaultCodeExpiration, tokenExpiration: DefaultTokenExpiration, @@ -374,15 +377,15 @@ func (s *OAuth2Server) RevokeToken(tokenString string) error { // getCodeKey Gets storage key for authorization code | 获取授权码的存储键 func (s *OAuth2Server) getCodeKey(code string) string { - return CodeKeyPrefix + code + return s.keyPrefix + CodeKeySuffix + code } // getTokenKey Gets storage key for access token | 获取访问令牌的存储键 func (s *OAuth2Server) getTokenKey(token string) string { - return TokenKeyPrefix + token + return s.keyPrefix + TokenKeySuffix + token } // getRefreshKey Gets storage key for refresh token | 获取刷新令牌的存储键 func (s *OAuth2Server) getRefreshKey(refreshToken string) string { - return RefreshKeyPrefix + refreshToken + return s.keyPrefix + RefreshKeySuffix + refreshToken } diff --git a/core/satoken.go b/core/satoken.go index 5e49620..8869394 100644 --- a/core/satoken.go +++ b/core/satoken.go @@ -17,7 +17,7 @@ import ( ) // Version Sa-Token-Go version | Sa-Token-Go版本 -const Version = "0.1.0" +const Version = "0.1.1" // ============ Exported Types | 导出的类型 ============ // Export main types and functions for external use | 导出主要类型和函数,方便外部使用 @@ -172,20 +172,20 @@ func NewBuilder() *Builder { } // NewNonceManager Creates a new nonce manager | 创建新的Nonce管理器 -func NewNonceManager(storage Storage, ttl ...int64) *NonceManager { +func NewNonceManager(storage Storage, prefix string, ttl ...int64) *NonceManager { var duration time.Duration if len(ttl) > 0 && ttl[0] > 0 { duration = time.Duration(ttl[0]) * time.Second } - return security.NewNonceManager(storage, duration) + return security.NewNonceManager(storage, prefix, duration) } // NewRefreshTokenManager Creates a new refresh token manager | 创建新的刷新令牌管理器 -func NewRefreshTokenManager(storage Storage, cfg *Config) *RefreshTokenManager { - return security.NewRefreshTokenManager(storage, cfg) +func NewRefreshTokenManager(storage Storage, prefix string, cfg *Config) *RefreshTokenManager { + return security.NewRefreshTokenManager(storage, prefix, cfg) } // NewOAuth2Server Creates a new OAuth2 server | 创建新的OAuth2服务器 -func NewOAuth2Server(storage Storage) *OAuth2Server { - return oauth2.NewOAuth2Server(storage) +func NewOAuth2Server(storage Storage, prefix string) *OAuth2Server { + return oauth2.NewOAuth2Server(storage, prefix) } diff --git a/core/security/nonce.go b/core/security/nonce.go index 4190a17..9b9dd54 100644 --- a/core/security/nonce.go +++ b/core/security/nonce.go @@ -25,9 +25,9 @@ import ( // Constants for nonce | Nonce常量 const ( - DefaultNonceTTL = 5 * time.Minute // Default nonce expiration | 默认nonce过期时间 - NonceLength = 32 // Nonce byte length | Nonce字节长度 - NonceKeyPrefix = "satoken:nonce:" // Storage key prefix | 存储键前缀 + DefaultNonceTTL = 5 * time.Minute // Default nonce expiration | 默认nonce过期时间 + NonceLength = 32 // Nonce byte length | Nonce字节长度 + NonceKeySuffix = "nonce:" // Key suffix after prefix | 前缀后的键后缀 ) // Error variables | 错误变量 @@ -37,20 +37,23 @@ var ( // NonceManager Nonce manager for anti-replay attacks | Nonce管理器,用于防重放攻击 type NonceManager struct { - storage adapter.Storage - ttl time.Duration - mu sync.RWMutex + storage adapter.Storage + keyPrefix string // Configurable prefix | 可配置的前缀 + ttl time.Duration + mu sync.RWMutex } // NewNonceManager Creates a new nonce manager | 创建新的Nonce管理器 +// prefix: key prefix (e.g., "satoken:" or "" for Java compatibility) | 键前缀(如:"satoken:" 或 "" 兼容Java) // ttl: time to live, default 5 minutes | 过期时间,默认5分钟 -func NewNonceManager(storage adapter.Storage, ttl time.Duration) *NonceManager { +func NewNonceManager(storage adapter.Storage, prefix string, ttl time.Duration) *NonceManager { if ttl == 0 { ttl = DefaultNonceTTL } return &NonceManager{ - storage: storage, - ttl: ttl, + storage: storage, + keyPrefix: prefix, + ttl: ttl, } } @@ -115,5 +118,5 @@ func (nm *NonceManager) IsValid(nonce string) bool { // getNonceKey Gets storage key for nonce | 获取nonce的存储键 func (nm *NonceManager) getNonceKey(nonce string) string { - return NonceKeyPrefix + nonce + return nm.keyPrefix + NonceKeySuffix + nonce } diff --git a/core/security/refresh_token.go b/core/security/refresh_token.go index fe74b9b..095b15a 100644 --- a/core/security/refresh_token.go +++ b/core/security/refresh_token.go @@ -30,7 +30,7 @@ const ( DefaultRefreshTTL = 30 * 24 * time.Hour // 30 days | 30天 DefaultAccessTTL = 2 * time.Hour // 2 hours | 2小时 RefreshTokenLength = 32 // Refresh token byte length | 刷新令牌字节长度 - RefreshKeyPrefix = "satoken:refresh:" // Storage key prefix | 存储键前缀 + RefreshKeySuffix = "refresh:" // Key suffix after prefix | 前缀后的键后缀 ) // Error variables | 错误变量 @@ -53,14 +53,16 @@ type RefreshTokenInfo struct { // RefreshTokenManager Refresh token manager | 刷新令牌管理器 type RefreshTokenManager struct { storage adapter.Storage + keyPrefix string // Configurable prefix | 可配置的前缀 tokenGen *token.Generator refreshTTL time.Duration // Refresh token TTL (30 days) | 刷新令牌有效期(30天) accessTTL time.Duration // Access token TTL (configurable) | 访问令牌有效期(可配置) } // NewRefreshTokenManager Creates a new refresh token manager | 创建新的刷新令牌管理器 +// prefix: key prefix (e.g., "satoken:" or "" for Java compatibility) | 键前缀(如:"satoken:" 或 "" 兼容Java) // cfg: configuration, uses Timeout for access token TTL | 配置,使用Timeout作为访问令牌有效期 -func NewRefreshTokenManager(storage adapter.Storage, cfg *config.Config) *RefreshTokenManager { +func NewRefreshTokenManager(storage adapter.Storage, prefix string, cfg *config.Config) *RefreshTokenManager { accessTTL := time.Duration(cfg.Timeout) * time.Second if accessTTL == 0 { @@ -69,6 +71,7 @@ func NewRefreshTokenManager(storage adapter.Storage, cfg *config.Config) *Refres return &RefreshTokenManager{ storage: storage, + keyPrefix: prefix, tokenGen: token.NewGenerator(cfg), refreshTTL: DefaultRefreshTTL, accessTTL: accessTTL, @@ -194,5 +197,5 @@ func (rtm *RefreshTokenManager) IsValid(refreshToken string) bool { // getRefreshKey Gets storage key for refresh token | 获取刷新令牌的存储键 func (rtm *RefreshTokenManager) getRefreshKey(refreshToken string) string { - return RefreshKeyPrefix + refreshToken + return rtm.keyPrefix + RefreshKeySuffix + refreshToken } diff --git a/demo/java_compat_demo.go b/demo/java_compat_demo.go new file mode 100644 index 0000000..0bb0553 --- /dev/null +++ b/demo/java_compat_demo.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/storage/memory" +) + +func main() { + fmt.Println("🔄 Java sa-token 兼容性演示") + fmt.Println("=" + "────────────────────────────────────────────────────────────" + "=") + fmt.Println() + + storage := memory.NewStorage() + + // 方式1: Go 默认配置(带前缀 "satoken:") + fmt.Println("【方式1】Go 默认配置 - 使用前缀 'satoken:'") + mgr1 := core.NewBuilder(). + Storage(storage). + TokenName("satoken"). // 使用默认的 token 名称 + KeyPrefix("satoken:"). // 显式设置前缀(默认值) + IsPrintBanner(false). + Build() + + token1, _ := mgr1.Login("user001", "pc") + fmt.Printf("✅ 登录成功,Token: %s\n", token1) + fmt.Println(" Redis Keys 示例:") + fmt.Println(" - satoken:token:" + token1) + fmt.Println(" - satoken:account:user001:pc") + fmt.Println(" - satoken:session:user001") + fmt.Println() + + // 方式2: Java sa-token 兼容配置(无前缀) + fmt.Println("【方式2】Java 兼容配置 - 无前缀(与Java默认行为一致)") + storage2 := memory.NewStorage() + mgr2 := core.NewBuilder(). + Storage(storage2). + TokenName("satoken"). // 必须与 Java 端配置一致 + KeyPrefix(""). // 空前缀,兼容 Java sa-token + IsPrintBanner(false). + Build() + + token2, _ := mgr2.Login("user002", "web") + fmt.Printf("✅ 登录成功,Token: %s\n", token2) + fmt.Println(" Redis Keys 示例(兼容Java):") + fmt.Println(" - token:" + token2) + fmt.Println(" - account:user002:web") + fmt.Println(" - session:user002") + fmt.Println() + + // 方式3: 自定义前缀(多应用隔离) + fmt.Println("【方式3】自定义前缀 - 用于多应用隔离") + storage3 := memory.NewStorage() + mgr3 := core.NewBuilder(). + Storage(storage3). + TokenName("satoken"). + KeyPrefix("myapp:sa:"). // 自定义前缀 + IsPrintBanner(false). + Build() + + token3, _ := mgr3.Login("user003", "app") + fmt.Printf("✅ 登录成功,Token: %s\n", token3) + fmt.Println(" Redis Keys 示例:") + fmt.Println(" - myapp:sa:token:" + token3) + fmt.Println(" - myapp:sa:account:user003:app") + fmt.Println(" - myapp:sa:session:user003") + fmt.Println() + + // 关键配置说明 + fmt.Println("=" + "────────────────────────────────────────────────────────────" + "=") + fmt.Println("📝 关键配置说明:") + fmt.Println() + fmt.Println("1. 与 Java sa-token 互通:") + fmt.Println(" cfg.SetKeyPrefix(\"\") // 设置为空字符串") + fmt.Println(" 或") + fmt.Println(" builder.KeyPrefix(\"\") // Builder 方式") + fmt.Println() + fmt.Println("2. 多应用隔离:") + fmt.Println(" cfg.SetKeyPrefix(\"app1:\") // 应用1") + fmt.Println(" cfg.SetKeyPrefix(\"app2:\") // 应用2") + fmt.Println() + fmt.Println("3. 默认 Go 行为:") + fmt.Println(" cfg.SetKeyPrefix(\"satoken:\") // 默认值") + fmt.Println() + fmt.Println("=" + "────────────────────────────────────────────────────────────" + "=") +} diff --git a/demo/usage_example.go b/demo/usage_example.go new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/demo/usage_example.go @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/redis-example/go.sum b/examples/redis-example/go.sum index d21fa79..7cbc6f4 100644 --- a/examples/redis-example/go.sum +++ b/examples/redis-example/go.sum @@ -1,7 +1,16 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= diff --git a/examples/redis-example/main.go b/examples/redis-example/main.go index f469f1a..28ebae5 100644 --- a/examples/redis-example/main.go +++ b/examples/redis-example/main.go @@ -13,7 +13,7 @@ import ( ) func main() { - fmt.Println("=== Sa-Token-Go Redis Storage Example ===\n") + fmt.Println("=== Sa-Token-Go Redis Storage Example ===") // Get Redis configuration from environment variables | 从环境变量获取 Redis 配置 redisAddr := os.Getenv("REDIS_ADDR") @@ -38,21 +38,31 @@ func main() { fmt.Printf("✅ Connected to Redis: %s\n\n", redisAddr) // Initialize Sa-Token with Redis storage | 使用 Redis 存储初始化 Sa-Token - redisStorage, err := redis.NewStorage(redisAddr, redisPassword) + redisURL := fmt.Sprintf("redis://:%s@%s/0", redisPassword, redisAddr) + redisStorage, err := redis.NewStorage(redisURL) // Storage 层不处理前缀,符合 Java sa-token 设计 if err != nil { log.Fatalf("❌ Failed to create Redis storage: %v\n", err) } + // 创建 Manager(符合 Java sa-token 标准设计) stputil.SetManager( core.NewBuilder(). Storage(redisStorage). TokenName("Authorization"). TokenStyle(core.TokenStyleRandom64). - Timeout(3600). // 1 hour | 1小时 + Timeout(3600). // 1 hour | 1小时 + KeyPrefix("satoken"). // ✅ 自动添加冒号,实际为 "satoken:"(符合 Java 设计) IsPrintBanner(true). Build(), ) + fmt.Println("📌 当前配置(符合 Java sa-token 标准):") + fmt.Println(" - Storage 层前缀: \"\" (空)") + fmt.Println(" - Manager 层前缀: \"satoken\" → 自动变为 \"satoken:\"") + fmt.Println(" - Redis Key 示例: satoken:login:token:xxx") + fmt.Println(" - ✅ 完全兼容 Java sa-token") + fmt.Println() + // Test authentication | 测试认证功能 fmt.Println("1. Login user | 登录用户") token, err := stputil.Login(1000) @@ -64,14 +74,14 @@ func main() { // Check login status | 检查登录状态 fmt.Println("2. Check login status | 检查登录状态") if stputil.IsLogin(token) { - fmt.Println("✅ User is logged in\n") + fmt.Println("✅ User is logged in") } // Set permissions and roles | 设置权限和角色 fmt.Println("3. Set permissions and roles | 设置权限和角色") stputil.SetPermissions(1000, []string{"user:read", "user:write", "admin:*"}) stputil.SetRoles(1000, []string{"admin", "user"}) - fmt.Println("✅ Permissions and roles set\n") + fmt.Println("✅ Permissions and roles set") // Check permission | 检查权限 fmt.Println("4. Check permissions | 检查权限") @@ -106,7 +116,7 @@ func main() { fmt.Println("✅ User logged out") if !stputil.IsLogin(token) { - fmt.Println("✅ Token is now invalid\n") + fmt.Println("✅ Token is now invalid") } // Close Redis connection | 关闭 Redis 连接 diff --git a/storage/redis/redis.go b/storage/redis/redis.go index 0e24558..f966005 100644 --- a/storage/redis/redis.go +++ b/storage/redis/redis.go @@ -13,7 +13,6 @@ import ( type Storage struct { client *redis.Client ctx context.Context - keyPrefix string opTimeout time.Duration } @@ -34,7 +33,7 @@ type Config struct { } // NewStorage 通过Redis URL创建存储 -func NewStorage(url string, keyPrefix string) (adapter.Storage, error) { +func NewStorage(url string) (adapter.Storage, error) { opts, err := redis.ParseURL(url) if err != nil { return nil, fmt.Errorf("failed to parse redis url: %w", err) @@ -51,13 +50,12 @@ func NewStorage(url string, keyPrefix string) (adapter.Storage, error) { return &Storage{ client: client, ctx: ctx, - keyPrefix: keyPrefix, opTimeout: 3 * time.Second, }, nil } // NewStorageFromConfig 通过配置创建存储 -func NewStorageFromConfig(cfg *Config, keyPrefix string) (adapter.Storage, error) { +func NewStorageFromConfig(cfg *Config) (adapter.Storage, error) { client := redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), Password: cfg.Password, @@ -83,24 +81,22 @@ func NewStorageFromConfig(cfg *Config, keyPrefix string) (adapter.Storage, error return &Storage{ client: client, ctx: ctx, - keyPrefix: keyPrefix, opTimeout: opTimeout, }, nil } // NewStorageFromClient 从已有的Redis客户端创建存储 -func NewStorageFromClient(client *redis.Client, keyPrefix string) adapter.Storage { +func NewStorageFromClient(client *redis.Client) adapter.Storage { return &Storage{ client: client, ctx: context.Background(), - keyPrefix: keyPrefix, opTimeout: 3 * time.Second, } } -// getKey 获取完整的键名 +// getKey 获取完整的键名(Storage 层不处理前缀,前缀由 Manager 层统一管理) func (s *Storage) getKey(key string) string { - return s.keyPrefix + key + return key } // Set 设置键值对 @@ -157,25 +153,17 @@ func (s *Storage) Keys(pattern string) ([]string, error) { defer cancel() var ( - cursor uint64 - result []string - prefixLen = len(s.keyPrefix) - fullPattern = s.getKey(pattern) + cursor uint64 + result []string ) for { - keys, next, err := s.client.Scan(ctx, cursor, fullPattern, 1000).Result() + keys, next, err := s.client.Scan(ctx, cursor, pattern, 1000).Result() if err != nil { return nil, err } if len(keys) > 0 { - for _, k := range keys { - if len(k) > prefixLen { - result = append(result, k[prefixLen:]) - } else { - result = append(result, k) - } - } + result = append(result, keys...) } cursor = next if cursor == 0 { @@ -199,14 +187,14 @@ func (s *Storage) TTL(key string) (time.Duration, error) { return s.client.TTL(ctx, s.getKey(key)).Result() } -// Clear 清空所有数据(使用前缀匹配删除) +// Clear 清空所有数据(⚠️ 警告:会清空整个 Redis,谨慎使用!应由 Manager 层控制) func (s *Storage) Clear() error { ctx, cancel := s.withTimeout() defer cancel() var cursor uint64 for { - keys, next, err := s.client.Scan(ctx, cursor, s.keyPrefix+"*", 1000).Result() + keys, next, err := s.client.Scan(ctx, cursor, "*", 1000).Result() if err != nil { return err } @@ -256,7 +244,6 @@ type Builder struct { password string database int poolSize int - prefix string } // NewBuilder 创建构建器 @@ -267,7 +254,6 @@ func NewBuilder() *Builder { password: "", database: 0, poolSize: 10, - prefix: "satoken:", } } @@ -301,12 +287,6 @@ func (b *Builder) PoolSize(poolSize int) *Builder { return b } -// KeyPrefix 设置键前缀 -func (b *Builder) KeyPrefix(prefix string) *Builder { - b.prefix = prefix - return b -} - // Build 构建存储 func (b *Builder) Build() (adapter.Storage, error) { return NewStorageFromConfig(&Config{ @@ -315,5 +295,5 @@ func (b *Builder) Build() (adapter.Storage, error) { Password: b.password, Database: b.database, PoolSize: b.poolSize, - }, b.prefix) + }) }