mirror of
https://github.com/click33/sa-token-go.git
synced 2026-04-22 21:17:04 +08:00
feat: introduce key prefix management for storage components, enhancing compatibility with Java sa-token and improving key handling across core functionalities
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
+17
-6
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-7
@@ -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
|
||||
}
|
||||
|
||||
+7
-7
@@ -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)
|
||||
}
|
||||
|
||||
+13
-10
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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("=" + "────────────────────────────────────────────────────────────" + "=")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 连接
|
||||
|
||||
+12
-32
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user