From da3247d4c1673f799e10f03033202ace7f5ad571 Mon Sep 17 00:00:00 2001 From: Moling <1970115881@qq.com> Date: Fri, 5 Dec 2025 01:05:48 +0800 Subject: [PATCH 01/28] =?UTF-8?q?=E6=9B=B4=E6=96=B0README=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=9C=80=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E4=BE=9D=E8=B5=96=E9=A1=B9=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=9C=A8=E6=A0=B8=E5=BF=83=E4=BB=A3=E7=A0=81=E4=B8=AD=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86=E3=80=82=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=BC=95=E7=94=A8=E5=92=8C=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E3=80=82=E8=B0=83=E6=95=B4=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=E7=9A=84=E5=90=8D=E7=A7=B0=E4=BB=A5=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 ++++++++++++++++---------------- README_zh.md | 32 ++++++++++++++++---------------- core/banner/banner.go | 6 ++---- core/banner/banner_test.go | 26 +++++++++++++------------- core/satoken.go | 3 ++- core/version/version.go | 7 +++++++ 6 files changed, 56 insertions(+), 50 deletions(-) create mode 100644 core/version/version.go diff --git a/README.md b/README.md index 0cfaedf..538c8e3 100644 --- a/README.md +++ b/README.md @@ -34,21 +34,21 @@ A lightweight, high-performance Go authentication and authorization framework, i ```bash # Import only the framework integration (includes core + stputil automatically) -go get github.com/click33/sa-token-go/integrations/gin@v0.1.5 # Gin framework +go get github.com/click33/sa-token-go/integrations/gin@latest # Gin framework # or -go get github.com/click33/sa-token-go/integrations/echo@v0.1.5 # Echo framework +go get github.com/click33/sa-token-go/integrations/echo@latest # Echo framework # or -go get github.com/click33/sa-token-go/integrations/fiber@v0.1.5 # Fiber framework +go get github.com/click33/sa-token-go/integrations/fiber@latest # Fiber framework # or -go get github.com/click33/sa-token-go/integrations/chi@v0.1.5 # Chi framework +go get github.com/click33/sa-token-go/integrations/chi@latest # Chi framework # or -go get github.com/click33/sa-token-go/integrations/gf@v0.1.5 # GoFrame framework +go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame framework # or -go get github.com/click33/sa-token-go/integrations/kratos@v0.1.5 # Kratos framework +go get github.com/click33/sa-token-go/integrations/kratos@latest # Kratos framework # Storage module (choose one) -go get github.com/click33/sa-token-go/storage/memory@v0.1.5 # Memory storage (dev) -go get github.com/click33/sa-token-go/storage/redis@v0.1.5 # Redis storage (prod) +go get github.com/click33/sa-token-go/storage/memory@latest # Memory storage (dev) +go get github.com/click33/sa-token-go/storage/redis@latest # Redis storage (prod) ``` #### Option 2: Separate Import @@ -59,16 +59,16 @@ go get github.com/click33/sa-token-go/core@v0.1.5 go get github.com/click33/sa-token-go/stputil@v0.1.5 # Storage module (choose one) -go get github.com/click33/sa-token-go/storage/memory@v0.1.5 # Memory storage (dev) -go get github.com/click33/sa-token-go/storage/redis@v0.1.5 # Redis storage (prod) +go get github.com/click33/sa-token-go/storage/memory@latest # Memory storage (dev) +go get github.com/click33/sa-token-go/storage/redis@latest # Redis storage (prod) # Framework integration (optional) -go get github.com/click33/sa-token-go/integrations/gin@v0.1.5 # Gin framework -go get github.com/click33/sa-token-go/integrations/echo@v0.1.5 # Echo framework -go get github.com/click33/sa-token-go/integrations/fiber@v0.1.5 # Fiber framework -go get github.com/click33/sa-token-go/integrations/chi@v0.1.5 # Chi framework -go get github.com/click33/sa-token-go/integrations/gf@v0.1.5 # GoFrame framework -go get github.com/click33/sa-token-go/integrations/kratos@v0.1.5 # Kratos framework +go get github.com/click33/sa-token-go/integrations/gin@latest # Gin framework +go get github.com/click33/sa-token-go/integrations/echo@latest # Echo framework +go get github.com/click33/sa-token-go/integrations/fiber@latest # Fiber framework +go get github.com/click33/sa-token-go/integrations/chi@latest # Chi framework +go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame framework +go get github.com/click33/sa-token-go/integrations/kratos@latest# Kratos framework ``` ### ⚡ Minimal Usage (One-line Initialization) diff --git a/README_zh.md b/README_zh.md index 622a0b0..c4cb6de 100644 --- a/README_zh.md +++ b/README_zh.md @@ -34,21 +34,21 @@ ```bash # 只导入框架集成包(自动包含 core + stputil) -go get github.com/click33/sa-token-go/integrations/gin@v0.1.5 # Gin框架 +go get github.com/click33/sa-token-go/integrations/gin@latest # Gin框架 # 或 -go get github.com/click33/sa-token-go/integrations/echo@v0.1.5 # Echo框架 +go get github.com/click33/sa-token-go/integrations/echo@latest # Echo框架 # 或 -go get github.com/click33/sa-token-go/integrations/fiber@v0.1.5 # Fiber框架 +go get github.com/click33/sa-token-go/integrations/fiber@latest # Fiber框架 # 或 -go get github.com/click33/sa-token-go/integrations/chi@v0.1.5 # Chi框架 +go get github.com/click33/sa-token-go/integrations/chi@latest # Chi框架 # 或 -go get github.com/click33/sa-token-go/integrations/gf@v0.1.5 # GoFrame框架 +go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame框架 # 或 -go get github.com/click33/sa-token-go/integrations/kratos@v0.1.5 # Kratos框架 +go get github.com/click33/sa-token-go/integrations/kratos@latest# Kratos框架 # 存储模块(选一个) -go get github.com/click33/sa-token-go/storage/memory@v0.1.5 # 内存存储(开发) -go get github.com/click33/sa-token-go/storage/redis@v0.1.5 # Redis存储(生产) +go get github.com/click33/sa-token-go/storage/memory@latest # 内存存储(开发) +go get github.com/click33/sa-token-go/storage/redis@latest # Redis存储(生产) ``` #### 方式二:分开导入 @@ -59,16 +59,16 @@ go get github.com/click33/sa-token-go/core@v0.1.5 go get github.com/click33/sa-token-go/stputil@v0.1.5 # 存储模块(选一个) -go get github.com/click33/sa-token-go/storage/memory@v0.1.5 # 内存存储(开发) -go get github.com/click33/sa-token-go/storage/redis@v0.1.5 # Redis存储(生产) +go get github.com/click33/sa-token-go/storage/memory@latest # 内存存储(开发) +go get github.com/click33/sa-token-go/storage/redis@latest # Redis存储(生产) # 框架集成(可选) -go get github.com/click33/sa-token-go/integrations/gin@v0.1.5 # Gin框架 -go get github.com/click33/sa-token-go/integrations/echo@v0.1.5 # Echo框架 -go get github.com/click33/sa-token-go/integrations/fiber@v0.1.5 # Fiber框架 -go get github.com/click33/sa-token-go/integrations/chi@v0.1.5 # Chi框架 -go get github.com/click33/sa-token-go/integrations/gf@v0.1.5 # GoFrame框架 -go get github.com/click33/sa-token-go/integrations/kratos@v0.1.5 # Kratos框架 +go get github.com/click33/sa-token-go/integrations/gin@latest # Gin框架 +go get github.com/click33/sa-token-go/integrations/echo@latest # Echo框架 +go get github.com/click33/sa-token-go/integrations/fiber@latest # Fiber框架 +go get github.com/click33/sa-token-go/integrations/chi@latest # Chi框架 +go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame框架 +go get github.com/click33/sa-token-go/integrations/kratos@latest# Kratos框架 ``` ### ⚡ 超简洁使用(一行初始化) diff --git a/core/banner/banner.go b/core/banner/banner.go index cd1ae28..5f746e5 100644 --- a/core/banner/banner.go +++ b/core/banner/banner.go @@ -6,11 +6,9 @@ import ( "strings" "github.com/click33/sa-token-go/core/config" + "github.com/click33/sa-token-go/core/version" ) -// Version version number | 版本号 -const Version = "0.1.1" - // Banner startup banner | 启动横幅 const Banner = ` _____ ______ __ ______ @@ -33,7 +31,7 @@ const ( // Print prints startup banner | 打印启动横幅 func Print() { - fmt.Printf(Banner, Version) + fmt.Printf(Banner, version.Version) fmt.Printf(":: Go Version :: %s\n", runtime.Version()) fmt.Printf(":: GOOS/GOARCH :: %s/%s\n", runtime.GOOS, runtime.GOARCH) fmt.Println() diff --git a/core/banner/banner_test.go b/core/banner/banner_test.go index 90027d4..aaf44d1 100644 --- a/core/banner/banner_test.go +++ b/core/banner/banner_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/click33/sa-token-go/core/config" + "github.com/click33/sa-token-go/core/version" ) // captureOutput captures stdout output for testing @@ -37,8 +38,8 @@ func TestPrint(t *testing.T) { t.Error("Output should contain 'Sa-Token-Go'") } - if !strings.Contains(output, Version) { - t.Errorf("Output should contain version %s", Version) + if !strings.Contains(output, version.Version) { + t.Errorf("Output should contain version %s", version.Version) } if !strings.Contains(output, "Go Version") { @@ -206,7 +207,7 @@ func TestPrintWithConfig(t *testing.T) { contains: []string{ "Configuration", "Token Name", - "sa-token", + "satoken", "Token Style", "uuid", "Token Timeout", @@ -215,10 +216,11 @@ func TestPrintWithConfig(t *testing.T) { "Concurrent", "Share Token", "Max Login Count", - "Read From Header", - "Read From Cookie", - "Read From Body", - "Logging", + "Read From", + "Header", + "Cookie MaxAge", + "Cookie Secure", + "Cookie HttpOnly", }, }, { @@ -248,11 +250,9 @@ func TestPrintWithConfig(t *testing.T) { "jwt-token", "jwt", "3600 seconds", - "JWT Secret", + "JWT Secret Key", "*** (configured)", - "Cookie Path", - "/api", - "Cookie SameSite", + "Cookie MaxAge", "Cookie HttpOnly", "Cookie Secure", }, @@ -283,8 +283,8 @@ func TestPrintWithConfig(t *testing.T) { CookieConfig: &config.CookieConfig{}, }, contains: []string{ - "JWT Secret", - "Not Set", + "JWT Secret Key", + "*** (configured)", }, }, } diff --git a/core/satoken.go b/core/satoken.go index fff2046..64aff31 100644 --- a/core/satoken.go +++ b/core/satoken.go @@ -14,10 +14,11 @@ import ( "github.com/click33/sa-token-go/core/session" "github.com/click33/sa-token-go/core/token" "github.com/click33/sa-token-go/core/utils" + "github.com/click33/sa-token-go/core/version" ) // Version Sa-Token-Go version | Sa-Token-Go版本 -const Version = "0.1.3" +const Version = version.Version // ============ Exported Types | 导出的类型 ============ // Export main types and functions for external use | 导出主要类型和函数,方便外部使用 diff --git a/core/version/version.go b/core/version/version.go new file mode 100644 index 0000000..f6ebaa6 --- /dev/null +++ b/core/version/version.go @@ -0,0 +1,7 @@ +package version + +// Version system level version number | 系统级版本号 +// This is the global version of Sa-Token-Go, modify this value to update the version across the entire project +// 这是 Sa-Token-Go 的全局版本号,修改此值可更新整个项目的版本 +const Version = "0.1.5" + -- Gitee From 22791966ae0b4900bd9251ee85ffa6a83eda79d6 Mon Sep 17 00:00:00 2001 From: Moling <1970115881@qq.com> Date: Mon, 8 Dec 2025 22:22:44 +0800 Subject: [PATCH 02/28] =?UTF-8?q?=E6=9B=B4=E6=96=B0README=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=9C=80=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E4=BE=9D=E8=B5=96=E9=A1=B9=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DCustomChecker=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=8F=82=E6=95=B0=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84HTTP=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=A0=81=E8=BD=AC=E6=8D=A2=E5=87=BD=E6=95=B0=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0Go=E6=A8=A1=E5=9D=97=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- README_zh.md | 4 ++-- integrations/kratos/checker_test.go | 4 ++-- integrations/kratos/options.go | 2 +- stputil/go.mod | 4 ++-- stputil/go.sum | 6 ++++++ 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 538c8e3..3b66ac9 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ go get github.com/click33/sa-token-go/storage/redis@latest # Redis storage (pro ```bash # Core modules -go get github.com/click33/sa-token-go/core@v0.1.5 -go get github.com/click33/sa-token-go/stputil@v0.1.5 +go get github.com/click33/sa-token-go/core@vlatest +go get github.com/click33/sa-token-go/stputil@vlatest # Storage module (choose one) go get github.com/click33/sa-token-go/storage/memory@latest # Memory storage (dev) diff --git a/README_zh.md b/README_zh.md index c4cb6de..509f5f7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -55,8 +55,8 @@ go get github.com/click33/sa-token-go/storage/redis@latest # Redis存储(生 ```bash # 核心模块 -go get github.com/click33/sa-token-go/core@v0.1.5 -go get github.com/click33/sa-token-go/stputil@v0.1.5 +go get github.com/click33/sa-token-go/core@vlatest +go get github.com/click33/sa-token-go/stputil@vlatest # 存储模块(选一个) go get github.com/click33/sa-token-go/storage/memory@latest # 内存存储(开发) diff --git a/integrations/kratos/checker_test.go b/integrations/kratos/checker_test.go index 7c231b1..2ab1d2b 100644 --- a/integrations/kratos/checker_test.go +++ b/integrations/kratos/checker_test.go @@ -259,7 +259,7 @@ func TestCustomChecker(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - checker := &CustomChecker{name: "custom", fn: tt.fn} + checker := &CustomChecker{fn: tt.fn} err := checker.Check(ctx, mgr, loginID) if (err != nil) != tt.wantErr { t.Errorf("CustomChecker.Check() error = %v, wantErr %v", err, tt.wantErr) @@ -380,7 +380,7 @@ func TestCheckerConstructors(t *testing.T) { } // Test NewCustomChecker - if c := NewCustomChecker("test", func(ctx context.Context, manager *manager.Manager, loginID string) error { + if c := NewCustomChecker(func(ctx context.Context, manager *manager.Manager, loginID string) error { return nil }); c == nil { t.Error("NewCustomChecker() should return non-nil") diff --git a/integrations/kratos/options.go b/integrations/kratos/options.go index 971c0d8..b8e3f62 100644 --- a/integrations/kratos/options.go +++ b/integrations/kratos/options.go @@ -2,9 +2,9 @@ package kratos import ( "context" - "github.com/click33/sa-token-go/core" "net/http" + "github.com/click33/sa-token-go/core" "github.com/go-kratos/kratos/v2/errors" ) diff --git a/stputil/go.mod b/stputil/go.mod index 9f31aa9..3f13e0a 100644 --- a/stputil/go.mod +++ b/stputil/go.mod @@ -5,10 +5,10 @@ go 1.23.0 require github.com/click33/sa-token-go/core v0.1.5 require ( - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/panjf2000/ants/v2 v2.11.3 // indirect - golang.org/x/sync v0.16.0 // indirect + golang.org/x/sync v0.19.0 // indirect ) replace github.com/click33/sa-token-go/core => ../core diff --git a/stputil/go.sum b/stputil/go.sum index dda2c2d..8c5cf6f 100644 --- a/stputil/go.sum +++ b/stputil/go.sum @@ -1,8 +1,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -- Gitee From 8265be4d6eeee50f4991168abadb6b6d3e51edd3 Mon Sep 17 00:00:00 2001 From: Zany2 <568562991@qq.com> Date: Tue, 9 Dec 2025 22:30:55 +0800 Subject: [PATCH 03/28] =?UTF-8?q?fix():=201.=E4=BF=AE=E6=94=B9=E7=BB=AD?= =?UTF-8?q?=E6=9C=9F=E9=98=88=E5=80=BC=E4=B8=8E=E8=BF=87=E6=9C=9F=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/builder/builder.go | 7 +++++-- core/config/config.go | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/builder/builder.go b/core/builder/builder.go index a06678d..a2827ab 100644 --- a/core/builder/builder.go +++ b/core/builder/builder.go @@ -311,9 +311,12 @@ func (b *Builder) Validate() error { return fmt.Errorf("MaxRefresh must be >= -1, got: %d", b.maxRefresh) } - // Check MaxRefresh does not exceed Timeout + // Adjust MaxRefresh if it exceeds Timeout | 如果 MaxRefresh 大于 Timeout,则自动调整为 Timeout/2 if b.timeout != config.NoLimit && b.maxRefresh > b.timeout { - return fmt.Errorf("MaxRefresh (%d) cannot be greater than Timeout (%d)", b.maxRefresh, b.timeout) + b.maxRefresh = b.timeout / 2 + if b.maxRefresh < 1 { + b.maxRefresh = 1 + } } // Check RenewInterval diff --git a/core/config/config.go b/core/config/config.go index 56ccad6..a6cde61 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -210,9 +210,12 @@ func (c *Config) Validate() error { return fmt.Errorf("MaxRefresh must be >= -1, got: %d", c.MaxRefresh) } - // Check MaxRefresh does not exceed Timeout + // Adjust MaxRefresh if it exceeds Timeout | 如果 MaxRefresh 大于 Timeout,则自动调整为 Timeout/2 if c.Timeout != NoLimit && c.MaxRefresh > c.Timeout { - return fmt.Errorf("MaxRefresh (%d) cannot be greater than Timeout (%d)", c.MaxRefresh, c.Timeout) + c.MaxRefresh = c.Timeout / 2 + if c.MaxRefresh < 1 { + c.MaxRefresh = 1 + } } // Check RenewInterval -- Gitee From 7f46eaf7b015868b3b19ea61319e6d785ce3c90d Mon Sep 17 00:00:00 2001 From: c <23@g> Date: Wed, 17 Dec 2025 13:18:33 +0700 Subject: [PATCH 04/28] chore: upgrade version to v0.1.6 - Add path-based authentication feature - Support Ant-style wildcard patterns - Integrate path auth into all framework integrations - Update documentation with detailed usage examples --- README.md | 4 +- README_zh.md | 4 +- core/errors.go | 24 ++ core/router/router.go | 192 +++++++++++ core/satoken.go | 10 + docs/.DS_Store | Bin 0 -> 6148 bytes docs/IMG_3976.JPG | Bin 255434 -> 0 bytes docs/guide/path-auth.md | 551 +++++++++++++++++++++++++++++++ docs/guide/path-auth_zh.md | 551 +++++++++++++++++++++++++++++++ integrations/chi/annotation.go | 5 +- integrations/chi/go.mod | 4 +- integrations/chi/plugin.go | 32 ++ integrations/echo/annotation.go | 5 +- integrations/echo/go.mod | 4 +- integrations/echo/plugin.go | 31 ++ integrations/fiber/annotation.go | 5 +- integrations/fiber/go.mod | 4 +- integrations/fiber/plugin.go | 26 ++ integrations/gf/annotation.go | 5 +- integrations/gf/go.mod | 4 +- integrations/gf/plugin.go | 27 ++ integrations/gin/annotation.go | 11 +- integrations/gin/go.mod | 4 +- integrations/gin/plugin.go | 28 ++ integrations/kratos/go.mod | 6 +- integrations/kratos/plugin.go | 44 ++- storage/memory/go.mod | 2 +- storage/redis/go.mod | 2 +- stputil/go.mod | 2 +- 29 files changed, 1543 insertions(+), 44 deletions(-) create mode 100644 core/router/router.go create mode 100644 docs/.DS_Store delete mode 100644 docs/IMG_3976.JPG create mode 100644 docs/guide/path-auth.md create mode 100644 docs/guide/path-auth_zh.md diff --git a/README.md b/README.md index 3b66ac9..35915d4 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A lightweight, high-performance Go authentication and authorization framework, i - 🔐 **Authentication** - Multi-device login, Token management - 🛡️ **Authorization** - Fine-grained permission control, wildcard support (`*`, `user:*`, `user:*:view`) +- 🛣️ **Path-Based Auth** - Flexible path-based authentication with Ant-style wildcards - 👥 **Role Management** - Flexible role authorization mechanism - 🚫 **Account Ban** - Temporary/permanent account disabling - 👢 **Kickout** - Force user logout, multi-device mutual exclusion @@ -105,7 +106,7 @@ func init() { ___/ / /_/ / / / / /_/ / ,< / __/ / / /_____/ /_/ / /_/ / /____/\__,_/ /_/ \____/_/|_|\___/_/ /_/ \____/\____/ -:: Sa-Token-Go :: (v0.1.5) +:: Sa-Token-Go :: (v0.1.6) :: Go Version :: go1.21.0 :: GOOS/GOARCH :: linux/amd64 @@ -575,6 +576,7 @@ sa-token-go/ - [Quick Start](docs/tutorial/quick-start.md) - Get started in 5 minutes - [Authentication](docs/guide/authentication.md) - Authentication guide +- [Path-Based Auth](docs/guide/path-auth.md) - Path-based authentication guide - [Permission](docs/guide/permission.md) - Permission system - [Annotations](docs/guide/annotation.md) - Decorator pattern guide - [Event Listener](docs/guide/listener.md) - Event system guide diff --git a/README_zh.md b/README_zh.md index 509f5f7..1e9bbaa 100644 --- a/README_zh.md +++ b/README_zh.md @@ -11,6 +11,7 @@ - 🔐 **登录认证** - 支持多设备登录、Token管理 - 🛡️ **权限验证** - 细粒度权限控制、通配符支持(`*`, `user:*`, `user:*:view`) +- 🛣️ **路径鉴权** - 灵活的路径鉴权、支持Ant风格通配符 - 👥 **角色管理** - 灵活的角色授权机制 - 🚫 **账号封禁** - 临时/永久封禁功能 - 👢 **踢人下线** - 强制用户下线、多端互斥登录 @@ -105,7 +106,7 @@ func init() { ___/ / /_/ / / / / /_/ / ,< / __/ / / /_____/ /_/ / /_/ / /____/\__,_/ /_/ \____/_/|_|\___/_/ /_/ \____/\____/ -:: Sa-Token-Go :: (v0.1.5) +:: Sa-Token-Go :: (v0.1.6) :: Go Version :: go1.21.0 :: GOOS/GOARCH :: linux/amd64 @@ -583,6 +584,7 @@ sa-token-go/ - [快速开始](docs/tutorial/quick-start_zh.md) - 5分钟上手 - [登录认证](docs/guide/authentication_zh.md) - 登录认证详解 +- [路径鉴权](docs/guide/path-auth_zh.md) - 路径鉴权详解 - [权限验证](docs/guide/permission_zh.md) - 权限系统详解 - [注解使用](docs/guide/annotation_zh.md) - 装饰器模式详解 - [事件监听](docs/guide/listener_zh.md) - 事件系统详解 diff --git a/core/errors.go b/core/errors.go index 0a80c2d..428deb2 100644 --- a/core/errors.go +++ b/core/errors.go @@ -63,6 +63,16 @@ var ( ErrMaxLoginCount = fmt.Errorf("max login limit: maximum number of concurrent logins reached") ) +// ============ Path Authentication Errors | 路径鉴权错误 ============ + +var ( + // ErrPathAuthRequired indicates path authentication is required | 路径需要鉴权 + ErrPathAuthRequired = fmt.Errorf("path authentication required: this path requires authentication") + + // ErrPathNotAllowed indicates path is not allowed | 路径不允许访问 + ErrPathNotAllowed = fmt.Errorf("path not allowed: access to this path is forbidden") +) + // ============ System Errors | 系统错误 ============ var ( @@ -165,6 +175,18 @@ func NewAccountDisabledError(loginID string) *SaTokenError { WithContext("loginID", loginID) } +// NewPathAuthRequiredError Creates a path authentication required error | 创建路径需要鉴权错误 +func NewPathAuthRequiredError(path string) *SaTokenError { + return NewError(CodePathAuthRequired, "path authentication required", ErrPathAuthRequired). + WithContext("path", path) +} + +// NewPathNotAllowedError Creates a path not allowed error | 创建路径不允许访问错误 +func NewPathNotAllowedError(path string) *SaTokenError { + return NewError(CodePathNotAllowed, "path not allowed", ErrPathNotAllowed). + WithContext("path", path) +} + // ============ Error Checking Helpers | 错误检查辅助函数 ============ // IsNotLoginError Checks if error is a not login error | 检查是否为未登录错误 @@ -204,6 +226,8 @@ const ( CodeBadRequest = 400 // Bad request | 错误的请求 CodeNotLogin = 401 // Not authenticated | 未认证 CodePermissionDenied = 403 // Permission denied | 权限不足 + CodePathAuthRequired = 401 // Path authentication required | 路径需要鉴权 + CodePathNotAllowed = 403 // Path not allowed | 路径不允许访问 CodeNotFound = 404 // Resource not found | 资源未找到 CodeServerError = 500 // Internal server error | 服务器内部错误 diff --git a/core/router/router.go b/core/router/router.go new file mode 100644 index 0000000..21eaff4 --- /dev/null +++ b/core/router/router.go @@ -0,0 +1,192 @@ +package router + +import ( + "strings" + "github.com/click33/sa-token-go/core/manager" +) + +// MatchPath matches a path against a pattern (Ant-style wildcard) | 匹配路径与模式(Ant风格通配符) +// Supported patterns: +// - "/**": Match all paths | 匹配所有路径 +// - "/api/**": Match all paths starting with "/api/" | 匹配所有以"/api/"开头的路径 +// - "/api/*": Match single-level paths under "/api/" | 匹配"/api/"下的单级路径 +// - "*.html": Match paths ending with ".html" | 匹配以".html"结尾的路径 +// - "/exact": Exact match | 精确匹配 +func MatchPath(path, pattern string) bool { + if pattern == "/**" { + return true + } + + if strings.HasSuffix(pattern, "/**") { + prefix := pattern[:len(pattern)-3] + return strings.HasPrefix(path, prefix) + } + + if strings.HasPrefix(pattern, "*") { + suffix := pattern[1:] + return strings.HasSuffix(path, suffix) + } + + if strings.HasSuffix(pattern, "/*") { + prefix := pattern[:len(pattern)-2] + if strings.HasPrefix(path, prefix) { + suffix := path[len(prefix):] + if suffix == "" || suffix == "/" { + return true + } + return !strings.Contains(suffix[1:], "/") + } + return false + } + + return path == pattern +} + +// MatchAny checks if path matches any pattern in the list | 检查路径是否匹配列表中的任意模式 +func MatchAny(path string, patterns []string) bool { + for _, pattern := range patterns { + if MatchPath(path, pattern) { + return true + } + } + return false +} + +// NeedAuth determines if authentication is needed for a path | 判断路径是否需要鉴权 +// Returns true if path matches include patterns but not exclude patterns | 如果路径匹配包含模式但不匹配排除模式,返回true +func NeedAuth(path string, include, exclude []string) bool { + return MatchAny(path, include) && !MatchAny(path, exclude) +} + +// PathAuthConfig path-based authentication configuration | 基于路径的鉴权配置 +// Configure which paths require authentication and which are excluded | 配置哪些路径需要鉴权,哪些路径被排除 +type PathAuthConfig struct { + // Include paths that require authentication (include patterns) | 需要鉴权的路径(包含模式) + Include []string + // Exclude paths excluded from authentication (exclude patterns) | 排除鉴权的路径(排除模式) + Exclude []string + // Validator optional login ID validator function | 可选的登录ID验证函数 + Validator func(loginID string) bool +} + +// NewPathAuthConfig creates a new path authentication configuration | 创建新的路径鉴权配置 +func NewPathAuthConfig() *PathAuthConfig { + return &PathAuthConfig{ + Include: []string{}, + Exclude: []string{}, + Validator: nil, + } +} + +// SetInclude sets paths that require authentication | 设置需要鉴权的路径 +func (c *PathAuthConfig) SetInclude(patterns []string) *PathAuthConfig { + c.Include = patterns + return c +} + +// SetExclude sets paths excluded from authentication | 设置排除鉴权的路径 +func (c *PathAuthConfig) SetExclude(patterns []string) *PathAuthConfig { + c.Exclude = patterns + return c +} + +// SetValidator sets a custom login ID validator function | 设置自定义的登录ID验证函数 +func (c *PathAuthConfig) SetValidator(validator func(loginID string) bool) *PathAuthConfig { + c.Validator = validator + return c +} + +// Check checks if a path requires authentication | 检查路径是否需要鉴权 +func (c *PathAuthConfig) Check(path string) bool { + return NeedAuth(path, c.Include, c.Exclude) +} + +// ValidateLoginID validates a login ID using the configured validator | 使用配置的验证器验证登录ID +func (c *PathAuthConfig) ValidateLoginID(loginID string) bool { + if c.Validator == nil { + return true + } + return c.Validator(loginID) +} + +// AuthResult authentication result after processing | 处理后的鉴权结果 +type AuthResult struct { + // NeedAuth whether authentication is required for this path | 此路径是否需要鉴权 + NeedAuth bool + // Token extracted token value | 提取的token值 + Token string + // TokenInfo token information if valid | 如果有效则包含token信息 + TokenInfo *manager.TokenInfo + // IsValid whether the token is valid | token是否有效 + IsValid bool +} + +// ShouldReject checks if the request should be rejected | 检查请求是否应该被拒绝 +func (r *AuthResult) ShouldReject() bool { + return r.NeedAuth && (!r.IsValid || r.Token == "") +} + +// LoginID gets the login ID from token info | 从token信息中获取登录ID +func (r *AuthResult) LoginID() string { + if r.TokenInfo != nil { + return r.TokenInfo.LoginID + } + return "" +} + +// ProcessAuth processes authentication for a request path | 处理请求路径的鉴权 +// This function checks if the path requires authentication, validates the token, +// and returns an AuthResult with all relevant information | 此函数检查路径是否需要鉴权,验证token,并返回包含所有相关信息的AuthResult +func ProcessAuth(path, tokenStr string, config *PathAuthConfig, mgr *manager.Manager) *AuthResult { + needAuth := config.Check(path) + + token := tokenStr + isValid := false + var tokenInfo *manager.TokenInfo + + if token != "" { + isValid = mgr.IsLogin(token) + if isValid { + info, err := mgr.GetTokenInfo(token) + if err == nil && info != nil { + tokenInfo = info + if needAuth && config.Validator != nil { + isValid = config.ValidateLoginID(tokenInfo.LoginID) + } + } + } + } + + return &AuthResult{ + NeedAuth: needAuth, + Token: token, + TokenInfo: tokenInfo, + IsValid: isValid, + } +} + +// PathAuthHandler interface for path authentication handlers | 路径鉴权处理器接口 +type PathAuthHandler interface { + GetPath() string + GetToken() string + GetManager() *manager.Manager + GetPathAuthConfig() *PathAuthConfig +} + +// CheckPathAuth checks path authentication using the handler interface | 使用处理器接口检查路径鉴权 +// Returns true if authentication is required and should be rejected | 如果需要鉴权且应该被拒绝,返回true +func CheckPathAuth(handler PathAuthHandler) bool { + path := handler.GetPath() + token := handler.GetToken() + manager := handler.GetManager() + config := handler.GetPathAuthConfig() + + if config == nil { + config = NewPathAuthConfig().SetInclude([]string{"/**"}) + } + + result := ProcessAuth(path, token, config, manager) + + return result.ShouldReject() +} + diff --git a/core/satoken.go b/core/satoken.go index 64aff31..851dcec 100644 --- a/core/satoken.go +++ b/core/satoken.go @@ -10,6 +10,7 @@ import ( "github.com/click33/sa-token-go/core/listener" "github.com/click33/sa-token-go/core/manager" "github.com/click33/sa-token-go/core/oauth2" + "github.com/click33/sa-token-go/core/router" "github.com/click33/sa-token-go/core/security" "github.com/click33/sa-token-go/core/session" "github.com/click33/sa-token-go/core/token" @@ -58,6 +59,8 @@ type ( OAuth2Client = oauth2.Client OAuth2AccessToken = oauth2.AccessToken OAuth2GrantType = oauth2.GrantType + PathAuthConfig = router.PathAuthConfig + AuthResult = router.AuthResult ) // Adapter interfaces | 适配器接口 @@ -120,6 +123,13 @@ var ( // Pattern matching | 模式匹配 MatchPattern = utils.MatchPattern + // Router utilities | 路由工具 + MatchPath = router.MatchPath + MatchAny = router.MatchAny + NeedAuth = router.NeedAuth + ProcessAuth = router.ProcessAuth + NewPathAuthConfig = router.NewPathAuthConfig + // Duration utilities | 时长工具 FormatDuration = utils.FormatDuration ParseDuration = utils.ParseDuration diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 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 0 && annotations[0].Ignore { if handler != nil { handler.ServeHTTP(w, r) @@ -28,22 +27,20 @@ func GetHandler(handler http.Handler, annotations ...*Annotation) http.Handler { return } - // Get token from context using configured TokenName | 从上下文获取Token(使用配置的TokenName) ctx := NewChiContext(w, r) saCtx := core.NewContext(ctx, stputil.GetManager()) token := saCtx.GetTokenValue() + if token == "" { writeErrorResponse(w, core.NewNotLoginError()) return } - // Check login | 检查登录 if !stputil.IsLogin(token) { writeErrorResponse(w, core.NewNotLoginError()) return } - // Get login ID | 获取登录ID loginID, err := stputil.GetLoginID(token) if err != nil { writeErrorResponse(w, err) diff --git a/integrations/chi/go.mod b/integrations/chi/go.mod index 7ef3a25..b497355 100644 --- a/integrations/chi/go.mod +++ b/integrations/chi/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/integrations/chi go 1.23.0 require ( - github.com/click33/sa-token-go/core v0.1.5 - github.com/click33/sa-token-go/stputil v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 ) require ( diff --git a/integrations/chi/plugin.go b/integrations/chi/plugin.go index 7fdab88..33f6e38 100644 --- a/integrations/chi/plugin.go +++ b/integrations/chi/plugin.go @@ -39,6 +39,38 @@ func (p *Plugin) AuthMiddleware() func(http.Handler) http.Handler { } } +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + token := r.Header.Get(p.manager.GetConfig().TokenName) + if token == "" { + cookie, _ := r.Cookie(p.manager.GetConfig().TokenName) + if cookie != nil { + token = cookie.Value + } + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + writeErrorResponse(w, core.NewPathAuthRequiredError(path)) + return + } + + if result.IsValid && result.TokenInfo != nil { + ctx := NewChiContext(w, r) + saCtx := core.NewContext(ctx, p.manager) + ctx.Set("satoken", saCtx) + ctx.Set("loginID", result.LoginID()) + } + + next.ServeHTTP(w, r) + }) + } +} + // PermissionRequired permission validation middleware | 权限验证中间件 func (p *Plugin) PermissionRequired(permission string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { diff --git a/integrations/echo/annotation.go b/integrations/echo/annotation.go index a8c9804..804471b 100644 --- a/integrations/echo/annotation.go +++ b/integrations/echo/annotation.go @@ -20,7 +20,6 @@ type Annotation struct { // GetHandler gets handler with annotations | 获取带注解的处理器 func GetHandler(handler echo.HandlerFunc, annotations ...*Annotation) echo.HandlerFunc { return func(c echo.Context) error { - // Check if authentication should be ignored | 检查是否忽略认证 if len(annotations) > 0 && annotations[0].Ignore { if handler != nil { return handler(c) @@ -28,20 +27,18 @@ func GetHandler(handler echo.HandlerFunc, annotations ...*Annotation) echo.Handl return nil } - // Get token from context using configured TokenName | 从上下文获取Token(使用配置的TokenName) ctx := NewEchoContext(c) saCtx := core.NewContext(ctx, stputil.GetManager()) token := saCtx.GetTokenValue() + if token == "" { return writeErrorResponse(c, core.NewNotLoginError()) } - // Check login | 检查登录 if !stputil.IsLogin(token) { return writeErrorResponse(c, core.NewNotLoginError()) } - // Get login ID | 获取登录ID loginID, err := stputil.GetLoginID(token) if err != nil { return writeErrorResponse(c, err) diff --git a/integrations/echo/go.mod b/integrations/echo/go.mod index ae7ee59..93cf344 100644 --- a/integrations/echo/go.mod +++ b/integrations/echo/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.5 - github.com/click33/sa-token-go/stputil v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 github.com/labstack/echo/v4 v4.11.4 ) diff --git a/integrations/echo/plugin.go b/integrations/echo/plugin.go index 50299c8..35fe69b 100644 --- a/integrations/echo/plugin.go +++ b/integrations/echo/plugin.go @@ -37,6 +37,37 @@ func (p *Plugin) AuthMiddleware() echo.MiddlewareFunc { } } +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + path := c.Request().URL.Path + token := c.Request().Header.Get(p.manager.GetConfig().TokenName) + if token == "" { + cookie, _ := c.Cookie(p.manager.GetConfig().TokenName) + if cookie != nil { + token = cookie.Value + } + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + return writeErrorResponse(c, core.NewPathAuthRequiredError(path)) + } + + if result.IsValid && result.TokenInfo != nil { + ctx := NewEchoContext(c) + saCtx := core.NewContext(ctx, p.manager) + c.Set("satoken", saCtx) + c.Set("loginID", result.LoginID()) + } + + return next(c) + } + } +} + // PermissionRequired permission validation middleware | 权限验证中间件 func (p *Plugin) PermissionRequired(permission string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { diff --git a/integrations/fiber/annotation.go b/integrations/fiber/annotation.go index 0182710..ed3ca62 100644 --- a/integrations/fiber/annotation.go +++ b/integrations/fiber/annotation.go @@ -20,7 +20,6 @@ type Annotation struct { // GetHandler gets handler with annotations | 获取带注解的处理器 func GetHandler(handler fiber.Handler, annotations ...*Annotation) fiber.Handler { return func(c *fiber.Ctx) error { - // Check if authentication should be ignored | 检查是否忽略认证 if len(annotations) > 0 && annotations[0].Ignore { if handler != nil { return handler(c) @@ -28,20 +27,18 @@ func GetHandler(handler fiber.Handler, annotations ...*Annotation) fiber.Handler return c.Next() } - // Get token from context using configured TokenName | 从上下文获取Token(使用配置的TokenName) ctx := NewFiberContext(c) saCtx := core.NewContext(ctx, stputil.GetManager()) token := saCtx.GetTokenValue() + if token == "" { return writeErrorResponse(c, core.NewNotLoginError()) } - // Check login | 检查登录 if !stputil.IsLogin(token) { return writeErrorResponse(c, core.NewNotLoginError()) } - // Get login ID | 获取登录ID loginID, err := stputil.GetLoginID(token) if err != nil { return writeErrorResponse(c, err) diff --git a/integrations/fiber/go.mod b/integrations/fiber/go.mod index abb4115..c5d004b 100644 --- a/integrations/fiber/go.mod +++ b/integrations/fiber/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.5 - github.com/click33/sa-token-go/stputil v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 github.com/gofiber/fiber/v2 v2.52.0 ) diff --git a/integrations/fiber/plugin.go b/integrations/fiber/plugin.go index 7aa193b..cf2e128 100644 --- a/integrations/fiber/plugin.go +++ b/integrations/fiber/plugin.go @@ -34,6 +34,32 @@ func (p *Plugin) AuthMiddleware() fiber.Handler { } } +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) fiber.Handler { + return func(c *fiber.Ctx) error { + path := c.Path() + token := c.Get(p.manager.GetConfig().TokenName) + if token == "" { + token = c.Cookies(p.manager.GetConfig().TokenName) + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + return writeErrorResponse(c, core.NewPathAuthRequiredError(path)) + } + + if result.IsValid && result.TokenInfo != nil { + ctx := NewFiberContext(c) + saCtx := core.NewContext(ctx, p.manager) + c.Locals("satoken", saCtx) + c.Locals("loginID", result.LoginID()) + } + + return c.Next() + } +} + // PermissionRequired permission validation middleware | 权限验证中间件 func (p *Plugin) PermissionRequired(permission string) fiber.Handler { return func(c *fiber.Ctx) error { diff --git a/integrations/gf/annotation.go b/integrations/gf/annotation.go index d1e0630..379a273 100644 --- a/integrations/gf/annotation.go +++ b/integrations/gf/annotation.go @@ -20,7 +20,6 @@ type Annotation struct { // GetHandler gets handler with annotations | 获取带注解的处理器 func GetHandler(handler ghttp.HandlerFunc, annotations ...*Annotation) ghttp.HandlerFunc { return func(r *ghttp.Request) { - // Check if authentication should be ignored | 检查是否忽略认证 if len(annotations) > 0 && annotations[0].Ignore { if handler != nil { handler(r) @@ -30,22 +29,20 @@ func GetHandler(handler ghttp.HandlerFunc, annotations ...*Annotation) ghttp.Han return } - // Get token from context using configured TokenName | 从上下文获取Token(使用配置的TokenName) ctx := NewGFContext(r) saCtx := core.NewContext(ctx, stputil.GetManager()) token := saCtx.GetTokenValue() + if token == "" { writeErrorResponse(r, core.NewNotLoginError()) return } - // Check login | 检查登录 if !stputil.IsLogin(token) { writeErrorResponse(r, core.NewNotLoginError()) return } - // Get login ID | 获取登录ID loginID, err := stputil.GetLoginID(token) if err != nil { writeErrorResponse(r, err) diff --git a/integrations/gf/go.mod b/integrations/gf/go.mod index 340f21f..2b3fbd2 100644 --- a/integrations/gf/go.mod +++ b/integrations/gf/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/integrations/gf go 1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.5 - github.com/click33/sa-token-go/stputil v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 github.com/gogf/gf/v2 v2.9.4 ) diff --git a/integrations/gf/plugin.go b/integrations/gf/plugin.go index d9d18db..78b51e7 100644 --- a/integrations/gf/plugin.go +++ b/integrations/gf/plugin.go @@ -47,6 +47,33 @@ func (p *Plugin) AuthMiddleware() ghttp.HandlerFunc { } +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) ghttp.HandlerFunc { + return func(r *ghttp.Request) { + path := r.Request.URL.Path + token := r.Header.Get(p.manager.GetConfig().TokenName) + if token == "" { + token = r.Cookie.Get(p.manager.GetConfig().TokenName).String() + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + writeErrorResponse(r, core.NewPathAuthRequiredError(path)) + return + } + + if result.IsValid && result.TokenInfo != nil { + ctx := NewGFContext(r) + saCtx := core.NewContext(ctx, p.manager) + r.SetCtxVar("satoken", saCtx) + r.SetCtxVar("loginID", result.LoginID()) + } + + r.Middleware.Next() + } +} + // PermissionRequired permission validation middleware | 权限验证中间件 func (p *Plugin) PermissionRequired(permission string) ghttp.HandlerFunc { return func(r *ghttp.Request) { diff --git a/integrations/gin/annotation.go b/integrations/gin/annotation.go index cc7a1dc..3b8d4f4 100644 --- a/integrations/gin/annotation.go +++ b/integrations/gin/annotation.go @@ -90,7 +90,6 @@ func (a *Annotation) Validate() bool { // GetHandler gets handler with annotations | 获取带注解的处理器 func GetHandler(handler interface{}, annotations ...*Annotation) ginfw.HandlerFunc { return func(c *ginfw.Context) { - // Check if authentication should be ignored | 检查是否忽略认证 if len(annotations) > 0 && annotations[0].Ignore { if callHandler(handler, c) { return @@ -99,24 +98,22 @@ func GetHandler(handler interface{}, annotations ...*Annotation) ginfw.HandlerFu return } - // Get token from context using configured TokenName | 从上下文获取Token(使用配置的TokenName) ctx := NewGinContext(c) saCtx := core.NewContext(ctx, stputil.GetManager()) token := saCtx.GetTokenValue() + if token == "" { writeErrorResponse(c, core.NewNotLoginError()) c.Abort() return } - // Check login | 检查登录 if !stputil.IsLogin(token) { writeErrorResponse(c, core.NewNotLoginError()) c.Abort() return } - // Get login ID | 获取登录ID loginID, err := stputil.GetLoginID(token) if err != nil { writeErrorResponse(c, err) @@ -289,31 +286,27 @@ func (h *HandlerWithAnnotations) ToGinHandler() ginfw.HandlerFunc { // Middleware 创建中间件版本 func Middleware(annotations ...*Annotation) ginfw.HandlerFunc { return func(c *ginfw.Context) { - - // 检查是否忽略认证 if len(annotations) > 0 && annotations[0].Ignore { c.Next() return } - // 获取Token(使用配置的TokenName) ctx := NewGinContext(c) saCtx := core.NewContext(ctx, stputil.GetManager()) token := saCtx.GetTokenValue() + if token == "" { writeErrorResponse(c, core.NewNotLoginError()) c.Abort() return } - // 检查登录 if !stputil.IsLogin(token) { writeErrorResponse(c, core.NewNotLoginError()) c.Abort() return } - // 获取登录ID loginID, err := stputil.GetLoginID(token) if err != nil { writeErrorResponse(c, err) diff --git a/integrations/gin/go.mod b/integrations/gin/go.mod index 3b8d3d3..f4b13a1 100644 --- a/integrations/gin/go.mod +++ b/integrations/gin/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.5 - github.com/click33/sa-token-go/stputil v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 github.com/gin-gonic/gin v1.10.0 github.com/stretchr/testify v1.11.1 ) diff --git a/integrations/gin/plugin.go b/integrations/gin/plugin.go index d5e3f29..8d084f7 100644 --- a/integrations/gin/plugin.go +++ b/integrations/gin/plugin.go @@ -39,6 +39,34 @@ func (p *Plugin) AuthMiddleware() gin.HandlerFunc { } } +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) gin.HandlerFunc { + return func(c *gin.Context) { + path := c.Request.URL.Path + token := c.GetHeader(p.manager.GetConfig().TokenName) + if token == "" { + token, _ = c.Cookie(p.manager.GetConfig().TokenName) + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + writeErrorResponse(c, core.NewPathAuthRequiredError(path)) + c.Abort() + return + } + + if result.IsValid && result.TokenInfo != nil { + ctx := NewGinContext(c) + saCtx := core.NewContext(ctx, p.manager) + c.Set("satoken", saCtx) + c.Set("loginID", result.LoginID()) + } + + c.Next() + } +} + // PermissionRequired permission validation middleware | 权限验证中间件 func (p *Plugin) PermissionRequired(permission string) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/integrations/kratos/go.mod b/integrations/kratos/go.mod index ee423cc..47ecfe7 100644 --- a/integrations/kratos/go.mod +++ b/integrations/kratos/go.mod @@ -5,9 +5,9 @@ go 1.24.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.5 - github.com/click33/sa-token-go/storage/memory v0.1.5 - github.com/click33/sa-token-go/stputil v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/storage/memory v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 github.com/go-kratos/kratos/v2 v2.9.1 ) diff --git a/integrations/kratos/plugin.go b/integrations/kratos/plugin.go index 76db2c4..b28332f 100644 --- a/integrations/kratos/plugin.go +++ b/integrations/kratos/plugin.go @@ -2,6 +2,7 @@ package kratos import ( "context" + "net/http" "sort" "strings" @@ -49,7 +50,6 @@ func (e *Plugin) Server() middleware.Middleware { return func(ctx context.Context, req interface{}) (reply interface{}, err error) { info, ok := transport.FromServerContext(ctx) if !ok { - // 无法获取传输层信息,直接放行 return handler(ctx, req) } @@ -90,6 +90,48 @@ func (e *Plugin) Server() middleware.Middleware { } } +// PathAuthMiddleware 基于路径的鉴权中间件 +// 使用 Ant 风格通配符进行路径匹配 +func (e *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (reply interface{}, err error) { + info, ok := transport.FromServerContext(ctx) + if !ok { + return handler(ctx, req) + } + + // 获取实际的 HTTP 路径 + var path string + if htr, ok := info.(interface{ Request() *http.Request }); ok { + path = htr.Request().URL.Path + } else { + // 如果无法获取路径,使用 operation 作为后备 + path = info.Operation() + } + + // 获取 token + kratosContext := NewKratosContext(ctx) + saCtx := core.NewContext(kratosContext, e.manager) + token := saCtx.GetTokenValue() + + // 处理路径鉴权 + result := core.ProcessAuth(path, token, config, e.manager) + + if result.ShouldReject() { + return nil, e.options.ErrorHandler(ctx, core.NewPathAuthRequiredError(path)) + } + + // 如果 token 有效,将相关信息存储到 context + if result.IsValid && result.TokenInfo != nil { + ctx = context.WithValue(ctx, "satoken", saCtx) + ctx = context.WithValue(ctx, "loginID", result.LoginID()) + } + + return handler(ctx, req) + } + } +} + // ========== 规则构建器 ========== // RuleBuilder 规则构建器(链式API) diff --git a/storage/memory/go.mod b/storage/memory/go.mod index ef4bdfc..12faa03 100644 --- a/storage/memory/go.mod +++ b/storage/memory/go.mod @@ -2,6 +2,6 @@ module github.com/click33/sa-token-go/storage/memory go 1.23.0 -require github.com/click33/sa-token-go/core v0.1.5 +require github.com/click33/sa-token-go/core v0.1.6 replace github.com/click33/sa-token-go/core => ../../core diff --git a/storage/redis/go.mod b/storage/redis/go.mod index 4901b81..6b9699f 100644 --- a/storage/redis/go.mod +++ b/storage/redis/go.mod @@ -3,7 +3,7 @@ module github.com/click33/sa-token-go/storage/redis go 1.23.0 require ( - github.com/click33/sa-token-go/core v0.1.5 + github.com/click33/sa-token-go/core v0.1.6 github.com/redis/go-redis/v9 v9.5.1 ) diff --git a/stputil/go.mod b/stputil/go.mod index 3f13e0a..b6e6cdb 100644 --- a/stputil/go.mod +++ b/stputil/go.mod @@ -2,7 +2,7 @@ module github.com/click33/sa-token-go/stputil go 1.23.0 -require github.com/click33/sa-token-go/core v0.1.5 +require github.com/click33/sa-token-go/core v0.1.6 require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect -- Gitee From 3835c6e2aa25539e223c545a810f9d4b818158c6 Mon Sep 17 00:00:00 2001 From: c <23@g> Date: Wed, 17 Dec 2025 13:24:03 +0700 Subject: [PATCH 05/28] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BE=A4=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=92=8C=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/version/version.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/version/version.go b/core/version/version.go index f6ebaa6..2297589 100644 --- a/core/version/version.go +++ b/core/version/version.go @@ -3,5 +3,4 @@ package version // Version system level version number | 系统级版本号 // This is the global version of Sa-Token-Go, modify this value to update the version across the entire project // 这是 Sa-Token-Go 的全局版本号,修改此值可更新整个项目的版本 -const Version = "0.1.5" - +const Version = "0.1.6" -- Gitee From 3002e7a11173b94493a08e55ebabe307fd6550f5 Mon Sep 17 00:00:00 2001 From: MoLing <1970115881@qq.com> Date: Wed, 17 Dec 2025 16:32:57 +0800 Subject: [PATCH 06/28] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E4=BB=A4=E7=89=8C=E5=A4=84=E7=90=86=E5=B9=B6=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A4=E7=89=8CloginID=E6=98=A0?= =?UTF-8?q?=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -删除了GenerateTokenAir和RefreshAccessToken方法中令牌loginID映射的保存。 -实现了将原始令牌存储值复制到新的访问令牌密钥,以维护JSON TokenInfo格式。 --- core/security/refresh_token.go | 18 ++--- go.work.sum | 1 + stputil/stputil_test.go | 131 +++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 stputil/stputil_test.go 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)) +} + + -- Gitee From 7e6922d94059b618c5a480634015dedd6e684af8 Mon Sep 17 00:00:00 2001 From: QiNian Date: Sun, 28 Dec 2025 21:44:58 +0800 Subject: [PATCH 07/28] =?UTF-8?q?stplogic=20=E5=A4=9A=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../multi-certification/authkit/stpkit.go | 9 + examples/multi-certification/go.mod | 22 + examples/multi-certification/go.sum | 16 + examples/multi-certification/main.go | 52 +++ go.work | 1 + stputil/go.sum | 10 +- stputil/stplogic.go | 379 ++++++++++++++++++ stputil/stplogic_test.go | 254 ++++++++++++ stputil/stputil.go | 135 ++++--- 9 files changed, 817 insertions(+), 61 deletions(-) create mode 100644 examples/multi-certification/authkit/stpkit.go create mode 100644 examples/multi-certification/go.mod create mode 100644 examples/multi-certification/go.sum create mode 100644 examples/multi-certification/main.go create mode 100644 stputil/stplogic.go create mode 100644 stputil/stplogic_test.go diff --git a/examples/multi-certification/authkit/stpkit.go b/examples/multi-certification/authkit/stpkit.go new file mode 100644 index 0000000..4cad6be --- /dev/null +++ b/examples/multi-certification/authkit/stpkit.go @@ -0,0 +1,9 @@ +package authkit + +import "github.com/click33/sa-token-go/stputil" + +var ( + ADMIN *stputil.StpLogic + USER *stputil.StpLogic + OTHER *stputil.StpLogic +) diff --git a/examples/multi-certification/go.mod b/examples/multi-certification/go.mod new file mode 100644 index 0000000..9aa5fe7 --- /dev/null +++ b/examples/multi-certification/go.mod @@ -0,0 +1,22 @@ +module github.com/click33/sa-token-go/examples/multi-certification + +go 1.25.3 + +require ( + github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/storage/memory v0.1.6 + github.com/click33/sa-token-go/stputil v0.1.6 +) + +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/panjf2000/ants/v2 v2.11.3 // indirect + golang.org/x/sync v0.19.0 // indirect +) + +replace ( + github.com/click33/sa-token-go/core => ../../core + github.com/click33/sa-token-go/storage/memory => ../../storage/memory + github.com/click33/sa-token-go/stputil => ../../stputil +) diff --git a/examples/multi-certification/go.sum b/examples/multi-certification/go.sum new file mode 100644 index 0000000..99a4055 --- /dev/null +++ b/examples/multi-certification/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +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/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/multi-certification/main.go b/examples/multi-certification/main.go new file mode 100644 index 0000000..a959ade --- /dev/null +++ b/examples/multi-certification/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/examples/multi-certification/authkit" + "github.com/click33/sa-token-go/storage/memory" + "github.com/click33/sa-token-go/stputil" +) + +func main() { + // 注意 多认证体现需要将不同的manager的KeyPrefix设置为不同的值 + storage := memory.NewStorage() + userManager := core.NewBuilder(). + Storage(storage). + Timeout(6600). + IsPrintBanner(false). + KeyPrefix("user"). // 要唯一 + Build() + + adminManager := core.NewBuilder(). + Storage(storage). + Timeout(3600). + IsPrintBanner(false). + KeyPrefix("admin"). // 要唯一 + TokenStyle(core.TokenStyleTik). + Build() + + authkit.ADMIN = stputil.NewStpLogic(adminManager) + authkit.USER = stputil.NewStpLogic(userManager) + + Run() +} + +func Run() { + userTokenValue, _ := authkit.USER.Login("ID1") + adminTokenValue, _ := authkit.ADMIN.Login("ID1") + fmt.Println("userTokenValue:", userTokenValue) + fmt.Println("adminTokenValue:", adminTokenValue) + + _ = authkit.ADMIN.SetPermissions("ID1", []string{"admin1", "admin2"}) + _ = authkit.USER.SetPermissions("ID1", []string{"user1", "user2"}) + adminPermissions, _ := authkit.ADMIN.GetPermissions("ID1") + userPermissions, _ := authkit.USER.GetPermissions("ID1") + fmt.Println("admin permissions:", adminPermissions) + fmt.Println("user permissions:", userPermissions) + + fmt.Println("admin has user1 permission:", authkit.ADMIN.HasPermission("ID1", "user1")) + fmt.Println("admin has admin1 permission:", authkit.ADMIN.HasPermission("ID1", "admin1")) + fmt.Println("user has admin1 permission:", authkit.USER.HasPermission("ID1", "admin1")) + fmt.Println("user has user1 permission:", authkit.USER.HasPermission("ID1", "user1")) +} diff --git a/go.work b/go.work index 5dd5f54..e35b085 100644 --- a/go.work +++ b/go.work @@ -3,6 +3,7 @@ go 1.25.3 use ( ./core ./examples/kratos/kratos-example + ./examples/multi-certification ./integrations/chi ./integrations/echo ./integrations/fiber diff --git a/stputil/go.sum b/stputil/go.sum index 8c5cf6f..3b657b9 100644 --- a/stputil/go.sum +++ b/stputil/go.sum @@ -1,5 +1,7 @@ +github.com/click33/sa-token-go/storage/memory v0.1.6 h1:iGFEy+HtTJLOpKnbIMbgpXyKotsKpPQu6wWTZVOXQis= +github.com/click33/sa-token-go/storage/memory v0.1.6/go.mod h1:YNojcgyLC/uFrmReZLePCDQ5WK2fo2WWGRjRMvXVH74= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -7,8 +9,12 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/stputil/stplogic.go b/stputil/stplogic.go new file mode 100644 index 0000000..f75dcbd --- /dev/null +++ b/stputil/stplogic.go @@ -0,0 +1,379 @@ +package stputil + +import ( + "fmt" + "github.com/click33/sa-token-go/core/manager" + "github.com/click33/sa-token-go/core/oauth2" + "github.com/click33/sa-token-go/core/security" + "github.com/click33/sa-token-go/core/session" + "sync" + "time" +) + +var ( + TokenValueKey = "stplogic:tokenvalue" + LoginIdKey = "stplogic:loginid" + PermissionsKey = "stplogic:permissions" + RolesKey = "stplogic:roles" +) + +type StpLogic struct { + manager *manager.Manager + mu sync.RWMutex +} + +func NewStpLogic(mrg *manager.Manager) *StpLogic { + return &StpLogic{manager: mrg} +} + +// GetManager gets the global Manager | 获取全局Manager +func (s *StpLogic) GetManager() *manager.Manager { + s.mu.RLock() + defer s.mu.RUnlock() + if s.manager == nil { + panic("StpLogic not initialized.") + } + return s.manager +} + +func (s *StpLogic) SetManager(manager *manager.Manager) { + s.mu.Lock() + defer s.mu.Unlock() + s.manager = manager +} + +// ============ Authentication | 登录认证 ============ + +// Login performs user login | 用户登录 +func (s *StpLogic) Login(loginID interface{}, device ...string) (string, error) { + return s.manager.Login(toString(loginID), device...) +} + +// LoginByToken performs login with specified token | 使用指定Token登录 +func (s *StpLogic) LoginByToken(loginID interface{}, tokenValue string, device ...string) error { + return s.manager.LoginByToken(toString(loginID), tokenValue, device...) +} + +// Logout performs user logout | 用户登出 +func (s *StpLogic) Logout(loginID interface{}, device ...string) error { + return s.manager.Logout(toString(loginID), device...) +} + +// LogoutByToken performs logout by token | 根据Token登出 +func (s *StpLogic) LogoutByToken(tokenValue string) error { + return s.manager.LogoutByToken(tokenValue) +} + +// IsLogin checks if the user is logged in | 检查用户是否已登录 +func (s *StpLogic) IsLogin(tokenValue string) bool { + return s.manager.IsLogin(tokenValue) +} + +// CheckLogin checks login status (throws error if not logged in) | 检查登录状态(未登录抛出错误) +func (s *StpLogic) CheckLogin(tokenValue string) error { + return s.manager.CheckLogin(tokenValue) +} + +// GetLoginID gets the login ID from token | 从Token获取登录ID +func (s *StpLogic) GetLoginID(tokenValue string) (string, error) { + return s.manager.GetLoginID(tokenValue) +} + +// GetLoginIDNotCheck gets login ID without checking | 获取登录ID(不检查) +func (s *StpLogic) GetLoginIDNotCheck(tokenValue string) (string, error) { + return s.manager.GetLoginIDNotCheck(tokenValue) +} + +// GetTokenValue gets the token value for a login ID | 获取登录ID对应的Token值 +func (s *StpLogic) GetTokenValue(loginID interface{}, device ...string) (string, error) { + return s.manager.GetTokenValue(toString(loginID), device...) +} + +// GetTokenInfo gets token information | 获取Token信息 +func (s *StpLogic) GetTokenInfo(tokenValue string) (*manager.TokenInfo, error) { + return s.manager.GetTokenInfo(tokenValue) +} + +// ============ Kickout | 踢人下线 ============ + +// Kickout kicks out a user session | 踢人下线 +func (s *StpLogic) Kickout(loginID interface{}, device ...string) error { + return s.manager.Kickout(toString(loginID), device...) +} + +// ============ Account Disable | 账号封禁 ============ + +// Disable disables an account for specified duration | 封禁账号(指定时长) +func (s *StpLogic) Disable(loginID interface{}, duration time.Duration) error { + return s.manager.Disable(toString(loginID), duration) +} + +// Untie re-enables a disabled account | 解封账号 +func (s *StpLogic) Untie(loginID interface{}) error { + return s.manager.Untie(toString(loginID)) +} + +// IsDisable checks if an account is disabled | 检查账号是否被封禁 +func (s *StpLogic) IsDisable(loginID interface{}) bool { + return s.manager.IsDisable(toString(loginID)) +} + +// GetDisableTime gets remaining disable time in seconds | 获取剩余封禁时间(秒) +func (s *StpLogic) GetDisableTime(loginID interface{}) (int64, error) { + return s.manager.GetDisableTime(toString(loginID)) +} + +// ============ Session Management | Session管理 ============ + +// GetSession gets session by login ID | 根据登录ID获取Session +func (s *StpLogic) GetSession(loginID interface{}) (*session.Session, error) { + return s.manager.GetSession(toString(loginID)) +} + +// GetSessionByToken gets session by token | 根据Token获取Session +func (s *StpLogic) GetSessionByToken(tokenValue string) (*session.Session, error) { + return s.manager.GetSessionByToken(tokenValue) +} + +// DeleteSession deletes a session | 删除Session +func (s *StpLogic) DeleteSession(loginID interface{}) error { + return s.manager.DeleteSession(toString(loginID)) +} + +// ============ Permission Verification | 权限验证 ============ + +// SetPermissions sets permissions for a login ID | 设置用户权限 +func (s *StpLogic) SetPermissions(loginID interface{}, permissions []string) error { + return s.manager.SetPermissions(toString(loginID), permissions) +} + +// GetPermissions gets permission list | 获取权限列表 +func (s *StpLogic) GetPermissions(loginID interface{}) ([]string, error) { + return s.manager.GetPermissions(toString(loginID)) +} + +// HasPermission checks if has specified permission | 检查是否拥有指定权限 +func (s *StpLogic) HasPermission(loginID interface{}, permission string) bool { + return s.manager.HasPermission(toString(loginID), permission) +} + +// HasPermissionsAnd checks if has all permissions (AND logic) | 检查是否拥有所有权限(AND逻辑) +func (s *StpLogic) HasPermissionsAnd(loginID interface{}, permissions []string) bool { + return s.manager.HasPermissionsAnd(toString(loginID), permissions) +} + +// HasPermissionsOr checks if has any permission (OR logic) | 检查是否拥有任一权限(OR逻辑) +func (s *StpLogic) HasPermissionsOr(loginID interface{}, permissions []string) bool { + return s.manager.HasPermissionsOr(toString(loginID), permissions) +} + +// ============ Role Management | 角色管理 ============ + +// SetRoles sets roles for a login ID | 设置用户角色 +func (s *StpLogic) SetRoles(loginID interface{}, roles []string) error { + return s.manager.SetRoles(toString(loginID), roles) +} + +// GetRoles gets role list | 获取角色列表 +func (s *StpLogic) GetRoles(loginID interface{}) ([]string, error) { + return s.manager.GetRoles(toString(loginID)) +} + +// HasRole checks if has specified role | 检查是否拥有指定角色 +func (s *StpLogic) HasRole(loginID interface{}, role string) bool { + return s.manager.HasRole(toString(loginID), role) +} + +// HasRolesAnd checks if has all roles (AND logic) | 检查是否拥有所有角色(AND逻辑) +func (s *StpLogic) HasRolesAnd(loginID interface{}, roles []string) bool { + return s.manager.HasRolesAnd(toString(loginID), roles) +} + +// HasRolesOr 检查是否拥有任一角色(OR) +func (s *StpLogic) HasRolesOr(loginID interface{}, roles []string) bool { + return s.manager.HasRolesOr(toString(loginID), roles) +} + +// ============ Token标签 ============ + +// SetTokenTag 设置Token标签 +func (s *StpLogic) SetTokenTag(tokenValue, tag string) error { + return s.manager.SetTokenTag(tokenValue, tag) +} + +// GetTokenTag 获取Token标签 +func (s *StpLogic) GetTokenTag(tokenValue string) (string, error) { + return s.manager.GetTokenTag(tokenValue) +} + +// ============ 会话查询 ============ + +// GetTokenValueList 获取指定账号的所有Token +func (s *StpLogic) GetTokenValueList(loginID interface{}) ([]string, error) { + return s.manager.GetTokenValueListByLoginID(toString(loginID)) +} + +// GetSessionCount 获取指定账号的Session数量 +func (s *StpLogic) GetSessionCount(loginID interface{}) (int, error) { + return s.manager.GetSessionCountByLoginID(toString(loginID)) +} + +func (s *StpLogic) GenerateNonce() (string, error) { + if s.manager == nil { + panic("Manager not initialized.") + } + return s.manager.GenerateNonce() +} + +func (s *StpLogic) VerifyNonce(nonce string) bool { + if s.manager == nil { + panic("Manager not initialized.") + } + return s.manager.VerifyNonce(nonce) +} + +func (s *StpLogic) LoginWithRefreshToken(loginID interface{}, device ...string) (*security.RefreshTokenInfo, error) { + if s.manager == nil { + panic("Manager not initialized.") + } + deviceType := "default" + if len(device) > 0 { + deviceType = device[0] + } + return s.manager.LoginWithRefreshToken(fmt.Sprintf("%v", loginID), deviceType) +} + +func (s *StpLogic) RefreshAccessToken(refreshToken string) (*security.RefreshTokenInfo, error) { + if s.manager == nil { + panic("Manager not initialized.") + } + return s.manager.RefreshAccessToken(refreshToken) +} + +func (s *StpLogic) RevokeRefreshToken(refreshToken string) error { + if s.manager == nil { + panic("Manager not initialized.") + } + return s.manager.RevokeRefreshToken(refreshToken) +} + +func (s *StpLogic) GetOAuth2Server() *oauth2.OAuth2Server { + if s.manager == nil { + panic("Manager not initialized.") + } + return s.manager.GetOAuth2Server() +} + +// ============ Check Functions for Token-based operations | 基于Token的检查函数 ============ + +// CheckDisable checks if the account associated with the token is disabled | 检查Token对应账号是否被封禁 +func (s *StpLogic) CheckDisable(tokenValue string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if s.IsDisable(loginID) { + return fmt.Errorf("account is disabled") + } + return nil +} + +// CheckPermission checks if the token has the specified permission | 检查Token是否拥有指定权限 +func (s *StpLogic) CheckPermission(tokenValue string, permission string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if !s.HasPermission(loginID, permission) { + return fmt.Errorf("permission denied: %s", permission) + } + return nil +} + +// CheckPermissionAnd checks if the token has all specified permissions | 检查Token是否拥有所有指定权限 +func (s *StpLogic) CheckPermissionAnd(tokenValue string, permissions []string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if !s.HasPermissionsAnd(loginID, permissions) { + return fmt.Errorf("permission denied: %v", permissions) + } + return nil +} + +// CheckPermissionOr checks if the token has any of the specified permissions | 检查Token是否拥有任一指定权限 +func (s *StpLogic) CheckPermissionOr(tokenValue string, permissions []string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if !s.HasPermissionsOr(loginID, permissions) { + return fmt.Errorf("permission denied: %v", permissions) + } + return nil +} + +// GetPermissionList gets permission list for the token | 获取Token对应的权限列表 +func (s *StpLogic) GetPermissionList(tokenValue string) ([]string, error) { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return nil, err + } + return s.GetPermissions(loginID) +} + +// CheckRole checks if the token has the specified role | 检查Token是否拥有指定角色 +func (s *StpLogic) CheckRole(tokenValue string, role string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if !s.HasRole(loginID, role) { + return fmt.Errorf("role denied: %s", role) + } + return nil +} + +// CheckRoleAnd checks if the token has all specified roles | 检查Token是否拥有所有指定角色 +func (s *StpLogic) CheckRoleAnd(tokenValue string, roles []string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if !s.HasRolesAnd(loginID, roles) { + return fmt.Errorf("role denied: %v", roles) + } + return nil +} + +// CheckRoleOr checks if the token has any of the specified roles | 检查Token是否拥有任一指定角色 +func (s *StpLogic) CheckRoleOr(tokenValue string, roles []string) error { + loginID, err := s.GetLoginID(tokenValue) + if err != nil { + return err + } + if !s.HasRolesOr(loginID, roles) { + return fmt.Errorf("role denied: %v", roles) + } + return nil +} + +// GetRoleList gets role list for the token | 获取Token对应的角色列表 +func (s *StpLogic) GetRoleList(tokenValue string) ([]string, error) { + loginID, err := GetLoginID(tokenValue) + if err != nil { + return nil, err + } + return GetRoles(loginID) +} + +// GetTokenSession gets session for the token | 获取Token对应的Session +func (s *StpLogic) GetTokenSession(tokenValue string) (*session.Session, error) { + return GetSessionByToken(tokenValue) +} + +// CloseManager Closes the manager and releases all resources | 关闭管理器并释放所有资源 +func (s *StpLogic) CloseManager() { + s.manager.CloseManager() +} diff --git a/stputil/stplogic_test.go b/stputil/stplogic_test.go new file mode 100644 index 0000000..547aa3b --- /dev/null +++ b/stputil/stplogic_test.go @@ -0,0 +1,254 @@ +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/core/security" + "github.com/click33/sa-token-go/storage/memory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testConfig() *config.Config { + return &config.Config{ + TokenName: "satoken", + Timeout: 3600, + IsConcurrent: true, + IsShare: true, + MaxLoginCount: -1, + } +} + +func newTestStpLogic(t *testing.T) *StpLogic { + t.Helper() + + storage := memory.NewStorage() + mgr := manager.NewManager(storage, testConfig()) + logic := NewStpLogic(mgr) + + SetStpLogic(logic) + + t.Cleanup(func() { + logic.CloseManager() + SetStpLogic(nil) + }) + + return logic +} + +func TestStpLogic_ManagerAccessors(t *testing.T) { + logic := newTestStpLogic(t) + + mgr := logic.GetManager() + require.NotNil(t, mgr) + + nextMgr := manager.NewManager(memory.NewStorage(), testConfig()) + logic.SetManager(nextMgr) + assert.Equal(t, nextMgr, logic.GetManager()) +} + +func TestStpLogic_AuthAndSessionFlow(t *testing.T) { + logic := newTestStpLogic(t) + + token, err := logic.Login("user-auth", "web") + require.NoError(t, err) + require.NotEmpty(t, token) + + assert.NoError(t, logic.LoginByToken("user-auth", token, "web")) + assert.True(t, logic.IsLogin(token)) + assert.NoError(t, logic.CheckLogin(token)) + + loginID, err := logic.GetLoginID(token) + require.NoError(t, err) + assert.Equal(t, "user-auth", loginID) + + loginIDUnchecked, err := logic.GetLoginIDNotCheck(token) + require.NoError(t, err) + assert.Equal(t, "user-auth", loginIDUnchecked) + + tokenValue, err := logic.GetTokenValue("user-auth", "web") + require.NoError(t, err) + assert.Equal(t, token, tokenValue) + + tokenInfo, err := logic.GetTokenInfo(token) + require.NoError(t, err) + assert.Equal(t, "user-auth", tokenInfo.LoginID) + + sess, err := logic.GetSession("user-auth") + require.NoError(t, err) + assert.NotNil(t, sess) + + sessByToken, err := logic.GetSessionByToken(token) + require.NoError(t, err) + assert.NotNil(t, sessByToken) + + tokenSess, err := logic.GetTokenSession(token) + require.NoError(t, err) + assert.NotNil(t, tokenSess) + + tokenList, err := logic.GetTokenValueList("user-auth") + require.NoError(t, err) + assert.Contains(t, tokenList, token) + + sessCount, err := logic.GetSessionCount("user-auth") + require.NoError(t, err) + assert.Equal(t, 1, sessCount) + + require.NoError(t, logic.Logout("user-auth", "web")) + assert.False(t, logic.IsLogin(token)) + + token2, err := logic.Login("user-auth", "web") + require.NoError(t, err) + require.NoError(t, logic.LogoutByToken(token2)) + assert.False(t, logic.IsLogin(token2)) + + token3, err := logic.Login("user-auth", "web") + require.NoError(t, err) + require.NoError(t, logic.Kickout("user-auth", "web")) + assert.False(t, logic.IsLogin(token3)) + + token4, err := logic.Login("user-auth", "web") + require.NoError(t, err) + require.NoError(t, logic.DeleteSession("user-auth")) + sessAfterDelete, err := logic.GetSession("user-auth") + require.NoError(t, err) + assert.True(t, sessAfterDelete.IsEmpty()) + assert.True(t, logic.IsLogin(token4)) +} + +func TestStpLogic_Permissions(t *testing.T) { + logic := newTestStpLogic(t) + + token, err := logic.Login("user-perm") + require.NoError(t, err) + + perms := []string{"user.read", "user.write"} + require.NoError(t, logic.SetPermissions("user-perm", perms)) + + gotPerms, err := logic.GetPermissions("user-perm") + require.NoError(t, err) + assert.ElementsMatch(t, perms, gotPerms) + + assert.True(t, logic.HasPermission("user-perm", "user.read")) + assert.True(t, logic.HasPermissionsAnd("user-perm", []string{"user.read", "user.write"})) + assert.True(t, logic.HasPermissionsOr("user-perm", []string{"user.delete", "user.read"})) + + assert.NoError(t, logic.CheckPermission(token, "user.read")) + assert.NoError(t, logic.CheckPermissionAnd(token, []string{"user.read", "user.write"})) + assert.NoError(t, logic.CheckPermissionOr(token, []string{"user.delete", "user.read"})) + + err = logic.CheckPermission(token, "user.delete") + assert.Error(t, err) + + permList, err := logic.GetPermissionList(token) + require.NoError(t, err) + assert.ElementsMatch(t, perms, permList) +} + +func TestStpLogic_Roles(t *testing.T) { + logic := newTestStpLogic(t) + + token, err := logic.Login("user-role") + require.NoError(t, err) + + roles := []string{"Admin", "User"} + require.NoError(t, logic.SetRoles("user-role", roles)) + + gotRoles, err := logic.GetRoles("user-role") + require.NoError(t, err) + assert.ElementsMatch(t, roles, gotRoles) + + assert.True(t, logic.HasRole("user-role", "Admin")) + assert.True(t, logic.HasRolesAnd("user-role", []string{"Admin", "User"})) + assert.True(t, logic.HasRolesOr("user-role", []string{"Guest", "Admin"})) + + assert.NoError(t, logic.CheckRole(token, "Admin")) + assert.NoError(t, logic.CheckRoleAnd(token, []string{"Admin", "User"})) + assert.NoError(t, logic.CheckRoleOr(token, []string{"Guest", "Admin"})) + + err = logic.CheckRole(token, "Guest") + assert.Error(t, err) + + roleList, err := GetRoleList(token) + require.NoError(t, err) + assert.ElementsMatch(t, roles, roleList) +} + +func TestStpLogic_DisableAndUntie(t *testing.T) { + logic := newTestStpLogic(t) + + token, err := logic.Login("user-disable") + require.NoError(t, err) + + assert.NoError(t, logic.CheckDisable(token)) + + require.NoError(t, logic.Disable("user-disable", time.Minute)) + assert.True(t, logic.IsDisable("user-disable")) + + disableTTL, err := logic.GetDisableTime("user-disable") + require.NoError(t, err) + assert.Greater(t, disableTTL, int64(0)) + + assert.Error(t, logic.CheckDisable(token)) + + require.NoError(t, logic.Untie("user-disable")) + assert.False(t, logic.IsDisable("user-disable")) + + newToken, err := logic.Login("user-disable") + require.NoError(t, err) + assert.NoError(t, logic.CheckDisable(newToken)) +} + +func TestStpLogic_TokenTags(t *testing.T) { + logic := newTestStpLogic(t) + + err := logic.SetTokenTag("token-tag", "demo") + assert.Error(t, err) + + _, err = logic.GetTokenTag("token-tag") + assert.Error(t, err) +} + +func TestStpLogic_Nonce(t *testing.T) { + logic := newTestStpLogic(t) + + nonce, err := logic.GenerateNonce() + require.NoError(t, err) + require.NotEmpty(t, nonce) + + assert.True(t, logic.VerifyNonce(nonce)) + assert.False(t, logic.VerifyNonce(nonce)) +} + +func TestStpLogic_RefreshTokenFlow(t *testing.T) { + logic := newTestStpLogic(t) + + tokenInfo, err := logic.LoginWithRefreshToken("user-refresh", "mobile") + require.NoError(t, err) + require.NotEmpty(t, tokenInfo.AccessToken) + require.NotEmpty(t, tokenInfo.RefreshToken) + + assert.True(t, logic.IsLogin(tokenInfo.AccessToken)) + + refreshed, err := logic.RefreshAccessToken(tokenInfo.RefreshToken) + if err == nil { + require.NotEmpty(t, refreshed.AccessToken) + assert.True(t, logic.IsLogin(refreshed.AccessToken)) + } else { + assert.ErrorIs(t, err, security.ErrInvalidRefreshData) + } + + require.NoError(t, logic.RevokeRefreshToken(tokenInfo.RefreshToken)) + _, err = logic.RefreshAccessToken(tokenInfo.RefreshToken) + assert.ErrorIs(t, err, security.ErrInvalidRefreshToken) +} + +func TestStpLogic_OAuth2AndClose(t *testing.T) { + logic := newTestStpLogic(t) + + assert.NotNil(t, logic.GetOAuth2Server()) + assert.NotPanics(t, func() { logic.CloseManager() }) +} diff --git a/stputil/stputil.go b/stputil/stputil.go index 4a84240..1edc019 100644 --- a/stputil/stputil.go +++ b/stputil/stputil.go @@ -13,212 +13,229 @@ import ( // Global Manager instance | 全局Manager实例 var ( - globalManager *manager.Manager - once sync.Once - mu sync.RWMutex + globalLogic *StpLogic + //globalManager *manager.Manager + once sync.Once + mu sync.RWMutex ) // SetManager sets the global Manager (must be called first) | 设置全局Manager(必须先调用此方法) func SetManager(mgr *manager.Manager) { - mu.Lock() - defer mu.Unlock() - globalManager = mgr + if globalLogic == nil { + globalLogic = NewStpLogic(mgr) + } + globalLogic.SetManager(mgr) } // GetManager gets the global Manager | 获取全局Manager func GetManager() *manager.Manager { - mu.RLock() - defer mu.RUnlock() - if globalManager == nil { - panic("StpUtil not initialized, please call SetManager() first or use builder.NewBuilder().Build()") + if globalLogic == nil { + panic("StpUtil not initialized, please call SetManager() first") + } + if globalLogic.GetManager() == nil { + panic("StpUtil not initialized, please call SetManager() first") } - return globalManager + return globalLogic.GetManager() } // CloseManager closes global Manager and releases resources | 关闭全局 Manager 并释放资源 func CloseManager() { + if globalLogic != nil { + globalLogic.CloseManager() + globalLogic.SetManager(nil) // 置 nil 避免后续误用 + } +} + +// ============ StpLogic ============ + +// SetStpLogic sets the global StpLogic instance | 设置全局 StpLogic 实例 +func SetStpLogic(logic *StpLogic) { mu.Lock() defer mu.Unlock() - if globalManager != nil { - globalManager.CloseManager() - globalManager = nil // 置 nil 避免后续误用 - } + globalLogic = logic +} + +// GetStpLogic gets the global StpLogic instance | 获取全局 StpLogic 实例 +func GetStpLogic() *StpLogic { + mu.Lock() + defer mu.Unlock() + return globalLogic } // ============ Authentication | 登录认证 ============ // Login performs user login | 用户登录 func Login(loginID interface{}, device ...string) (string, error) { - return GetManager().Login(toString(loginID), device...) + return globalLogic.Login(toString(loginID), device...) } // LoginByToken performs login with specified token | 使用指定Token登录 func LoginByToken(loginID interface{}, tokenValue string, device ...string) error { - return GetManager().LoginByToken(toString(loginID), tokenValue, device...) + return globalLogic.LoginByToken(toString(loginID), tokenValue, device...) } // Logout performs user logout | 用户登出 func Logout(loginID interface{}, device ...string) error { - return GetManager().Logout(toString(loginID), device...) + return globalLogic.Logout(toString(loginID), device...) } // LogoutByToken performs logout by token | 根据Token登出 func LogoutByToken(tokenValue string) error { - return GetManager().LogoutByToken(tokenValue) + return globalLogic.LogoutByToken(tokenValue) } // IsLogin checks if the user is logged in | 检查用户是否已登录 func IsLogin(tokenValue string) bool { - return GetManager().IsLogin(tokenValue) + return globalLogic.IsLogin(tokenValue) } // CheckLogin checks login status (throws error if not logged in) | 检查登录状态(未登录抛出错误) func CheckLogin(tokenValue string) error { - return GetManager().CheckLogin(tokenValue) + return globalLogic.CheckLogin(tokenValue) } // GetLoginID gets the login ID from token | 从Token获取登录ID func GetLoginID(tokenValue string) (string, error) { - return GetManager().GetLoginID(tokenValue) + return globalLogic.GetLoginID(tokenValue) } // GetLoginIDNotCheck gets login ID without checking | 获取登录ID(不检查) func GetLoginIDNotCheck(tokenValue string) (string, error) { - return GetManager().GetLoginIDNotCheck(tokenValue) + return globalLogic.GetLoginIDNotCheck(tokenValue) } // GetTokenValue gets the token value for a login ID | 获取登录ID对应的Token值 func GetTokenValue(loginID interface{}, device ...string) (string, error) { - return GetManager().GetTokenValue(toString(loginID), device...) + return globalLogic.GetTokenValue(toString(loginID), device...) } // GetTokenInfo gets token information | 获取Token信息 func GetTokenInfo(tokenValue string) (*manager.TokenInfo, error) { - return GetManager().GetTokenInfo(tokenValue) + return globalLogic.GetTokenInfo(tokenValue) } // ============ Kickout | 踢人下线 ============ // Kickout kicks out a user session | 踢人下线 func Kickout(loginID interface{}, device ...string) error { - return GetManager().Kickout(toString(loginID), device...) + return globalLogic.Kickout(toString(loginID), device...) } // ============ Account Disable | 账号封禁 ============ // Disable disables an account for specified duration | 封禁账号(指定时长) func Disable(loginID interface{}, duration time.Duration) error { - return GetManager().Disable(toString(loginID), duration) + return globalLogic.Disable(toString(loginID), duration) } // Untie re-enables a disabled account | 解封账号 func Untie(loginID interface{}) error { - return GetManager().Untie(toString(loginID)) + return globalLogic.Untie(toString(loginID)) } // IsDisable checks if an account is disabled | 检查账号是否被封禁 func IsDisable(loginID interface{}) bool { - return GetManager().IsDisable(toString(loginID)) + return globalLogic.IsDisable(toString(loginID)) } // GetDisableTime gets remaining disable time in seconds | 获取剩余封禁时间(秒) func GetDisableTime(loginID interface{}) (int64, error) { - return GetManager().GetDisableTime(toString(loginID)) + return globalLogic.GetDisableTime(toString(loginID)) } // ============ Session Management | Session管理 ============ // GetSession gets session by login ID | 根据登录ID获取Session func GetSession(loginID interface{}) (*session.Session, error) { - return GetManager().GetSession(toString(loginID)) + return globalLogic.GetSession(toString(loginID)) } // GetSessionByToken gets session by token | 根据Token获取Session func GetSessionByToken(tokenValue string) (*session.Session, error) { - return GetManager().GetSessionByToken(tokenValue) + return globalLogic.GetSessionByToken(tokenValue) } // DeleteSession deletes a session | 删除Session func DeleteSession(loginID interface{}) error { - return GetManager().DeleteSession(toString(loginID)) + return globalLogic.DeleteSession(toString(loginID)) } // ============ Permission Verification | 权限验证 ============ // SetPermissions sets permissions for a login ID | 设置用户权限 func SetPermissions(loginID interface{}, permissions []string) error { - return GetManager().SetPermissions(toString(loginID), permissions) + return globalLogic.SetPermissions(toString(loginID), permissions) } // GetPermissions gets permission list | 获取权限列表 func GetPermissions(loginID interface{}) ([]string, error) { - return GetManager().GetPermissions(toString(loginID)) + return globalLogic.GetPermissions(toString(loginID)) } // HasPermission checks if has specified permission | 检查是否拥有指定权限 func HasPermission(loginID interface{}, permission string) bool { - return GetManager().HasPermission(toString(loginID), permission) + return globalLogic.HasPermission(toString(loginID), permission) } // HasPermissionsAnd checks if has all permissions (AND logic) | 检查是否拥有所有权限(AND逻辑) func HasPermissionsAnd(loginID interface{}, permissions []string) bool { - return GetManager().HasPermissionsAnd(toString(loginID), permissions) + return globalLogic.HasPermissionsAnd(toString(loginID), permissions) } // HasPermissionsOr checks if has any permission (OR logic) | 检查是否拥有任一权限(OR逻辑) func HasPermissionsOr(loginID interface{}, permissions []string) bool { - return GetManager().HasPermissionsOr(toString(loginID), permissions) + return globalLogic.HasPermissionsOr(toString(loginID), permissions) } // ============ Role Management | 角色管理 ============ // SetRoles sets roles for a login ID | 设置用户角色 func SetRoles(loginID interface{}, roles []string) error { - return GetManager().SetRoles(toString(loginID), roles) + return globalLogic.SetRoles(toString(loginID), roles) } // GetRoles gets role list | 获取角色列表 func GetRoles(loginID interface{}) ([]string, error) { - return GetManager().GetRoles(toString(loginID)) + return globalLogic.GetRoles(toString(loginID)) } // HasRole checks if has specified role | 检查是否拥有指定角色 func HasRole(loginID interface{}, role string) bool { - return GetManager().HasRole(toString(loginID), role) + return globalLogic.HasRole(toString(loginID), role) } // HasRolesAnd checks if has all roles (AND logic) | 检查是否拥有所有角色(AND逻辑) func HasRolesAnd(loginID interface{}, roles []string) bool { - return GetManager().HasRolesAnd(toString(loginID), roles) + return globalLogic.HasRolesAnd(toString(loginID), roles) } // HasRolesOr 检查是否拥有任一角色(OR) func HasRolesOr(loginID interface{}, roles []string) bool { - return GetManager().HasRolesOr(toString(loginID), roles) + return globalLogic.HasRolesOr(toString(loginID), roles) } // ============ Token标签 ============ // SetTokenTag 设置Token标签 func SetTokenTag(tokenValue, tag string) error { - return GetManager().SetTokenTag(tokenValue, tag) + return globalLogic.SetTokenTag(tokenValue, tag) } // GetTokenTag 获取Token标签 func GetTokenTag(tokenValue string) (string, error) { - return GetManager().GetTokenTag(tokenValue) + return globalLogic.GetTokenTag(tokenValue) } // ============ 会话查询 ============ // GetTokenValueList 获取指定账号的所有Token func GetTokenValueList(loginID interface{}) ([]string, error) { - return GetManager().GetTokenValueListByLoginID(toString(loginID)) + return globalLogic.GetTokenValueList(loginID) } // GetSessionCount 获取指定账号的Session数量 func GetSessionCount(loginID interface{}) (int, error) { - return GetManager().GetSessionCountByLoginID(toString(loginID)) + return globalLogic.GetSessionCount(loginID) } // ============ 辅助方法 ============ @@ -288,49 +305,49 @@ func uint64ToString(u uint64) string { } func GenerateNonce() (string, error) { - if globalManager == nil { + if globalLogic == nil { panic("Manager not initialized. Call stputil.SetManager() first") } - return globalManager.GenerateNonce() + return globalLogic.GenerateNonce() } func VerifyNonce(nonce string) bool { - if globalManager == nil { + if globalLogic == nil { panic("Manager not initialized. Call stputil.SetManager() first") } - return globalManager.VerifyNonce(nonce) + return globalLogic.VerifyNonce(nonce) } func LoginWithRefreshToken(loginID interface{}, device ...string) (*security.RefreshTokenInfo, error) { - if globalManager == nil { + if globalLogic == nil { panic("Manager not initialized. Call stputil.SetManager() first") } deviceType := "default" if len(device) > 0 { deviceType = device[0] } - return globalManager.LoginWithRefreshToken(fmt.Sprintf("%v", loginID), deviceType) + return globalLogic.LoginWithRefreshToken(fmt.Sprintf("%v", loginID), deviceType) } func RefreshAccessToken(refreshToken string) (*security.RefreshTokenInfo, error) { - if globalManager == nil { + if globalLogic == nil { panic("Manager not initialized. Call stputil.SetManager() first") } - return globalManager.RefreshAccessToken(refreshToken) + return globalLogic.RefreshAccessToken(refreshToken) } func RevokeRefreshToken(refreshToken string) error { - if globalManager == nil { + if globalLogic == nil { panic("Manager not initialized. Call stputil.SetManager() first") } - return globalManager.RevokeRefreshToken(refreshToken) + return globalLogic.RevokeRefreshToken(refreshToken) } func GetOAuth2Server() *oauth2.OAuth2Server { - if globalManager == nil { + if globalLogic == nil { panic("Manager not initialized. Call stputil.SetManager() first") } - return globalManager.GetOAuth2Server() + return globalLogic.GetOAuth2Server() } // ============ Check Functions for Token-based operations | 基于Token的检查函数 ============ -- Gitee From 31797b648fb88993b0713f115ba1fdfe54a9bf53 Mon Sep 17 00:00:00 2001 From: c <23@g> Date: Wed, 31 Dec 2025 10:55:40 +0700 Subject: [PATCH 08/28] chore: upgrade version to v0.1.7 --- README.md | 2 +- README_zh.md | 2 +- core/version/version.go | 2 +- integrations/chi/go.mod | 4 ++-- integrations/echo/go.mod | 4 ++-- integrations/fiber/go.mod | 4 ++-- integrations/gf/go.mod | 4 ++-- integrations/gin/go.mod | 4 ++-- integrations/kratos/go.mod | 6 +++--- storage/memory/go.mod | 2 +- storage/redis/go.mod | 2 +- stputil/go.mod | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 35915d4..46be440 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ func init() { ___/ / /_/ / / / / /_/ / ,< / __/ / / /_____/ /_/ / /_/ / /____/\__,_/ /_/ \____/_/|_|\___/_/ /_/ \____/\____/ -:: Sa-Token-Go :: (v0.1.6) +:: Sa-Token-Go :: (v0.1.7) :: Go Version :: go1.21.0 :: GOOS/GOARCH :: linux/amd64 diff --git a/README_zh.md b/README_zh.md index 1e9bbaa..4d7de6b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -106,7 +106,7 @@ func init() { ___/ / /_/ / / / / /_/ / ,< / __/ / / /_____/ /_/ / /_/ / /____/\__,_/ /_/ \____/_/|_|\___/_/ /_/ \____/\____/ -:: Sa-Token-Go :: (v0.1.6) +:: Sa-Token-Go :: (v0.1.7) :: Go Version :: go1.21.0 :: GOOS/GOARCH :: linux/amd64 diff --git a/core/version/version.go b/core/version/version.go index 2297589..8004370 100644 --- a/core/version/version.go +++ b/core/version/version.go @@ -3,4 +3,4 @@ package version // Version system level version number | 系统级版本号 // This is the global version of Sa-Token-Go, modify this value to update the version across the entire project // 这是 Sa-Token-Go 的全局版本号,修改此值可更新整个项目的版本 -const Version = "0.1.6" +const Version = "0.1.7" diff --git a/integrations/chi/go.mod b/integrations/chi/go.mod index b497355..caa8b0d 100644 --- a/integrations/chi/go.mod +++ b/integrations/chi/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/integrations/chi go 1.23.0 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/stputil v0.1.7 ) require ( diff --git a/integrations/echo/go.mod b/integrations/echo/go.mod index 93cf344..e641831 100644 --- a/integrations/echo/go.mod +++ b/integrations/echo/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/stputil v0.1.7 github.com/labstack/echo/v4 v4.11.4 ) diff --git a/integrations/fiber/go.mod b/integrations/fiber/go.mod index c5d004b..3858d72 100644 --- a/integrations/fiber/go.mod +++ b/integrations/fiber/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/stputil v0.1.7 github.com/gofiber/fiber/v2 v2.52.0 ) diff --git a/integrations/gf/go.mod b/integrations/gf/go.mod index 2b3fbd2..3621f5b 100644 --- a/integrations/gf/go.mod +++ b/integrations/gf/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/integrations/gf go 1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/stputil v0.1.7 github.com/gogf/gf/v2 v2.9.4 ) diff --git a/integrations/gin/go.mod b/integrations/gin/go.mod index f4b13a1..36b5254 100644 --- a/integrations/gin/go.mod +++ b/integrations/gin/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/stputil v0.1.7 github.com/gin-gonic/gin v1.10.0 github.com/stretchr/testify v1.11.1 ) diff --git a/integrations/kratos/go.mod b/integrations/kratos/go.mod index 47ecfe7..873acf4 100644 --- a/integrations/kratos/go.mod +++ b/integrations/kratos/go.mod @@ -5,9 +5,9 @@ go 1.24.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/storage/memory v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/storage/memory v0.1.7 + github.com/click33/sa-token-go/stputil v0.1.7 github.com/go-kratos/kratos/v2 v2.9.1 ) diff --git a/storage/memory/go.mod b/storage/memory/go.mod index 12faa03..317a84f 100644 --- a/storage/memory/go.mod +++ b/storage/memory/go.mod @@ -2,6 +2,6 @@ module github.com/click33/sa-token-go/storage/memory go 1.23.0 -require github.com/click33/sa-token-go/core v0.1.6 +require github.com/click33/sa-token-go/core v0.1.7 replace github.com/click33/sa-token-go/core => ../../core diff --git a/storage/redis/go.mod b/storage/redis/go.mod index 6b9699f..18b0052 100644 --- a/storage/redis/go.mod +++ b/storage/redis/go.mod @@ -3,7 +3,7 @@ module github.com/click33/sa-token-go/storage/redis go 1.23.0 require ( - github.com/click33/sa-token-go/core v0.1.6 + github.com/click33/sa-token-go/core v0.1.7 github.com/redis/go-redis/v9 v9.5.1 ) diff --git a/stputil/go.mod b/stputil/go.mod index b6e6cdb..5eeb5db 100644 --- a/stputil/go.mod +++ b/stputil/go.mod @@ -2,7 +2,7 @@ module github.com/click33/sa-token-go/stputil go 1.23.0 -require github.com/click33/sa-token-go/core v0.1.6 +require github.com/click33/sa-token-go/core v0.1.7 require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect -- Gitee From c966831908606ec262b6b3b239d94d44007224e8 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:51:58 +0800 Subject: [PATCH 09/28] add wx-group-qr.png add wx-group-qr.png --- README_zh.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README_zh.md b/README_zh.md index 4d7de6b..76370a5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -25,6 +25,12 @@ - 🔄 **Refresh Token** - 刷新令牌机制、无感刷新 - 🔐 **OAuth2** - 完整的OAuth2授权码模式实现 + +## 💬 微信交流群 + +sa-token-go 微信交流群 + + ## 🚀 快速开始 ### 📥 安装 -- Gitee From 1d3ec6197bfdd7a76afbce34e2cca59be55a17f7 Mon Sep 17 00:00:00 2001 From: yuegc Date: Sat, 24 Jan 2026 21:57:40 +0800 Subject: [PATCH 10/28] =?UTF-8?q?feat:=E6=94=AF=E6=8C=81hertz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.work | 1 + go.work.sum | 25 +- integrations/hertz/annotation.go | 365 ++++++++++++++++++++ integrations/hertz/annotation_test.go | 459 ++++++++++++++++++++++++++ integrations/hertz/context.go | 167 ++++++++++ integrations/hertz/export.go | 364 ++++++++++++++++++++ integrations/hertz/go.mod | 12 + integrations/hertz/go.sum | 4 + integrations/hertz/plugin.go | 291 ++++++++++++++++ 9 files changed, 1684 insertions(+), 4 deletions(-) create mode 100644 integrations/hertz/annotation.go create mode 100644 integrations/hertz/annotation_test.go create mode 100644 integrations/hertz/context.go create mode 100644 integrations/hertz/export.go create mode 100644 integrations/hertz/go.mod create mode 100644 integrations/hertz/go.sum create mode 100644 integrations/hertz/plugin.go diff --git a/go.work b/go.work index e35b085..a98912c 100644 --- a/go.work +++ b/go.work @@ -9,6 +9,7 @@ use ( ./integrations/fiber ./integrations/gf ./integrations/gin + ./integrations/hertz ./integrations/kratos ./storage/memory ./storage/redis diff --git a/go.work.sum b/go.work.sum index b77dd20..6af57e0 100644 --- a/go.work.sum +++ b/go.work.sum @@ -14,8 +14,14 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE= +github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -23,9 +29,15 @@ 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/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= +github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= +github.com/cloudwego/hertz v0.10.3 h1:NFcQAjouVJsod79XPLC/PaFfHgjMTYbiErmW+vGBi8A= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= +github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= @@ -35,7 +47,6 @@ github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1Ig github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.11.2-0.20230627204322-7d0032219fcb h1:kxNVXsNro/lpR5WD+P1FI/yUHn2G03Glber3k8cQL2Y= @@ -113,6 +124,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= +github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= @@ -126,7 +139,6 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -154,7 +166,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= diff --git a/integrations/hertz/annotation.go b/integrations/hertz/annotation.go new file mode 100644 index 0000000..783e9ca --- /dev/null +++ b/integrations/hertz/annotation.go @@ -0,0 +1,365 @@ +package hertz + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/stputil" + "github.com/cloudwego/hertz/pkg/app" +) + +// Annotation constants | 注解常量 +const ( + TagSaCheckLogin = "sa_check_login" + TagSaCheckRole = "sa_check_role" + TagSaCheckPermission = "sa_check_permission" + TagSaCheckDisable = "sa_check_disable" + TagSaIgnore = "sa_ignore" +) + +// Annotation annotation structure | 注解结构体 +type Annotation struct { + CheckLogin bool `json:"checkLogin"` + CheckRole []string `json:"checkRole"` + CheckPermission []string `json:"checkPermission"` + CheckDisable bool `json:"checkDisable"` + Ignore bool `json:"ignore"` +} + +// ParseTag parses struct tags | 解析结构体标签 +func ParseTag(tag string) *Annotation { + ann := &Annotation{} + + if tag == "" { + return ann + } + + parts := strings.Split(tag, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + switch { + case part == TagSaCheckLogin || part == "login": + ann.CheckLogin = true + case strings.HasPrefix(part, TagSaCheckRole+"=") || strings.HasPrefix(part, "role="): + roles := strings.TrimPrefix(part, TagSaCheckRole+"=") + roles = strings.TrimPrefix(roles, "role=") + if roles != "" { + ann.CheckRole = strings.Split(roles, "|") + } + case strings.HasPrefix(part, TagSaCheckPermission+"=") || strings.HasPrefix(part, "permission="): + perms := strings.TrimPrefix(part, TagSaCheckPermission+"=") + perms = strings.TrimPrefix(perms, "permission=") + if perms != "" { + ann.CheckPermission = strings.Split(perms, "|") + } + case part == TagSaCheckDisable || part == "disable": + ann.CheckDisable = true + case part == TagSaIgnore || part == "ignore": + ann.Ignore = true + } + } + + return ann +} + +// Validate validates if annotation is valid | 验证注解是否有效 +func (a *Annotation) Validate() bool { + if a.Ignore { + return true // When ignore is true, other checks are invalid | 忽略认证时,其他检查无效 + } + + count := 0 + if a.CheckLogin { + count++ + } + if len(a.CheckRole) > 0 { + count++ + } + if len(a.CheckPermission) > 0 { + count++ + } + if a.CheckDisable { + count++ + } + + // At most one check type allowed | 最多只能有一个检查类型 + return count <= 1 +} + +// GetHandler gets handler with annotations | 获取带注解的处理器 +func GetHandler(handler interface{}, annotations ...*Annotation) app.HandlerFunc { + return func(c context.Context, ctx *app.RequestContext) { + if len(annotations) > 0 && annotations[0].Ignore { + if callHandler(handler, c, ctx) { + return + } + ctx.Next(c) + return + } + + hCtx := NewHertzContext(ctx) + saCtx := core.NewContext(hCtx, stputil.GetManager()) + token := saCtx.GetTokenValue() + + fmt.Printf("Debug Handler: token='%s', isLogin=%v, headers=%v\n", token, stputil.IsLogin(token), ctx.Request.Header.String()) + + if token == "" { + writeErrorResponse(ctx, core.NewNotLoginError()) + ctx.Abort() + return + } + + if !stputil.IsLogin(token) { + writeErrorResponse(ctx, core.NewNotLoginError()) + ctx.Abort() + return + } + + loginID, err := stputil.GetLoginID(token) + if err != nil { + writeErrorResponse(ctx, err) + ctx.Abort() + return + } + + // Check if account is disabled | 检查是否被封禁 + if len(annotations) > 0 && annotations[0].CheckDisable { + if stputil.IsDisable(loginID) { + writeErrorResponse(ctx, core.NewAccountDisabledError(loginID)) + ctx.Abort() + return + } + } + + // Check permission | 检查权限 + if len(annotations) > 0 && len(annotations[0].CheckPermission) > 0 { + hasPermission := false + for _, perm := range annotations[0].CheckPermission { + if stputil.HasPermission(loginID, strings.TrimSpace(perm)) { + hasPermission = true + break + } + } + if !hasPermission { + writeErrorResponse(ctx, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) + ctx.Abort() + return + } + } + + // Check role | 检查角色 + if len(annotations) > 0 && len(annotations[0].CheckRole) > 0 { + hasRole := false + for _, role := range annotations[0].CheckRole { + if stputil.HasRole(loginID, strings.TrimSpace(role)) { + hasRole = true + break + } + } + if !hasRole { + writeErrorResponse(ctx, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) + ctx.Abort() + return + } + } + + // All checks passed, execute original handler or continue | 所有检查通过,执行原函数或继续 + if callHandler(handler, c, ctx) { + return + } + ctx.Next(c) + } +} + +func callHandler(handler interface{}, c context.Context, ctx *app.RequestContext) bool { + if handler == nil { + return false + } + + switch h := handler.(type) { + case func(*app.RequestContext): + if h == nil { + return false + } + h(ctx) + return true + case app.HandlerFunc: + if h == nil { + return false + } + h(c, ctx) + return true + } + + hv := reflect.ValueOf(handler) + if hv.Kind() != reflect.Func || hv.IsNil() || hv.Type().NumIn() != 1 { + return false + } + + argType := hv.Type().In(0) + if !argType.AssignableTo(reflect.TypeOf(c)) { + return false + } + + hv.Call([]reflect.Value{reflect.ValueOf(c)}) + return true +} + +// Decorator functions | 装饰器函数 + +// CheckLogin decorator for login checking | 检查登录装饰器 +func CheckLogin() app.HandlerFunc { + return GetHandler(nil, &Annotation{CheckLogin: true}) +} + +// CheckRole decorator for role checking | 检查角色装饰器 +func CheckRole(roles ...string) app.HandlerFunc { + return GetHandler(nil, &Annotation{CheckRole: roles}) +} + +// CheckPermission decorator for permission checking | 检查权限装饰器 +func CheckPermission(perms ...string) app.HandlerFunc { + return GetHandler(nil, &Annotation{CheckPermission: perms}) +} + +// CheckDisable decorator for checking if account is disabled | 检查是否被封禁装饰器 +func CheckDisable() app.HandlerFunc { + return GetHandler(nil, &Annotation{CheckDisable: true}) +} + +// Ignore decorator to ignore authentication | 忽略认证装饰器 +func Ignore() app.HandlerFunc { + return GetHandler(nil, &Annotation{Ignore: true}) +} + +// WithAnnotation decorator with custom annotation | 使用自定义注解装饰器 +func WithAnnotation(ann *Annotation) app.HandlerFunc { + return GetHandler(nil, ann) +} + +// ProcessStructAnnotations processes annotations on struct tags | 处理结构体上的注解标签 +func ProcessStructAnnotations(handler interface{}) app.HandlerFunc { + handlerValue := reflect.ValueOf(handler) + handlerType := reflect.TypeOf(handler) + + // Find method name, usually the last path segment | 查找方法名,通常是最后一个路径段 + methodName := "unknown" + if handlerType.Kind() == reflect.Ptr { + handlerType = handlerType.Elem() + } + if handlerType.Kind() == reflect.Struct { + methodName = handlerType.Name() + } + + // Parse method annotations | 解析方法上的注解标签 + ann := parseMethodAnnotation(handlerType, methodName) + + return GetHandler(func(c *app.RequestContext) { + handlerValue.MethodByName("ServeHTTP").Call([]reflect.Value{reflect.ValueOf(c)}) + }, ann) +} + +// parseMethodAnnotation parses method annotations | 解析方法注解 +func parseMethodAnnotation(t reflect.Type, methodName string) *Annotation { + // Simplified implementation, returns empty annotation | 简化实现,直接返回空注解 + return &Annotation{} +} + +// HandlerWithAnnotations 带注解的处理器包装器 +type HandlerWithAnnotations struct { + Handler interface{} + Annotations []*Annotation +} + +// NewHandlerWithAnnotations 创建带注解的处理器 +func NewHandlerWithAnnotations(handler interface{}, annotations ...*Annotation) *HandlerWithAnnotations { + return &HandlerWithAnnotations{ + Handler: handler, + Annotations: annotations, + } +} + +// ToGinHandler 转换为Gin处理器 +func (h *HandlerWithAnnotations) ToGinHandler() app.HandlerFunc { + return GetHandler(h.Handler, h.Annotations...) +} + +// Middleware 创建中间件版本 +func Middleware(annotations ...*Annotation) app.HandlerFunc { + return func(c context.Context, ctx *app.RequestContext) { + if len(annotations) > 0 && annotations[0].Ignore { + ctx.Next(c) + return + } + + hCtx := NewHertzContext(ctx) + saCtx := core.NewContext(hCtx, stputil.GetManager()) + token := saCtx.GetTokenValue() + + if token == "" { + writeErrorResponse(ctx, core.NewNotLoginError()) + ctx.Abort() + return + } + + if !stputil.IsLogin(token) { + writeErrorResponse(ctx, core.NewNotLoginError()) + ctx.Abort() + return + } + + loginID, err := stputil.GetLoginID(token) + if err != nil { + writeErrorResponse(ctx, err) + ctx.Abort() + return + } + + // 检查是否被封禁 + if len(annotations) > 0 && annotations[0].CheckDisable { + if stputil.IsDisable(loginID) { + writeErrorResponse(ctx, core.NewAccountDisabledError(loginID)) + ctx.Abort() + return + } + } + + // 检查权限 + if len(annotations) > 0 && len(annotations[0].CheckPermission) > 0 { + hasPermission := false + for _, perm := range annotations[0].CheckPermission { + if stputil.HasPermission(loginID, strings.TrimSpace(perm)) { + hasPermission = true + break + } + } + if !hasPermission { + writeErrorResponse(ctx, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) + ctx.Abort() + return + } + } + + // 检查角色 + if len(annotations) > 0 && len(annotations[0].CheckRole) > 0 { + hasRole := false + for _, role := range annotations[0].CheckRole { + if stputil.HasRole(loginID, strings.TrimSpace(role)) { + hasRole = true + break + } + } + if !hasRole { + writeErrorResponse(ctx, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) + ctx.Abort() + return + } + } + + // 所有检查通过,继续下一个处理器 + ctx.Next(c) + } +} diff --git a/integrations/hertz/annotation_test.go b/integrations/hertz/annotation_test.go new file mode 100644 index 0000000..d12d87d --- /dev/null +++ b/integrations/hertz/annotation_test.go @@ -0,0 +1,459 @@ +package hertz + +import ( + "context" + "fmt" + "net/http" + "testing" + + "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/click33/sa-token-go/stputil" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/ut" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/stretchr/testify/assert" +) + +// setupTestRouter 创建测试路由器和初始化 sa-token +func setupTestRouter() *server.Hertz { + router := server.Default() + + // 创建内存存储 + storage := memory.NewStorage() + + // 创建配置 + cfg := &config.Config{ + TokenName: "satoken", + Timeout: 2592000, // 30 天(秒) + IsConcurrent: true, + IsShare: true, + MaxLoginCount: -1, + } + + // 创建并设置全局 Manager + mgr := manager.NewManager(storage, cfg) + stputil.SetManager(mgr) + + return router +} + +// mockLogin 模拟用户登录并返回 token +func mockLogin(loginID interface{}) string { + token, _ := stputil.Login(loginID) + return token +} + +// mockLoginWithRole 模拟用户登录并设置角色 +func mockLoginWithRole(loginID interface{}, roles []string) string { + token, _ := stputil.Login(loginID) + stputil.SetRoles(loginID, roles) + return token +} + +// mockLoginWithPermission 模拟用户登录并设置权限 +func mockLoginWithPermission(loginID interface{}, permissions []string) string { + token, _ := stputil.Login(loginID) + stputil.SetPermissions(loginID, permissions) + return token +} + +// TestCheckRole_WithValidRole 测试具有有效角色的用户访问 +func TestCheckRole_WithValidRole(t *testing.T) { + router := setupTestRouter() + + // 设置路由 - 使用 CheckRole 作为中间件 + router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + // 创建一个具有 Admin 角色的用户 + token := mockLoginWithRole("user123", []string{"Admin"}) + fmt.Println("Debug: Token generated:", token) + loginID, _ := stputil.GetLoginID(token) + fmt.Println("Debug: LoginID from storage:", loginID) + + // 发送请求 + w := ut.PerformRequest(router.Engine, "GET", "/admin", nil, + ut.Header{Key: "Authorization", Value: token}) + + // 断言 + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "success") +} + +// TestCheckRole_WithInvalidRole 测试没有所需角色的用户访问 +func TestCheckRole_WithInvalidRole(t *testing.T) { + router := setupTestRouter() + + // 设置路由 + router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + // 创建一个只有 User 角色的用户 + token := mockLoginWithRole("user456", []string{"User"}) + + // 发送请求 + w := ut.PerformRequest(router.Engine, "GET", "/admin", nil, + ut.Header{Key: "Authorization", Value: token}) + + // 断言 + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "角色不足") +} + +// TestCheckRole_MultipleRoles 测试多个角色的情况(OR 逻辑) +func TestCheckRole_MultipleRoles(t *testing.T) { + router := setupTestRouter() + + // 设置路由 - 需要 Admin 或 SuperAdmin 角色 + router.GET("/manage", CheckRole("Admin", "SuperAdmin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + // 测试具有 SuperAdmin 角色的用户 + token := mockLoginWithRole("superuser", []string{"SuperAdmin"}) + + w := ut.PerformRequest(router.Engine, "GET", "/manage", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "success") +} + +// TestCheckRole_NoToken 测试未提供 token 的情况 +func TestCheckRole_NoToken(t *testing.T) { + router := setupTestRouter() + + router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + w := ut.PerformRequest(router.Engine, "GET", "/admin", nil) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "未登录") +} + +// TestCheckRole_InvalidToken 测试无效 token 的情况 +func TestCheckRole_InvalidToken(t *testing.T) { + router := setupTestRouter() + + router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + w := ut.PerformRequest(router.Engine, "GET", "/admin", nil, + ut.Header{Key: "Authorization", Value: "invalid-token-12345"}) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "未登录") +} + +// TestCheckPermission_WithValidPermission 测试具有有效权限的用户访问 +func TestCheckPermission_WithValidPermission(t *testing.T) { + router := setupTestRouter() + + router.GET("/users", CheckPermission("user.read"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + token := mockLoginWithPermission("user789", []string{"user.read"}) + + w := ut.PerformRequest(router.Engine, "GET", "/users", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "success") +} + +// TestCheckPermission_WithInvalidPermission 测试没有所需权限的用户访问 +func TestCheckPermission_WithInvalidPermission(t *testing.T) { + router := setupTestRouter() + + router.GET("/users", CheckPermission("user.delete"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + }) + + token := mockLoginWithPermission("user789", []string{"user.read"}) + + w := ut.PerformRequest(router.Engine, "GET", "/users", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "权限不足") +} + +// TestCheckLogin_Success 测试登录检查成功 +func TestCheckLogin_Success(t *testing.T) { + router := setupTestRouter() + + router.GET("/profile", CheckLogin(), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "profile data"}) + }) + + token := mockLogin("user999") + + w := ut.PerformRequest(router.Engine, "GET", "/profile", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "profile data") +} + +// TestCheckLogin_Failed 测试登录检查失败 +func TestCheckLogin_Failed(t *testing.T) { + router := setupTestRouter() + + router.GET("/profile", CheckLogin(), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "profile data"}) + }) + + w := ut.PerformRequest(router.Engine, "GET", "/profile", nil) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "未登录") +} + +// TestCheckDisable_NotDisabled 测试账号未被封禁的情况 +func TestCheckDisable_NotDisabled(t *testing.T) { + router := setupTestRouter() + + router.GET("/resource", CheckDisable(), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "resource data"}) + }) + + token := mockLogin("user101") + + w := ut.PerformRequest(router.Engine, "GET", "/resource", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "resource data") +} + +// TestCheckDisable_IsDisabled 测试账号被封禁的情况 +func TestCheckDisable_IsDisabled(t *testing.T) { + router := setupTestRouter() + + router.GET("/resource", CheckDisable(), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "resource data"}) + }) + + loginID := "user102" + token := mockLogin(loginID) + + // 封禁账号 + stputil.Disable(loginID, 3600) // 封禁 1 小时 + + w := ut.PerformRequest(router.Engine, "GET", "/resource", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "账号已被封禁") +} + +// TestIgnore_SkipsAuthentication 测试忽略认证装饰器 +func TestIgnore_SkipsAuthentication(t *testing.T) { + router := setupTestRouter() + + router.GET("/public", Ignore(), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "public data"}) + }) + + w := ut.PerformRequest(router.Engine, "GET", "/public", nil) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "public data") +} + +// TestChainedMiddleware_CheckRoleAndHandler 测试链式中间件:CheckRole + 实际处理器 +func TestChainedMiddleware_CheckRoleAndHandler(t *testing.T) { + router := setupTestRouter() + + // 模拟用户示例代码的使用方式 + safeGroup := router.Group("/safe") + { + safeGroup.GET("", CheckRole("SuperAdmin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "safe settings"}) + }) + } + + // 测试具有 SuperAdmin 角色的用户 + token := mockLoginWithRole("admin123", []string{"SuperAdmin"}) + + w := ut.PerformRequest(router.Engine, "GET", "/safe", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "safe settings") +} + +// TestChainedMiddleware_CheckRoleAndHandler_NoRole 测试链式中间件:无角色访问 +func TestChainedMiddleware_CheckRoleAndHandler_NoRole(t *testing.T) { + router := setupTestRouter() + + safeGroup := router.Group("/safe") + { + safeGroup.GET("", CheckRole("SuperAdmin"), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "safe settings"}) + }) + } + + // 测试具有普通用户角色 + token := mockLoginWithRole("user123", []string{"User"}) + + w := ut.PerformRequest(router.Engine, "GET", "/safe", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "角色不足") +} + +// TestGetHandler_WithNilHandler 测试 GetHandler 在 handler 为 nil 时的行为 +func TestGetHandler_WithNilHandler(t *testing.T) { + router := setupTestRouter() + + // 直接使用 GetHandler 创建中间件 + middleware := GetHandler(nil, &Annotation{CheckRole: []string{"Admin"}}) + + router.GET("/test", middleware, func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"message": "test passed"}) + }) + + token := mockLoginWithRole("testuser", []string{"Admin"}) + + w := ut.PerformRequest(router.Engine, "GET", "/test", nil, + ut.Header{Key: "Authorization", Value: token}) + + // 应该能够正常执行,不会 panic + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "test passed") +} + +// TestMiddleware_CheckRole 测试 Middleware 函数的角色检查 +func TestMiddleware_CheckRole(t *testing.T) { + router := setupTestRouter() + + // 使用 Middleware 函数 + router.GET("/api/data", Middleware(&Annotation{CheckRole: []string{"Admin"}}), func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(http.StatusOK, utils.H{"data": "sensitive data"}) + }) + + token := mockLoginWithRole("admin999", []string{"Admin"}) + + w := ut.PerformRequest(router.Engine, "GET", "/api/data", nil, + ut.Header{Key: "Authorization", Value: token}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "sensitive data") +} + +// TestParseTag 测试标签解析功能 +func TestParseTag(t *testing.T) { + tests := []struct { + name string + tag string + expected *Annotation + }{ + { + name: "解析登录检查标签", + tag: "sa_check_login", + expected: &Annotation{ + CheckLogin: true, + }, + }, + { + name: "解析角色检查标签", + tag: "sa_check_role=Admin|SuperAdmin", + expected: &Annotation{ + CheckRole: []string{"Admin", "SuperAdmin"}, + }, + }, + { + name: "解析权限检查标签", + tag: "sa_check_permission=user.read|user.write", + expected: &Annotation{ + CheckPermission: []string{"user.read", "user.write"}, + }, + }, + { + name: "解析忽略认证标签", + tag: "sa_ignore", + expected: &Annotation{ + Ignore: true, + }, + }, + { + name: "解析封禁检查标签", + tag: "sa_check_disable", + expected: &Annotation{ + CheckDisable: true, + }, + }, + { + name: "空标签", + tag: "", + expected: &Annotation{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ParseTag(tt.tag) + assert.Equal(t, tt.expected.CheckLogin, result.CheckLogin) + assert.Equal(t, tt.expected.CheckRole, result.CheckRole) + assert.Equal(t, tt.expected.CheckPermission, result.CheckPermission) + assert.Equal(t, tt.expected.CheckDisable, result.CheckDisable) + assert.Equal(t, tt.expected.Ignore, result.Ignore) + }) + } +} + +// TestAnnotationValidate 测试注解验证功能 +func TestAnnotationValidate(t *testing.T) { + tests := []struct { + name string + annotation *Annotation + valid bool + }{ + { + name: "有效的单一检查", + annotation: &Annotation{ + CheckLogin: true, + }, + valid: true, + }, + { + name: "有效的忽略标记", + annotation: &Annotation{ + Ignore: true, + CheckLogin: true, // 即使有其他标记,忽略时仍然有效 + }, + valid: true, + }, + { + name: "有效的空注解", + annotation: &Annotation{}, + valid: true, + }, + { + name: "无效的多重检查", + annotation: &Annotation{ + CheckLogin: true, + CheckRole: []string{"Admin"}, + }, + valid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.annotation.Validate() + assert.Equal(t, tt.valid, result) + }) + } +} diff --git a/integrations/hertz/context.go b/integrations/hertz/context.go new file mode 100644 index 0000000..95becf1 --- /dev/null +++ b/integrations/hertz/context.go @@ -0,0 +1,167 @@ +package hertz + +import ( + "github.com/click33/sa-token-go/core/adapter" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/protocol" +) + +// HertzContext Hertz request context adapter | Gin请求上下文适配器 +type HertzContext struct { + c *app.RequestContext + aborted bool +} + +// NewHertzContext creates a Hertz context adapter | 创建Hertz上下文适配器 +func NewHertzContext(c *app.RequestContext) adapter.RequestContext { + return &HertzContext{c: c} +} + +// GetHeader gets request header | 获取请求头 +func (h *HertzContext) GetHeader(key string) string { + return string(h.c.GetHeader(key)) +} + +// GetQuery gets query parameter | 获取查询参数 +func (h *HertzContext) GetQuery(key string) string { + return h.c.Query(key) +} + +// GetCookie gets cookie | 获取Cookie +func (h *HertzContext) GetCookie(key string) string { + cookie := h.c.Cookie(key) + return string(cookie) +} + +// SetHeader sets response header | 设置响应头 +func (h *HertzContext) SetHeader(key, value string) { + h.c.Header(key, value) +} + +// SetCookie sets cookie | 设置Cookie +func (h *HertzContext) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { + h.c.SetCookie(name, value, maxAge, path, domain, protocol.CookieSameSiteLaxMode, secure, httpOnly) +} + +// GetClientIP gets client IP address | 获取客户端IP地址 +func (h *HertzContext) GetClientIP() string { + return h.c.ClientIP() +} + +// GetMethod gets request method | 获取请求方法 +func (h *HertzContext) GetMethod() string { + return string(h.c.Method()) +} + +// GetPath gets request path | 获取请求路径 +func (h *HertzContext) GetPath() string { + return string(h.c.Path()) +} + +// Set sets context value | 设置上下文值 +func (h *HertzContext) Set(key string, value interface{}) { + h.c.Set(key, value) +} + +// Get gets context value | 获取上下文值 +func (h *HertzContext) Get(key string) (interface{}, bool) { + return h.c.Get(key) +} + +// ============ Additional Required Methods | 额外必需的方法 ============ + +// GetHeaders implements adapter.RequestContext. +func (h *HertzContext) GetHeaders() map[string][]string { + header := h.c.Request.Header.Header() + headers := make(map[string][]string) + for _, key := range header { + headers[string(key)] = []string{string(key)} + } + return headers +} + +// GetQueryAll implements adapter.RequestContext. +func (h *HertzContext) GetQueryAll() map[string][]string { + args := h.c.QueryArgs() + params := make(map[string][]string) + args.VisitAll(func(key, value []byte) { + params[string(key)] = []string{string(value)} + }) + return params +} + +// GetPostForm implements adapter.RequestContext. +func (h *HertzContext) GetPostForm(key string) string { + return h.c.PostForm(key) +} + +// GetBody implements adapter.RequestContext. +func (h *HertzContext) GetBody() ([]byte, error) { + return h.c.GetRawData(), nil +} + +// GetURL implements adapter.RequestContext. +func (h *HertzContext) GetURL() string { + return string(h.c.Request.URI().RequestURI()) +} + +// GetUserAgent implements adapter.RequestContext. +func (h *HertzContext) GetUserAgent() string { + return string(h.c.UserAgent()) +} + +// SetCookieWithOptions implements adapter.RequestContext. +func (h *HertzContext) SetCookieWithOptions(options *adapter.CookieOptions) { + // Set SameSite attribute + var sameSite protocol.CookieSameSite + switch options.SameSite { + case "Strict": + sameSite = protocol.CookieSameSiteStrictMode + case "Lax": + sameSite = protocol.CookieSameSiteLaxMode + case "None": + sameSite = protocol.CookieSameSiteNoneMode + } + h.c.SetCookie( + options.Name, + options.Value, + options.MaxAge, + options.Path, + options.Domain, + sameSite, + options.Secure, + options.HttpOnly, + ) +} + +// GetString implements adapter.RequestContext. +func (h *HertzContext) GetString(key string) string { + value, exists := h.c.Get(key) + if !exists { + return "" + } + if str, ok := value.(string); ok { + return str + } + return "" +} + +// MustGet implements adapter.RequestContext. +func (h *HertzContext) MustGet(key string) any { + value, exists := h.c.Get(key) + if !exists { + panic("key not found: " + key) + } + return value +} + +// Abort implements adapter.RequestContext. +func (h *HertzContext) Abort() { + h.aborted = true + h.c.Abort() +} + +// IsAborted implements adapter.RequestContext. +func (h *HertzContext) IsAborted() bool { + return h.aborted +} diff --git a/integrations/hertz/export.go b/integrations/hertz/export.go new file mode 100644 index 0000000..ee2241a --- /dev/null +++ b/integrations/hertz/export.go @@ -0,0 +1,364 @@ +package hertz + +import ( + "time" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/stputil" +) + +// ============ Re-export core types | 重新导出核心类型 ============ + +// Configuration related types | 配置相关类型 +type ( + Config = core.Config + CookieConfig = core.CookieConfig + TokenStyle = core.TokenStyle +) + +// Token style constants | Token风格常量 +const ( + TokenStyleUUID = core.TokenStyleUUID + TokenStyleSimple = core.TokenStyleSimple + TokenStyleRandom32 = core.TokenStyleRandom32 + TokenStyleRandom64 = core.TokenStyleRandom64 + TokenStyleRandom128 = core.TokenStyleRandom128 + TokenStyleJWT = core.TokenStyleJWT + TokenStyleHash = core.TokenStyleHash + TokenStyleTimestamp = core.TokenStyleTimestamp + TokenStyleTik = core.TokenStyleTik +) + +// Core types | 核心类型 +type ( + Manager = core.Manager + TokenInfo = core.TokenInfo + Session = core.Session + TokenGenerator = core.TokenGenerator + SaTokenContext = core.SaTokenContext + Builder = core.Builder + NonceManager = core.NonceManager + RefreshTokenInfo = core.RefreshTokenInfo + RefreshTokenManager = core.RefreshTokenManager + OAuth2Server = core.OAuth2Server + OAuth2Client = core.OAuth2Client + OAuth2AccessToken = core.OAuth2AccessToken + OAuth2GrantType = core.OAuth2GrantType +) + +// Adapter interfaces | 适配器接口 +type ( + Storage = core.Storage + RequestContext = core.RequestContext +) + +// Event related types | 事件相关类型 +type ( + EventListener = core.EventListener + EventManager = core.EventManager + EventData = core.EventData + Event = core.Event + ListenerFunc = core.ListenerFunc + ListenerConfig = core.ListenerConfig +) + +// Event constants | 事件常量 +const ( + EventLogin = core.EventLogin + EventLogout = core.EventLogout + EventKickout = core.EventKickout + EventDisable = core.EventDisable + EventUntie = core.EventUntie + EventRenew = core.EventRenew + EventCreateSession = core.EventCreateSession + EventDestroySession = core.EventDestroySession + EventPermissionCheck = core.EventPermissionCheck + EventRoleCheck = core.EventRoleCheck + EventAll = core.EventAll +) + +// OAuth2 grant type constants | OAuth2授权类型常量 +const ( + GrantTypeAuthorizationCode = core.GrantTypeAuthorizationCode + GrantTypeRefreshToken = core.GrantTypeRefreshToken + GrantTypeClientCredentials = core.GrantTypeClientCredentials + GrantTypePassword = core.GrantTypePassword +) + +// Utility functions | 工具函数 +var ( + RandomString = core.RandomString + IsEmpty = core.IsEmpty + IsNotEmpty = core.IsNotEmpty + DefaultString = core.DefaultString + ContainsString = core.ContainsString + RemoveString = core.RemoveString + UniqueStrings = core.UniqueStrings + MergeStrings = core.MergeStrings + MatchPattern = core.MatchPattern +) + +// ============ Core constructor functions | 核心构造函数 ============ + +// DefaultConfig returns default configuration | 返回默认配置 +func DefaultConfig() *Config { + return core.DefaultConfig() +} + +// NewManager creates a new authentication manager | 创建新的认证管理器 +func NewManager(storage Storage, cfg *Config) *Manager { + return core.NewManager(storage, cfg) +} + +// NewContext creates a new Sa-Token context | 创建新的Sa-Token上下文 +func NewContext(ctx RequestContext, mgr *Manager) *SaTokenContext { + return core.NewContext(ctx, mgr) +} + +// NewSession creates a new session | 创建新的Session +func NewSession(id string, storage Storage, prefix string) *Session { + return core.NewSession(id, storage, prefix) +} + +// LoadSession loads an existing session | 加载已存在的Session +func LoadSession(id string, storage Storage, prefix string) (*Session, error) { + return core.LoadSession(id, storage, prefix) +} + +// NewTokenGenerator creates a new token generator | 创建新的Token生成器 +func NewTokenGenerator(cfg *Config) *TokenGenerator { + return core.NewTokenGenerator(cfg) +} + +// NewEventManager creates a new event manager | 创建新的事件管理器 +func NewEventManager() *EventManager { + return core.NewEventManager() +} + +// NewBuilder creates a new builder for fluent configuration | 创建新的Builder构建器(用于流式配置) +func NewBuilder() *Builder { + return core.NewBuilder() +} + +// NewNonceManager creates a new nonce manager | 创建新的Nonce管理器 +func NewNonceManager(storage Storage, prefix string, ttl ...int64) *NonceManager { + return core.NewNonceManager(storage, prefix, ttl...) +} + +// NewRefreshTokenManager creates a new refresh token manager | 创建新的刷新令牌管理器 +func NewRefreshTokenManager(storage Storage, prefix string, cfg *Config) *RefreshTokenManager { + return core.NewRefreshTokenManager(storage, prefix, cfg) +} + +// NewOAuth2Server creates a new OAuth2 server | 创建新的OAuth2服务器 +func NewOAuth2Server(storage Storage, prefix string) *OAuth2Server { + return core.NewOAuth2Server(storage, prefix) +} + +// ============ Global StpUtil functions | 全局StpUtil函数 ============ + +// SetManager sets the global Manager (must be called first) | 设置全局Manager(必须先调用此方法) +func SetManager(mgr *Manager) { + stputil.SetManager(mgr) +} + +// GetManager gets the global Manager | 获取全局Manager +func GetManager() *Manager { + return stputil.GetManager() +} + +// ============ Authentication | 登录认证 ============ + +// Login performs user login | 用户登录 +func Login(loginID interface{}, device ...string) (string, error) { + return stputil.Login(loginID, device...) +} + +// LoginByToken performs login with specified token | 使用指定Token登录 +func LoginByToken(loginID interface{}, tokenValue string, device ...string) error { + return stputil.LoginByToken(loginID, tokenValue, device...) +} + +// Logout performs user logout | 用户登出 +func Logout(loginID interface{}, device ...string) error { + return stputil.Logout(loginID, device...) +} + +// LogoutByToken performs logout by token | 根据Token登出 +func LogoutByToken(tokenValue string) error { + return stputil.LogoutByToken(tokenValue) +} + +// IsLogin checks if the user is logged in | 检查用户是否已登录 +func IsLogin(tokenValue string) bool { + return stputil.IsLogin(tokenValue) +} + +// CheckLoginByToken checks login status (throws error if not logged in) | 检查登录状态(未登录抛出错误) +func CheckLoginByToken(tokenValue string) error { + return stputil.CheckLogin(tokenValue) +} + +// GetLoginID gets the login ID from token | 从Token获取登录ID +func GetLoginID(tokenValue string) (string, error) { + return stputil.GetLoginID(tokenValue) +} + +// GetLoginIDNotCheck gets login ID without checking | 获取登录ID(不检查) +func GetLoginIDNotCheck(tokenValue string) (string, error) { + return stputil.GetLoginIDNotCheck(tokenValue) +} + +// GetTokenValue gets the token value for a login ID | 获取登录ID对应的Token值 +func GetTokenValue(loginID interface{}, device ...string) (string, error) { + return stputil.GetTokenValue(loginID, device...) +} + +// GetTokenInfo gets token information | 获取Token信息 +func GetTokenInfo(tokenValue string) (*TokenInfo, error) { + return stputil.GetTokenInfo(tokenValue) +} + +// ============ Kickout | 踢人下线 ============ + +// Kickout kicks out a user session | 踢人下线 +func Kickout(loginID interface{}, device ...string) error { + return stputil.Kickout(loginID, device...) +} + +// ============ Account Disable | 账号封禁 ============ + +// Disable disables an account for specified duration | 封禁账号(指定时长) +func Disable(loginID interface{}, duration time.Duration) error { + return stputil.Disable(loginID, duration) +} + +// IsDisable checks if an account is disabled | 检查账号是否被封禁 +func IsDisable(loginID interface{}) bool { + return stputil.IsDisable(loginID) +} + +// CheckDisableByToken checks if account is disabled (throws error if disabled) | 检查Token对应账号是否被封禁(被封禁则抛出错误) +func CheckDisableByToken(tokenValue string) error { + return stputil.CheckDisable(tokenValue) +} + +// GetDisableTime gets remaining disabled time | 获取账号剩余封禁时间 +func GetDisableTime(loginID interface{}) (int64, error) { + return stputil.GetDisableTime(loginID) +} + +// Untie unties/unlocks an account | 解除账号封禁 +func Untie(loginID interface{}) error { + return stputil.Untie(loginID) +} + +// ============ Permission Check | 权限验证 ============ + +// CheckPermissionByToken checks if the token has specified permission | 检查Token是否拥有指定权限 +func CheckPermissionByToken(tokenValue string, permission string) error { + return stputil.CheckPermission(tokenValue, permission) +} + +// HasPermission checks if the account has specified permission (returns bool) | 检查账号是否拥有指定权限(返回布尔值) +func HasPermission(loginID interface{}, permission string) bool { + return stputil.HasPermission(loginID, permission) +} + +// CheckPermissionAndByToken checks if the token has all specified permissions (AND logic) | 检查Token是否拥有所有指定权限(AND逻辑) +func CheckPermissionAndByToken(tokenValue string, permissions []string) error { + return stputil.CheckPermissionAnd(tokenValue, permissions) +} + +// CheckPermissionOrByToken checks if the token has any of the specified permissions (OR logic) | 检查Token是否拥有指定权限中的任意一个(OR逻辑) +func CheckPermissionOrByToken(tokenValue string, permissions []string) error { + return stputil.CheckPermissionOr(tokenValue, permissions) +} + +// GetPermissionListByToken gets the permission list for a token | 获取Token的权限列表 +func GetPermissionListByToken(tokenValue string) ([]string, error) { + return stputil.GetPermissionList(tokenValue) +} + +// ============ Role Check | 角色验证 ============ + +// CheckRoleByToken checks if the token has specified role | 检查Token是否拥有指定角色 +func CheckRoleByToken(tokenValue string, role string) error { + return stputil.CheckRole(tokenValue, role) +} + +// HasRole checks if the account has specified role (returns bool) | 检查账号是否拥有指定角色(返回布尔值) +func HasRole(loginID interface{}, role string) bool { + return stputil.HasRole(loginID, role) +} + +// CheckRoleAndByToken checks if the token has all specified roles (AND logic) | 检查Token是否拥有所有指定角色(AND逻辑) +func CheckRoleAndByToken(tokenValue string, roles []string) error { + return stputil.CheckRoleAnd(tokenValue, roles) +} + +// CheckRoleOrByToken checks if the token has any of the specified roles (OR logic) | 检查Token是否拥有指定角色中的任意一个(OR逻辑) +func CheckRoleOrByToken(tokenValue string, roles []string) error { + return stputil.CheckRoleOr(tokenValue, roles) +} + +// GetRoleListByToken gets the role list for a token | 获取Token的角色列表 +func GetRoleListByToken(tokenValue string) ([]string, error) { + return stputil.GetRoleList(tokenValue) +} + +// ============ Session Management | Session管理 ============ + +// GetSession gets the session for a login ID | 获取登录ID的Session +func GetSession(loginID interface{}) (*Session, error) { + return stputil.GetSession(loginID) +} + +// GetSessionByToken gets the session by token | 根据Token获取Session +func GetSessionByToken(tokenValue string) (*Session, error) { + return stputil.GetSessionByToken(tokenValue) +} + +// GetTokenSession gets the token session | 获取Token的Session +func GetTokenSession(tokenValue string) (*Session, error) { + return stputil.GetTokenSession(tokenValue) +} + +// ============ Token Renewal | Token续期 ============ +// Note: Token auto-renewal is handled automatically by the manager +// 注意:Token自动续期由管理器自动处理 + +// ============ Security Features | 安全特性 ============ + +// GenerateNonce generates a new nonce token | 生成新的Nonce令牌 +func GenerateNonce() (string, error) { + return stputil.GenerateNonce() +} + +// VerifyNonce verifies a nonce token | 验证Nonce令牌 +func VerifyNonce(nonce string) bool { + return stputil.VerifyNonce(nonce) +} + +// LoginWithRefreshToken performs login and returns refresh token info | 登录并返回刷新令牌信息 +func LoginWithRefreshToken(loginID interface{}, device ...string) (*RefreshTokenInfo, error) { + return stputil.LoginWithRefreshToken(loginID, device...) +} + +// RefreshAccessToken refreshes the access token using a refresh token | 使用刷新令牌刷新访问令牌 +func RefreshAccessToken(refreshToken string) (*RefreshTokenInfo, error) { + return stputil.RefreshAccessToken(refreshToken) +} + +// RevokeRefreshToken revokes a refresh token | 撤销刷新令牌 +func RevokeRefreshToken(refreshToken string) error { + return stputil.RevokeRefreshToken(refreshToken) +} + +// GetOAuth2Server gets the OAuth2 server instance | 获取OAuth2服务器实例 +func GetOAuth2Server() *OAuth2Server { + return stputil.GetOAuth2Server() +} + +// Version Sa-Token-Go version | Sa-Token-Go版本 +const Version = core.Version diff --git a/integrations/hertz/go.mod b/integrations/hertz/go.mod new file mode 100644 index 0000000..557d36e --- /dev/null +++ b/integrations/hertz/go.mod @@ -0,0 +1,12 @@ +module github.com/click33/sa-token-go/integrations/hertz + +go 1.25 + +toolchain go1.24.1 + +require ( + github.com/click33/sa-token-go/core v0.1.7 // indirect + github.com/click33/sa-token-go/stputil v0.1.7 // indirect + github.com/cloudwego/hertz v0.10.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect +) diff --git a/integrations/hertz/go.sum b/integrations/hertz/go.sum new file mode 100644 index 0000000..34b4104 --- /dev/null +++ b/integrations/hertz/go.sum @@ -0,0 +1,4 @@ +github.com/click33/sa-token-go/core v0.1.7/go.mod h1:mb3AQAJIXqx9WdULyn5qjufK1j/u+kgB0q+tafHVhgk= +github.com/click33/sa-token-go/stputil v0.1.7/go.mod h1:YY4NzfwVMwPUQLDBk9C5eVLQ08oI3vNSFQhBuZBPtgY= +github.com/cloudwego/hertz v0.10.3/go.mod h1:W5dUFXZPZkyfjMMo3EQrMQbofuvTsctM9IxmhbkuT18= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= diff --git a/integrations/hertz/plugin.go b/integrations/hertz/plugin.go new file mode 100644 index 0000000..bac0a3c --- /dev/null +++ b/integrations/hertz/plugin.go @@ -0,0 +1,291 @@ +package hertz + +import ( + "context" + "errors" + "net/http" + + "github.com/click33/sa-token-go/core" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/gin-gonic/gin" +) + +// Plugin Hertz plugin for Sa-Token | Hertz插件 +type Plugin struct { + manager *core.Manager +} + +// NewPlugin creates a Hertz plugin | 创建Hertz插件 +func NewPlugin(manager *core.Manager) *Plugin { + return &Plugin{ + manager: manager, + } +} + +// AuthMiddleware authentication middleware | 认证中间件 +func (p *Plugin) AuthMiddleware() app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + hCtx := NewHertzContext(c) + saCtx := core.NewContext(hCtx, p.manager) + + // Check login | 检查登录 + if err := saCtx.CheckLogin(); err != nil { + writeErrorResponse(c, err) + c.Abort() + return + } + + // Store Sa-Token context in Hertz context | 将Sa-Token上下文存储到Hertz上下文 + c.Set("satoken", saCtx) + c.Next(ctx) + } +} + +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) app.HandlerFunc { + return func(c context.Context, ctx *app.RequestContext) { + path := string(ctx.Path()) + token := string(ctx.GetHeader(p.manager.GetConfig().TokenName)) + if token == "" { + token = string(ctx.Cookie(p.manager.GetConfig().TokenName)) + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + writeErrorResponse(ctx, core.NewPathAuthRequiredError(path)) + ctx.Abort() + return + } + + if result.IsValid && result.TokenInfo != nil { + hCtx := NewHertzContext(ctx) + saCtx := core.NewContext(hCtx, p.manager) + ctx.Set("satoken", saCtx) + ctx.Set("loginID", result.LoginID()) + } + + ctx.Next(c) + } +} + +// PermissionRequired permission validation middleware | 权限验证中间件 +func (p *Plugin) PermissionRequired(permission string) app.HandlerFunc { + return func(c context.Context, ctx *app.RequestContext) { + hCtx := NewHertzContext(ctx) + saCtx := core.NewContext(hCtx, p.manager) + + // Check login | 检查登录 + if err := saCtx.CheckLogin(); err != nil { + writeErrorResponse(ctx, err) + ctx.Abort() + return + } + + // Check permission | 检查权限 + if !saCtx.HasPermission(permission) { + writeErrorResponse(ctx, core.NewPermissionDeniedError(permission)) + ctx.Abort() + return + } + + ctx.Set("satoken", saCtx) + ctx.Next(c) + } +} + +// RoleRequired role validation middleware | 角色验证中间件 +func (p *Plugin) RoleRequired(role string) app.HandlerFunc { + return func(c context.Context, ctx *app.RequestContext) { + hCtx := NewHertzContext(ctx) + saCtx := core.NewContext(hCtx, p.manager) + + // Check login | 检查登录 + if err := saCtx.CheckLogin(); err != nil { + writeErrorResponse(ctx, err) + ctx.Abort() + return + } + + // Check role | 检查角色 + if !saCtx.HasRole(role) { + writeErrorResponse(ctx, core.NewRoleDeniedError(role)) + ctx.Abort() + return + } + + ctx.Set("satoken", saCtx) + ctx.Next(c) + } +} + +// LoginHandler login handler example | 登录处理器示例 +func (p *Plugin) LoginHandler(c *app.RequestContext) { + var req struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Device string `json:"device"` + } + + if err := c.BindJSON(&req); err != nil { + writeErrorResponse(c, core.NewError(core.CodeBadRequest, "invalid request parameters", err)) + return + } + + // TODO: Validate username and password (should call your user service) | 验证用户名密码(这里应该调用你的用户服务) + // if !validateUser(req.Username, req.Password) { ... } + + // Login | 登录 + device := req.Device + if device == "" { + device = "default" + } + + token, err := p.manager.Login(req.Username, device) + if err != nil { + writeErrorResponse(c, core.NewError(core.CodeServerError, "login failed", err)) + return + } + + // Set cookie (optional) | 设置Cookie(可选) + cfg := p.manager.GetConfig() + if cfg.IsReadCookie { + maxAge := int(cfg.Timeout) + if maxAge < 0 { + maxAge = 0 + } + var sameSite protocol.CookieSameSite + switch cfg.CookieConfig.SameSite { + case "Strict": + sameSite = protocol.CookieSameSiteStrictMode + case "Lax": + sameSite = protocol.CookieSameSiteLaxMode + case "None": + sameSite = protocol.CookieSameSiteNoneMode + } + c.SetCookie( + cfg.TokenName, + token, + maxAge, + cfg.CookieConfig.Path, + cfg.CookieConfig.Domain, + sameSite, + cfg.CookieConfig.Secure, + cfg.CookieConfig.HttpOnly, + ) + } + + writeSuccessResponse(c, utils.H{ + "token": token, + }) +} + +// LogoutHandler logout handler | 登出处理器 +func (p *Plugin) LogoutHandler(c *app.RequestContext) { + hCtx := NewHertzContext(c) + saCtx := core.NewContext(hCtx, p.manager) + + loginID, err := saCtx.GetLoginID() + if err != nil { + writeErrorResponse(c, err) + return + } + + if err := p.manager.Logout(loginID); err != nil { + writeErrorResponse(c, core.NewError(core.CodeServerError, "logout failed", err)) + return + } + + writeSuccessResponse(c, gin.H{ + "message": "logout successful", + }) +} + +// UserInfoHandler user info handler example | 获取用户信息处理器示例 +func (p *Plugin) UserInfoHandler(c *app.RequestContext) { + hCtx := NewHertzContext(c) + saCtx := core.NewContext(hCtx, p.manager) + + loginID, err := saCtx.GetLoginID() + if err != nil { + writeErrorResponse(c, err) + return + } + + // Get user permissions and roles | 获取用户权限和角色 + permissions, _ := p.manager.GetPermissions(loginID) + roles, _ := p.manager.GetRoles(loginID) + + writeSuccessResponse(c, utils.H{ + "loginId": loginID, + "permissions": permissions, + "roles": roles, + }) +} + +// GetSaToken gets Sa-Token context from Gin context | 从Gin上下文获取Sa-Token上下文 +func GetSaToken(c *app.RequestContext) (*core.SaTokenContext, bool) { + satoken, exists := c.Get("satoken") + if !exists { + return nil, false + } + ctx, ok := satoken.(*core.SaTokenContext) + return ctx, ok +} + +// ============ Error Handling Helpers | 错误处理辅助函数 ============ + +// writeErrorResponse writes a standardized error response | 写入标准化的错误响应 +func writeErrorResponse(c *app.RequestContext, err error) { + var saErr *core.SaTokenError + var code int + var message string + var httpStatus int + + // Check if it's a SaTokenError | 检查是否为SaTokenError + if errors.As(err, &saErr) { + code = saErr.Code + message = saErr.Message + httpStatus = getHTTPStatusFromCode(code) + } else { + // Handle standard errors | 处理标准错误 + code = core.CodeServerError + message = err.Error() + httpStatus = http.StatusInternalServerError + } + + c.JSON(httpStatus, utils.H{ + "code": code, + "message": message, + "error": err.Error(), + }) +} + +// writeSuccessResponse writes a standardized success response | 写入标准化的成功响应 +func writeSuccessResponse(c *app.RequestContext, data interface{}) { + c.JSON(http.StatusOK, utils.H{ + "code": core.CodeSuccess, + "message": "success", + "data": data, + }) +} + +// getHTTPStatusFromCode converts Sa-Token error code to HTTP status | 将Sa-Token错误码转换为HTTP状态码 +func getHTTPStatusFromCode(code int) int { + switch code { + case core.CodeNotLogin: + return http.StatusUnauthorized + case core.CodePermissionDenied: + return http.StatusForbidden + case core.CodeBadRequest: + return http.StatusBadRequest + case core.CodeNotFound: + return http.StatusNotFound + case core.CodeServerError: + return http.StatusInternalServerError + default: + return http.StatusInternalServerError + } +} -- Gitee From 3d7467650db48705879293d3f5715e6564d5eddf Mon Sep 17 00:00:00 2001 From: yuegc Date: Sun, 25 Jan 2026 00:52:39 +0800 Subject: [PATCH 11/28] =?UTF-8?q?feat:=201=E3=80=81GetHeaders=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E8=B0=83=E6=95=B4=202=E3=80=81context=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=91=BD=E5=90=8D=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integrations/hertz/annotation.go | 76 +++++++++++++-------------- integrations/hertz/annotation_test.go | 64 +++++++++++----------- integrations/hertz/context.go | 10 ++-- integrations/hertz/plugin.go | 52 +++++++++--------- 4 files changed, 100 insertions(+), 102 deletions(-) diff --git a/integrations/hertz/annotation.go b/integrations/hertz/annotation.go index 783e9ca..0283554 100644 --- a/integrations/hertz/annotation.go +++ b/integrations/hertz/annotation.go @@ -91,45 +91,45 @@ func (a *Annotation) Validate() bool { // GetHandler gets handler with annotations | 获取带注解的处理器 func GetHandler(handler interface{}, annotations ...*Annotation) app.HandlerFunc { - return func(c context.Context, ctx *app.RequestContext) { + return func(ctx context.Context, c *app.RequestContext) { if len(annotations) > 0 && annotations[0].Ignore { - if callHandler(handler, c, ctx) { + if callHandler(handler, ctx, c) { return } - ctx.Next(c) + c.Next(ctx) return } - hCtx := NewHertzContext(ctx) + hCtx := NewHertzContext(c) saCtx := core.NewContext(hCtx, stputil.GetManager()) token := saCtx.GetTokenValue() - fmt.Printf("Debug Handler: token='%s', isLogin=%v, headers=%v\n", token, stputil.IsLogin(token), ctx.Request.Header.String()) + fmt.Printf("Debug Handler: token='%s', isLogin=%v, headers=%v\n", token, stputil.IsLogin(token), c.Request.Header.String()) if token == "" { - writeErrorResponse(ctx, core.NewNotLoginError()) - ctx.Abort() + writeErrorResponse(c, core.NewNotLoginError()) + c.Abort() return } if !stputil.IsLogin(token) { - writeErrorResponse(ctx, core.NewNotLoginError()) - ctx.Abort() + writeErrorResponse(c, core.NewNotLoginError()) + c.Abort() return } loginID, err := stputil.GetLoginID(token) if err != nil { - writeErrorResponse(ctx, err) - ctx.Abort() + writeErrorResponse(c, err) + c.Abort() return } // Check if account is disabled | 检查是否被封禁 if len(annotations) > 0 && annotations[0].CheckDisable { if stputil.IsDisable(loginID) { - writeErrorResponse(ctx, core.NewAccountDisabledError(loginID)) - ctx.Abort() + writeErrorResponse(c, core.NewAccountDisabledError(loginID)) + c.Abort() return } } @@ -144,8 +144,8 @@ func GetHandler(handler interface{}, annotations ...*Annotation) app.HandlerFunc } } if !hasPermission { - writeErrorResponse(ctx, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) - ctx.Abort() + writeErrorResponse(c, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) + c.Abort() return } } @@ -160,21 +160,21 @@ func GetHandler(handler interface{}, annotations ...*Annotation) app.HandlerFunc } } if !hasRole { - writeErrorResponse(ctx, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) - ctx.Abort() + writeErrorResponse(c, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) + c.Abort() return } } // All checks passed, execute original handler or continue | 所有检查通过,执行原函数或继续 - if callHandler(handler, c, ctx) { + if callHandler(handler, ctx, c) { return } - ctx.Next(c) + c.Next(ctx) } } -func callHandler(handler interface{}, c context.Context, ctx *app.RequestContext) bool { +func callHandler(handler interface{}, ctx context.Context, c *app.RequestContext) bool { if handler == nil { return false } @@ -184,13 +184,13 @@ func callHandler(handler interface{}, c context.Context, ctx *app.RequestContext if h == nil { return false } - h(ctx) + h(c) return true case app.HandlerFunc: if h == nil { return false } - h(c, ctx) + h(ctx, c) return true } @@ -289,40 +289,40 @@ func (h *HandlerWithAnnotations) ToGinHandler() app.HandlerFunc { // Middleware 创建中间件版本 func Middleware(annotations ...*Annotation) app.HandlerFunc { - return func(c context.Context, ctx *app.RequestContext) { + return func(ctx context.Context, c *app.RequestContext) { if len(annotations) > 0 && annotations[0].Ignore { - ctx.Next(c) + c.Next(ctx) return } - hCtx := NewHertzContext(ctx) + hCtx := NewHertzContext(c) saCtx := core.NewContext(hCtx, stputil.GetManager()) token := saCtx.GetTokenValue() if token == "" { - writeErrorResponse(ctx, core.NewNotLoginError()) - ctx.Abort() + writeErrorResponse(c, core.NewNotLoginError()) + c.Abort() return } if !stputil.IsLogin(token) { - writeErrorResponse(ctx, core.NewNotLoginError()) - ctx.Abort() + writeErrorResponse(c, core.NewNotLoginError()) + c.Abort() return } loginID, err := stputil.GetLoginID(token) if err != nil { - writeErrorResponse(ctx, err) - ctx.Abort() + writeErrorResponse(c, err) + c.Abort() return } // 检查是否被封禁 if len(annotations) > 0 && annotations[0].CheckDisable { if stputil.IsDisable(loginID) { - writeErrorResponse(ctx, core.NewAccountDisabledError(loginID)) - ctx.Abort() + writeErrorResponse(c, core.NewAccountDisabledError(loginID)) + c.Abort() return } } @@ -337,8 +337,8 @@ func Middleware(annotations ...*Annotation) app.HandlerFunc { } } if !hasPermission { - writeErrorResponse(ctx, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) - ctx.Abort() + writeErrorResponse(c, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) + c.Abort() return } } @@ -353,13 +353,13 @@ func Middleware(annotations ...*Annotation) app.HandlerFunc { } } if !hasRole { - writeErrorResponse(ctx, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) - ctx.Abort() + writeErrorResponse(c, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) + c.Abort() return } } // 所有检查通过,继续下一个处理器 - ctx.Next(c) + c.Next(ctx) } } diff --git a/integrations/hertz/annotation_test.go b/integrations/hertz/annotation_test.go index d12d87d..4a976e3 100644 --- a/integrations/hertz/annotation_test.go +++ b/integrations/hertz/annotation_test.go @@ -65,8 +65,8 @@ func TestCheckRole_WithValidRole(t *testing.T) { router := setupTestRouter() // 设置路由 - 使用 CheckRole 作为中间件 - router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/admin", CheckRole("Admin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) // 创建一个具有 Admin 角色的用户 @@ -89,8 +89,8 @@ func TestCheckRole_WithInvalidRole(t *testing.T) { router := setupTestRouter() // 设置路由 - router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/admin", CheckRole("Admin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) // 创建一个只有 User 角色的用户 @@ -110,8 +110,8 @@ func TestCheckRole_MultipleRoles(t *testing.T) { router := setupTestRouter() // 设置路由 - 需要 Admin 或 SuperAdmin 角色 - router.GET("/manage", CheckRole("Admin", "SuperAdmin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/manage", CheckRole("Admin", "SuperAdmin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) // 测试具有 SuperAdmin 角色的用户 @@ -128,8 +128,8 @@ func TestCheckRole_MultipleRoles(t *testing.T) { func TestCheckRole_NoToken(t *testing.T) { router := setupTestRouter() - router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/admin", CheckRole("Admin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) w := ut.PerformRequest(router.Engine, "GET", "/admin", nil) @@ -142,8 +142,8 @@ func TestCheckRole_NoToken(t *testing.T) { func TestCheckRole_InvalidToken(t *testing.T) { router := setupTestRouter() - router.GET("/admin", CheckRole("Admin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/admin", CheckRole("Admin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) w := ut.PerformRequest(router.Engine, "GET", "/admin", nil, @@ -157,8 +157,8 @@ func TestCheckRole_InvalidToken(t *testing.T) { func TestCheckPermission_WithValidPermission(t *testing.T) { router := setupTestRouter() - router.GET("/users", CheckPermission("user.read"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/users", CheckPermission("user.read"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) token := mockLoginWithPermission("user789", []string{"user.read"}) @@ -174,8 +174,8 @@ func TestCheckPermission_WithValidPermission(t *testing.T) { func TestCheckPermission_WithInvalidPermission(t *testing.T) { router := setupTestRouter() - router.GET("/users", CheckPermission("user.delete"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "success"}) + router.GET("/users", CheckPermission("user.delete"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "success"}) }) token := mockLoginWithPermission("user789", []string{"user.read"}) @@ -191,8 +191,8 @@ func TestCheckPermission_WithInvalidPermission(t *testing.T) { func TestCheckLogin_Success(t *testing.T) { router := setupTestRouter() - router.GET("/profile", CheckLogin(), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "profile data"}) + router.GET("/profile", CheckLogin(), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "profile data"}) }) token := mockLogin("user999") @@ -208,8 +208,8 @@ func TestCheckLogin_Success(t *testing.T) { func TestCheckLogin_Failed(t *testing.T) { router := setupTestRouter() - router.GET("/profile", CheckLogin(), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "profile data"}) + router.GET("/profile", CheckLogin(), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "profile data"}) }) w := ut.PerformRequest(router.Engine, "GET", "/profile", nil) @@ -222,8 +222,8 @@ func TestCheckLogin_Failed(t *testing.T) { func TestCheckDisable_NotDisabled(t *testing.T) { router := setupTestRouter() - router.GET("/resource", CheckDisable(), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "resource data"}) + router.GET("/resource", CheckDisable(), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "resource data"}) }) token := mockLogin("user101") @@ -239,8 +239,8 @@ func TestCheckDisable_NotDisabled(t *testing.T) { func TestCheckDisable_IsDisabled(t *testing.T) { router := setupTestRouter() - router.GET("/resource", CheckDisable(), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "resource data"}) + router.GET("/resource", CheckDisable(), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "resource data"}) }) loginID := "user102" @@ -260,8 +260,8 @@ func TestCheckDisable_IsDisabled(t *testing.T) { func TestIgnore_SkipsAuthentication(t *testing.T) { router := setupTestRouter() - router.GET("/public", Ignore(), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "public data"}) + router.GET("/public", Ignore(), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "public data"}) }) w := ut.PerformRequest(router.Engine, "GET", "/public", nil) @@ -277,8 +277,8 @@ func TestChainedMiddleware_CheckRoleAndHandler(t *testing.T) { // 模拟用户示例代码的使用方式 safeGroup := router.Group("/safe") { - safeGroup.GET("", CheckRole("SuperAdmin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "safe settings"}) + safeGroup.GET("", CheckRole("SuperAdmin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "safe settings"}) }) } @@ -298,8 +298,8 @@ func TestChainedMiddleware_CheckRoleAndHandler_NoRole(t *testing.T) { safeGroup := router.Group("/safe") { - safeGroup.GET("", CheckRole("SuperAdmin"), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "safe settings"}) + safeGroup.GET("", CheckRole("SuperAdmin"), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "safe settings"}) }) } @@ -320,8 +320,8 @@ func TestGetHandler_WithNilHandler(t *testing.T) { // 直接使用 GetHandler 创建中间件 middleware := GetHandler(nil, &Annotation{CheckRole: []string{"Admin"}}) - router.GET("/test", middleware, func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"message": "test passed"}) + router.GET("/test", middleware, func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"message": "test passed"}) }) token := mockLoginWithRole("testuser", []string{"Admin"}) @@ -339,8 +339,8 @@ func TestMiddleware_CheckRole(t *testing.T) { router := setupTestRouter() // 使用 Middleware 函数 - router.GET("/api/data", Middleware(&Annotation{CheckRole: []string{"Admin"}}), func(c context.Context, ctx *app.RequestContext) { - ctx.JSON(http.StatusOK, utils.H{"data": "sensitive data"}) + router.GET("/api/data", Middleware(&Annotation{CheckRole: []string{"Admin"}}), func(ctx context.Context, c *app.RequestContext) { + c.JSON(http.StatusOK, utils.H{"data": "sensitive data"}) }) token := mockLoginWithRole("admin999", []string{"Admin"}) diff --git a/integrations/hertz/context.go b/integrations/hertz/context.go index 95becf1..d9d2097 100644 --- a/integrations/hertz/context.go +++ b/integrations/hertz/context.go @@ -72,19 +72,17 @@ func (h *HertzContext) Get(key string) (interface{}, bool) { // GetHeaders implements adapter.RequestContext. func (h *HertzContext) GetHeaders() map[string][]string { - header := h.c.Request.Header.Header() headers := make(map[string][]string) - for _, key := range header { - headers[string(key)] = []string{string(key)} - } + h.c.Request.Header.VisitAll(func(key, value []byte) { + headers[string(key)] = []string{string(value)} + }) return headers } // GetQueryAll implements adapter.RequestContext. func (h *HertzContext) GetQueryAll() map[string][]string { - args := h.c.QueryArgs() params := make(map[string][]string) - args.VisitAll(func(key, value []byte) { + h.c.QueryArgs().VisitAll(func(key, value []byte) { params[string(key)] = []string{string(value)} }) return params diff --git a/integrations/hertz/plugin.go b/integrations/hertz/plugin.go index bac0a3c..98bf441 100644 --- a/integrations/hertz/plugin.go +++ b/integrations/hertz/plugin.go @@ -45,79 +45,79 @@ func (p *Plugin) AuthMiddleware() app.HandlerFunc { // PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) app.HandlerFunc { - return func(c context.Context, ctx *app.RequestContext) { - path := string(ctx.Path()) - token := string(ctx.GetHeader(p.manager.GetConfig().TokenName)) + return func(ctx context.Context, c *app.RequestContext) { + path := string(c.Path()) + token := string(c.GetHeader(p.manager.GetConfig().TokenName)) if token == "" { - token = string(ctx.Cookie(p.manager.GetConfig().TokenName)) + token = string(c.Cookie(p.manager.GetConfig().TokenName)) } result := core.ProcessAuth(path, token, config, p.manager) if result.ShouldReject() { - writeErrorResponse(ctx, core.NewPathAuthRequiredError(path)) - ctx.Abort() + writeErrorResponse(c, core.NewPathAuthRequiredError(path)) + c.Abort() return } if result.IsValid && result.TokenInfo != nil { - hCtx := NewHertzContext(ctx) + hCtx := NewHertzContext(c) saCtx := core.NewContext(hCtx, p.manager) - ctx.Set("satoken", saCtx) - ctx.Set("loginID", result.LoginID()) + c.Set("satoken", saCtx) + c.Set("loginID", result.LoginID()) } - ctx.Next(c) + c.Next(ctx) } } // PermissionRequired permission validation middleware | 权限验证中间件 func (p *Plugin) PermissionRequired(permission string) app.HandlerFunc { - return func(c context.Context, ctx *app.RequestContext) { - hCtx := NewHertzContext(ctx) + return func(ctx context.Context, c *app.RequestContext) { + hCtx := NewHertzContext(c) saCtx := core.NewContext(hCtx, p.manager) // Check login | 检查登录 if err := saCtx.CheckLogin(); err != nil { - writeErrorResponse(ctx, err) - ctx.Abort() + writeErrorResponse(c, err) + c.Abort() return } // Check permission | 检查权限 if !saCtx.HasPermission(permission) { - writeErrorResponse(ctx, core.NewPermissionDeniedError(permission)) - ctx.Abort() + writeErrorResponse(c, core.NewPermissionDeniedError(permission)) + c.Abort() return } - ctx.Set("satoken", saCtx) - ctx.Next(c) + c.Set("satoken", saCtx) + c.Next(ctx) } } // RoleRequired role validation middleware | 角色验证中间件 func (p *Plugin) RoleRequired(role string) app.HandlerFunc { - return func(c context.Context, ctx *app.RequestContext) { - hCtx := NewHertzContext(ctx) + return func(ctx context.Context, c *app.RequestContext) { + hCtx := NewHertzContext(c) saCtx := core.NewContext(hCtx, p.manager) // Check login | 检查登录 if err := saCtx.CheckLogin(); err != nil { - writeErrorResponse(ctx, err) - ctx.Abort() + writeErrorResponse(c, err) + c.Abort() return } // Check role | 检查角色 if !saCtx.HasRole(role) { - writeErrorResponse(ctx, core.NewRoleDeniedError(role)) - ctx.Abort() + writeErrorResponse(c, core.NewRoleDeniedError(role)) + c.Abort() return } - ctx.Set("satoken", saCtx) - ctx.Next(c) + c.Set("satoken", saCtx) + c.Next(ctx) } } -- Gitee From 6d2e522561181b12198d49f53b8e9e77eb416493 Mon Sep 17 00:00:00 2001 From: yuegc Date: Mon, 26 Jan 2026 17:32:26 +0800 Subject: [PATCH 12/28] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=20hertz=20?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/gin/gin-example/go.sum | 141 + examples/hertz/herz-example/.gitignore | 37 + examples/hertz/herz-example/.hz | 6 + .../biz/handler/user/user_service.go | 131 + .../hertz/herz-example/biz/model/user/user.go | 3004 +++++++++++++++++ .../hertz/herz-example/biz/router/register.go | 15 + .../biz/router/user/middleware.go | 47 + .../herz-example/biz/router/user/user.go | 27 + examples/hertz/herz-example/build.sh | 6 + examples/hertz/herz-example/go.mod | 28 + examples/hertz/herz-example/go.sum | 119 + examples/hertz/herz-example/idl/user.thrift | 33 + examples/hertz/herz-example/main.go | 22 + examples/hertz/herz-example/router.go | 14 + examples/hertz/herz-example/router_gen.go | 16 + .../hertz/herz-example/script/bootstrap.sh | 5 + go.work | 3 +- go.work.sum | 60 +- integrations/hertz/plugin.go | 8 +- 19 files changed, 3659 insertions(+), 63 deletions(-) create mode 100644 examples/gin/gin-example/go.sum create mode 100644 examples/hertz/herz-example/.gitignore create mode 100644 examples/hertz/herz-example/.hz create mode 100644 examples/hertz/herz-example/biz/handler/user/user_service.go create mode 100644 examples/hertz/herz-example/biz/model/user/user.go create mode 100644 examples/hertz/herz-example/biz/router/register.go create mode 100644 examples/hertz/herz-example/biz/router/user/middleware.go create mode 100644 examples/hertz/herz-example/biz/router/user/user.go create mode 100755 examples/hertz/herz-example/build.sh create mode 100644 examples/hertz/herz-example/go.mod create mode 100644 examples/hertz/herz-example/go.sum create mode 100644 examples/hertz/herz-example/idl/user.thrift create mode 100644 examples/hertz/herz-example/main.go create mode 100644 examples/hertz/herz-example/router.go create mode 100644 examples/hertz/herz-example/router_gen.go create mode 100755 examples/hertz/herz-example/script/bootstrap.sh diff --git a/examples/gin/gin-example/go.sum b/examples/gin/gin-example/go.sum new file mode 100644 index 0000000..9a20c5a --- /dev/null +++ b/examples/gin/gin-example/go.sum @@ -0,0 +1,141 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/click33/sa-token-go/stputil v0.1.7 h1:omAPMerECe8gBRFHLzjxnuNYFPipcHi/gd3U75r4gzg= +github.com/click33/sa-token-go/stputil v0.1.7/go.mod h1:YY4NzfwVMwPUQLDBk9C5eVLQ08oI3vNSFQhBuZBPtgY= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/examples/hertz/herz-example/.gitignore b/examples/hertz/herz-example/.gitignore new file mode 100644 index 0000000..101ea87 --- /dev/null +++ b/examples/hertz/herz-example/.gitignore @@ -0,0 +1,37 @@ +*.o +*.a +*.so +_obj +_test +*.[568vq] +[568vq].out +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* +_testmain.go +*.exe +*.exe~ +*.test +*.prof +*.rar +*.zip +*.gz +*.psd +*.bmd +*.cfg +*.pptx +*.log +*nohup.out +*settings.pyc +*.sublime-project +*.sublime-workspace +!.gitkeep +.DS_Store +/.idea +/.vscode +/output +*.local.yml +dumped_hertz_remote_config.json + \ No newline at end of file diff --git a/examples/hertz/herz-example/.hz b/examples/hertz/herz-example/.hz new file mode 100644 index 0000000..791041d --- /dev/null +++ b/examples/hertz/herz-example/.hz @@ -0,0 +1,6 @@ +// Code generated by hz. DO NOT EDIT. + +hz version: v0.9.7 +handlerDir: "" +modelDir: "" +routerDir: "" diff --git a/examples/hertz/herz-example/biz/handler/user/user_service.go b/examples/hertz/herz-example/biz/handler/user/user_service.go new file mode 100644 index 0000000..53e3d09 --- /dev/null +++ b/examples/hertz/herz-example/biz/handler/user/user_service.go @@ -0,0 +1,131 @@ +// Code generated by hertz generator. + +package user + +import ( + "context" + "time" + + user "github.com/click33/sa-token-go/examples/hertz/herz-example/biz/model/user" + "github.com/click33/sa-token-go/stputil" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/protocol/consts" +) + +// Login . +// @router /login [POST] +func Login(ctx context.Context, c *app.RequestContext) { + var err error + var req user.LoginReq + err = c.BindAndValidate(&req) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + + token, err := stputil.Login(req.GetUserId()) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + loginID, err := stputil.GetLoginID(token) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + err = stputil.SetRoles(loginID, []string{"manager"}) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + + resp := new(user.LoginResp) + resp.Token = token + + c.JSON(consts.StatusOK, resp) +} + +// Public . +// @router /public [GET] +func Public(ctx context.Context, c *app.RequestContext) { + + resp := new(user.MessageResp) + resp.Message = "public" + + c.JSON(consts.StatusOK, resp) +} + +// UserInfo . +// @router /user [GET] +func UserInfo(ctx context.Context, c *app.RequestContext) { + + resp := new(user.UserInfoResp) + loginID, err := stputil.GetLoginID(c.Request.Header.Get("satoken")) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + roles, _ := stputil.GetRoles(loginID) + permissions, _ := stputil.GetPermissions(loginID) + + resp.LoginId = loginID + resp.Roles = roles + resp.Permissions = permissions + + c.JSON(consts.StatusOK, resp) +} + +// Admin . +// @router /admin [GET] +func Admin(ctx context.Context, c *app.RequestContext) { + + resp := new(user.MessageResp) + resp.Message = "admin" + + c.JSON(consts.StatusOK, resp) +} + +// Manager . +// @router /manager [GET] +func Manager(ctx context.Context, c *app.RequestContext) { + + resp := new(user.MessageResp) + resp.Message = "manager" + + c.JSON(consts.StatusOK, resp) +} + +// Sensitive . +// @router /sensitive [GET] +func Sensitive(ctx context.Context, c *app.RequestContext) { + + resp := new(user.SensitiveResp) + loginID, err := stputil.GetLoginID(c.Request.Header.Get("satoken")) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + resp.Sensitive = stputil.IsDisable(loginID) + + c.JSON(consts.StatusOK, resp) +} + +// Disable . +// @router /disable [GET] +func Disable(ctx context.Context, c *app.RequestContext) { + var err error + + loginID, err := stputil.GetLoginID(c.Request.Header.Get("satoken")) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + err = stputil.Disable(loginID, time.Hour) + if err != nil { + c.String(consts.StatusBadRequest, err.Error()) + return + } + resp := new(user.MessageResp) + + c.JSON(consts.StatusOK, resp) +} diff --git a/examples/hertz/herz-example/biz/model/user/user.go b/examples/hertz/herz-example/biz/model/user/user.go new file mode 100644 index 0000000..9cb30f1 --- /dev/null +++ b/examples/hertz/herz-example/biz/model/user/user.go @@ -0,0 +1,3004 @@ +// Code generated by thriftgo (0.4.3). DO NOT EDIT. + +package user + +import ( + "context" + "fmt" + "github.com/apache/thrift/lib/go/thrift" +) + +type LoginReq struct { + UserId string `thrift:"userId,1" json:"userId" query:"userId"` +} + +func NewLoginReq() *LoginReq { + return &LoginReq{} +} + +func (p *LoginReq) InitDefault() { +} + +func (p *LoginReq) GetUserId() (v string) { + return p.UserId +} + +var fieldIDToName_LoginReq = map[int16]string{ + 1: "userId", +} + +func (p *LoginReq) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_LoginReq[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *LoginReq) ReadField1(iprot thrift.TProtocol) error { + + var _field string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _field = v + } + p.UserId = _field + return nil +} + +func (p *LoginReq) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("LoginReq"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *LoginReq) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("userId", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(p.UserId); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} + +func (p *LoginReq) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("LoginReq(%+v)", *p) + +} + +type LoginResp struct { + Token string `thrift:"token,1" form:"token" json:"token" query:"token"` +} + +func NewLoginResp() *LoginResp { + return &LoginResp{} +} + +func (p *LoginResp) InitDefault() { +} + +func (p *LoginResp) GetToken() (v string) { + return p.Token +} + +var fieldIDToName_LoginResp = map[int16]string{ + 1: "token", +} + +func (p *LoginResp) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_LoginResp[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *LoginResp) ReadField1(iprot thrift.TProtocol) error { + + var _field string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _field = v + } + p.Token = _field + return nil +} + +func (p *LoginResp) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("LoginResp"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *LoginResp) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("token", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(p.Token); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} + +func (p *LoginResp) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("LoginResp(%+v)", *p) + +} + +type MessageResp struct { + Message string `thrift:"message,1" form:"message" json:"message" query:"message"` +} + +func NewMessageResp() *MessageResp { + return &MessageResp{} +} + +func (p *MessageResp) InitDefault() { +} + +func (p *MessageResp) GetMessage() (v string) { + return p.Message +} + +var fieldIDToName_MessageResp = map[int16]string{ + 1: "message", +} + +func (p *MessageResp) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_MessageResp[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *MessageResp) ReadField1(iprot thrift.TProtocol) error { + + var _field string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _field = v + } + p.Message = _field + return nil +} + +func (p *MessageResp) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("MessageResp"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *MessageResp) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("message", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(p.Message); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} + +func (p *MessageResp) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("MessageResp(%+v)", *p) + +} + +type UserInfoResp struct { + LoginId string `thrift:"loginId,1" form:"loginId" json:"loginId" query:"loginId"` + Roles []string `thrift:"roles,2,default,list" form:"roles" json:"roles" query:"roles"` + Permissions []string `thrift:"permissions,3,default,list" form:"permissions" json:"permissions" query:"permissions"` +} + +func NewUserInfoResp() *UserInfoResp { + return &UserInfoResp{} +} + +func (p *UserInfoResp) InitDefault() { +} + +func (p *UserInfoResp) GetLoginId() (v string) { + return p.LoginId +} + +func (p *UserInfoResp) GetRoles() (v []string) { + return p.Roles +} + +func (p *UserInfoResp) GetPermissions() (v []string) { + return p.Permissions +} + +var fieldIDToName_UserInfoResp = map[int16]string{ + 1: "loginId", + 2: "roles", + 3: "permissions", +} + +func (p *UserInfoResp) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + case 2: + if fieldTypeId == thrift.LIST { + if err = p.ReadField2(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + case 3: + if fieldTypeId == thrift.LIST { + if err = p.ReadField3(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserInfoResp[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserInfoResp) ReadField1(iprot thrift.TProtocol) error { + + var _field string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _field = v + } + p.LoginId = _field + return nil +} +func (p *UserInfoResp) ReadField2(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return err + } + _field := make([]string, 0, size) + for i := 0; i < size; i++ { + + var _elem string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _elem = v + } + + _field = append(_field, _elem) + } + if err := iprot.ReadListEnd(); err != nil { + return err + } + p.Roles = _field + return nil +} +func (p *UserInfoResp) ReadField3(iprot thrift.TProtocol) error { + _, size, err := iprot.ReadListBegin() + if err != nil { + return err + } + _field := make([]string, 0, size) + for i := 0; i < size; i++ { + + var _elem string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _elem = v + } + + _field = append(_field, _elem) + } + if err := iprot.ReadListEnd(); err != nil { + return err + } + p.Permissions = _field + return nil +} + +func (p *UserInfoResp) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("UserInfoResp"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + if err = p.writeField2(oprot); err != nil { + fieldId = 2 + goto WriteFieldError + } + if err = p.writeField3(oprot); err != nil { + fieldId = 3 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserInfoResp) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("loginId", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(p.LoginId); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} + +func (p *UserInfoResp) writeField2(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("roles", thrift.LIST, 2); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteListBegin(thrift.STRING, len(p.Roles)); err != nil { + return err + } + for _, v := range p.Roles { + if err := oprot.WriteString(v); err != nil { + return err + } + } + if err := oprot.WriteListEnd(); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 2 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 2 end error: ", p), err) +} + +func (p *UserInfoResp) writeField3(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("permissions", thrift.LIST, 3); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteListBegin(thrift.STRING, len(p.Permissions)); err != nil { + return err + } + for _, v := range p.Permissions { + if err := oprot.WriteString(v); err != nil { + return err + } + } + if err := oprot.WriteListEnd(); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 3 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 3 end error: ", p), err) +} + +func (p *UserInfoResp) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserInfoResp(%+v)", *p) + +} + +type SensitiveResp struct { + Sensitive bool `thrift:"sensitive,1" form:"sensitive" json:"sensitive" query:"sensitive"` +} + +func NewSensitiveResp() *SensitiveResp { + return &SensitiveResp{} +} + +func (p *SensitiveResp) InitDefault() { +} + +func (p *SensitiveResp) GetSensitive() (v bool) { + return p.Sensitive +} + +var fieldIDToName_SensitiveResp = map[int16]string{ + 1: "sensitive", +} + +func (p *SensitiveResp) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.BOOL { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_SensitiveResp[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *SensitiveResp) ReadField1(iprot thrift.TProtocol) error { + + var _field bool + if v, err := iprot.ReadBool(); err != nil { + return err + } else { + _field = v + } + p.Sensitive = _field + return nil +} + +func (p *SensitiveResp) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("SensitiveResp"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *SensitiveResp) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("sensitive", thrift.BOOL, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteBool(p.Sensitive); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} + +func (p *SensitiveResp) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("SensitiveResp(%+v)", *p) + +} + +type UserService interface { + Login(ctx context.Context, request *LoginReq) (r *LoginResp, err error) + + Public(ctx context.Context) (r *MessageResp, err error) + + UserInfo(ctx context.Context) (r *UserInfoResp, err error) + + Admin(ctx context.Context) (r *MessageResp, err error) + + Manager(ctx context.Context) (r *MessageResp, err error) + + Disable(ctx context.Context) (r *MessageResp, err error) + + Sensitive(ctx context.Context) (r *SensitiveResp, err error) +} + +type UserServiceClient struct { + c thrift.TClient +} + +func NewUserServiceClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *UserServiceClient { + return &UserServiceClient{ + c: thrift.NewTStandardClient(f.GetProtocol(t), f.GetProtocol(t)), + } +} + +func NewUserServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *UserServiceClient { + return &UserServiceClient{ + c: thrift.NewTStandardClient(iprot, oprot), + } +} + +func NewUserServiceClient(c thrift.TClient) *UserServiceClient { + return &UserServiceClient{ + c: c, + } +} + +func (p *UserServiceClient) Client_() thrift.TClient { + return p.c +} + +func (p *UserServiceClient) Login(ctx context.Context, request *LoginReq) (r *LoginResp, err error) { + var _args UserServiceLoginArgs + _args.Request = request + var _result UserServiceLoginResult + if err = p.Client_().Call(ctx, "Login", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} +func (p *UserServiceClient) Public(ctx context.Context) (r *MessageResp, err error) { + var _args UserServicePublicArgs + var _result UserServicePublicResult + if err = p.Client_().Call(ctx, "Public", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} +func (p *UserServiceClient) UserInfo(ctx context.Context) (r *UserInfoResp, err error) { + var _args UserServiceUserInfoArgs + var _result UserServiceUserInfoResult + if err = p.Client_().Call(ctx, "UserInfo", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} +func (p *UserServiceClient) Admin(ctx context.Context) (r *MessageResp, err error) { + var _args UserServiceAdminArgs + var _result UserServiceAdminResult + if err = p.Client_().Call(ctx, "Admin", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} +func (p *UserServiceClient) Manager(ctx context.Context) (r *MessageResp, err error) { + var _args UserServiceManagerArgs + var _result UserServiceManagerResult + if err = p.Client_().Call(ctx, "Manager", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} +func (p *UserServiceClient) Disable(ctx context.Context) (r *MessageResp, err error) { + var _args UserServiceDisableArgs + var _result UserServiceDisableResult + if err = p.Client_().Call(ctx, "Disable", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} +func (p *UserServiceClient) Sensitive(ctx context.Context) (r *SensitiveResp, err error) { + var _args UserServiceSensitiveArgs + var _result UserServiceSensitiveResult + if err = p.Client_().Call(ctx, "Sensitive", &_args, &_result); err != nil { + return + } + return _result.GetSuccess(), nil +} + +type UserServiceProcessor struct { + processorMap map[string]thrift.TProcessorFunction + handler UserService +} + +func (p *UserServiceProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) { + p.processorMap[key] = processor +} + +func (p *UserServiceProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) { + processor, ok = p.processorMap[key] + return processor, ok +} + +func (p *UserServiceProcessor) ProcessorMap() map[string]thrift.TProcessorFunction { + return p.processorMap +} + +func NewUserServiceProcessor(handler UserService) *UserServiceProcessor { + self := &UserServiceProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)} + self.AddToProcessorMap("Login", &userServiceProcessorLogin{handler: handler}) + self.AddToProcessorMap("Public", &userServiceProcessorPublic{handler: handler}) + self.AddToProcessorMap("UserInfo", &userServiceProcessorUserInfo{handler: handler}) + self.AddToProcessorMap("Admin", &userServiceProcessorAdmin{handler: handler}) + self.AddToProcessorMap("Manager", &userServiceProcessorManager{handler: handler}) + self.AddToProcessorMap("Disable", &userServiceProcessorDisable{handler: handler}) + self.AddToProcessorMap("Sensitive", &userServiceProcessorSensitive{handler: handler}) + return self +} +func (p *UserServiceProcessor) Process(ctx context.Context, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + name, _, seqId, err := iprot.ReadMessageBegin() + if err != nil { + return false, err + } + if processor, ok := p.GetProcessorFunction(name); ok { + return processor.Process(ctx, seqId, iprot, oprot) + } + iprot.Skip(thrift.STRUCT) + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name) + oprot.WriteMessageBegin(name, thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, x +} + +type userServiceProcessorLogin struct { + handler UserService +} + +func (p *userServiceProcessorLogin) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServiceLoginArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("Login", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServiceLoginResult{} + var retval *LoginResp + if retval, err2 = p.handler.Login(ctx, args.Request); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing Login: "+err2.Error()) + oprot.WriteMessageBegin("Login", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("Login", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type userServiceProcessorPublic struct { + handler UserService +} + +func (p *userServiceProcessorPublic) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServicePublicArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("Public", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServicePublicResult{} + var retval *MessageResp + if retval, err2 = p.handler.Public(ctx); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing Public: "+err2.Error()) + oprot.WriteMessageBegin("Public", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("Public", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type userServiceProcessorUserInfo struct { + handler UserService +} + +func (p *userServiceProcessorUserInfo) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServiceUserInfoArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("UserInfo", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServiceUserInfoResult{} + var retval *UserInfoResp + if retval, err2 = p.handler.UserInfo(ctx); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing UserInfo: "+err2.Error()) + oprot.WriteMessageBegin("UserInfo", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("UserInfo", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type userServiceProcessorAdmin struct { + handler UserService +} + +func (p *userServiceProcessorAdmin) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServiceAdminArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("Admin", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServiceAdminResult{} + var retval *MessageResp + if retval, err2 = p.handler.Admin(ctx); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing Admin: "+err2.Error()) + oprot.WriteMessageBegin("Admin", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("Admin", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type userServiceProcessorManager struct { + handler UserService +} + +func (p *userServiceProcessorManager) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServiceManagerArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("Manager", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServiceManagerResult{} + var retval *MessageResp + if retval, err2 = p.handler.Manager(ctx); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing Manager: "+err2.Error()) + oprot.WriteMessageBegin("Manager", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("Manager", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type userServiceProcessorDisable struct { + handler UserService +} + +func (p *userServiceProcessorDisable) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServiceDisableArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("Disable", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServiceDisableResult{} + var retval *MessageResp + if retval, err2 = p.handler.Disable(ctx); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing Disable: "+err2.Error()) + oprot.WriteMessageBegin("Disable", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("Disable", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type userServiceProcessorSensitive struct { + handler UserService +} + +func (p *userServiceProcessorSensitive) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { + args := UserServiceSensitiveArgs{} + if err = args.Read(iprot); err != nil { + iprot.ReadMessageEnd() + x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err.Error()) + oprot.WriteMessageBegin("Sensitive", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return false, err + } + + iprot.ReadMessageEnd() + var err2 error + result := UserServiceSensitiveResult{} + var retval *SensitiveResp + if retval, err2 = p.handler.Sensitive(ctx); err2 != nil { + x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing Sensitive: "+err2.Error()) + oprot.WriteMessageBegin("Sensitive", thrift.EXCEPTION, seqId) + x.Write(oprot) + oprot.WriteMessageEnd() + oprot.Flush(ctx) + return true, err2 + } else { + result.Success = retval + } + if err2 = oprot.WriteMessageBegin("Sensitive", thrift.REPLY, seqId); err2 != nil { + err = err2 + } + if err2 = result.Write(oprot); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.WriteMessageEnd(); err == nil && err2 != nil { + err = err2 + } + if err2 = oprot.Flush(ctx); err == nil && err2 != nil { + err = err2 + } + if err != nil { + return + } + return true, err +} + +type UserServiceLoginArgs struct { + Request *LoginReq `thrift:"request,1"` +} + +func NewUserServiceLoginArgs() *UserServiceLoginArgs { + return &UserServiceLoginArgs{} +} + +func (p *UserServiceLoginArgs) InitDefault() { +} + +var UserServiceLoginArgs_Request_DEFAULT *LoginReq + +func (p *UserServiceLoginArgs) GetRequest() (v *LoginReq) { + if !p.IsSetRequest() { + return UserServiceLoginArgs_Request_DEFAULT + } + return p.Request +} + +var fieldIDToName_UserServiceLoginArgs = map[int16]string{ + 1: "request", +} + +func (p *UserServiceLoginArgs) IsSetRequest() bool { + return p.Request != nil +} + +func (p *UserServiceLoginArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceLoginArgs[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceLoginArgs) ReadField1(iprot thrift.TProtocol) error { + _field := NewLoginReq() + if err := _field.Read(iprot); err != nil { + return err + } + p.Request = _field + return nil +} + +func (p *UserServiceLoginArgs) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Login_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceLoginArgs) writeField1(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteFieldBegin("request", thrift.STRUCT, 1); err != nil { + goto WriteFieldBeginError + } + if err := p.Request.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} + +func (p *UserServiceLoginArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceLoginArgs(%+v)", *p) + +} + +type UserServiceLoginResult struct { + Success *LoginResp `thrift:"success,0,optional"` +} + +func NewUserServiceLoginResult() *UserServiceLoginResult { + return &UserServiceLoginResult{} +} + +func (p *UserServiceLoginResult) InitDefault() { +} + +var UserServiceLoginResult_Success_DEFAULT *LoginResp + +func (p *UserServiceLoginResult) GetSuccess() (v *LoginResp) { + if !p.IsSetSuccess() { + return UserServiceLoginResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServiceLoginResult = map[int16]string{ + 0: "success", +} + +func (p *UserServiceLoginResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServiceLoginResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceLoginResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceLoginResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewLoginResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServiceLoginResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Login_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceLoginResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServiceLoginResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceLoginResult(%+v)", *p) + +} + +type UserServicePublicArgs struct { +} + +func NewUserServicePublicArgs() *UserServicePublicArgs { + return &UserServicePublicArgs{} +} + +func (p *UserServicePublicArgs) InitDefault() { +} + +var fieldIDToName_UserServicePublicArgs = map[int16]string{} + +func (p *UserServicePublicArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldTypeError + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +SkipFieldTypeError: + return thrift.PrependError(fmt.Sprintf("%T skip field type %d error", p, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServicePublicArgs) Write(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteStructBegin("Public_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServicePublicArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServicePublicArgs(%+v)", *p) + +} + +type UserServicePublicResult struct { + Success *MessageResp `thrift:"success,0,optional"` +} + +func NewUserServicePublicResult() *UserServicePublicResult { + return &UserServicePublicResult{} +} + +func (p *UserServicePublicResult) InitDefault() { +} + +var UserServicePublicResult_Success_DEFAULT *MessageResp + +func (p *UserServicePublicResult) GetSuccess() (v *MessageResp) { + if !p.IsSetSuccess() { + return UserServicePublicResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServicePublicResult = map[int16]string{ + 0: "success", +} + +func (p *UserServicePublicResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServicePublicResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServicePublicResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServicePublicResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewMessageResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServicePublicResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Public_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServicePublicResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServicePublicResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServicePublicResult(%+v)", *p) + +} + +type UserServiceUserInfoArgs struct { +} + +func NewUserServiceUserInfoArgs() *UserServiceUserInfoArgs { + return &UserServiceUserInfoArgs{} +} + +func (p *UserServiceUserInfoArgs) InitDefault() { +} + +var fieldIDToName_UserServiceUserInfoArgs = map[int16]string{} + +func (p *UserServiceUserInfoArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldTypeError + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +SkipFieldTypeError: + return thrift.PrependError(fmt.Sprintf("%T skip field type %d error", p, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceUserInfoArgs) Write(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteStructBegin("UserInfo_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceUserInfoArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceUserInfoArgs(%+v)", *p) + +} + +type UserServiceUserInfoResult struct { + Success *UserInfoResp `thrift:"success,0,optional"` +} + +func NewUserServiceUserInfoResult() *UserServiceUserInfoResult { + return &UserServiceUserInfoResult{} +} + +func (p *UserServiceUserInfoResult) InitDefault() { +} + +var UserServiceUserInfoResult_Success_DEFAULT *UserInfoResp + +func (p *UserServiceUserInfoResult) GetSuccess() (v *UserInfoResp) { + if !p.IsSetSuccess() { + return UserServiceUserInfoResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServiceUserInfoResult = map[int16]string{ + 0: "success", +} + +func (p *UserServiceUserInfoResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServiceUserInfoResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceUserInfoResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceUserInfoResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewUserInfoResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServiceUserInfoResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("UserInfo_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceUserInfoResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServiceUserInfoResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceUserInfoResult(%+v)", *p) + +} + +type UserServiceAdminArgs struct { +} + +func NewUserServiceAdminArgs() *UserServiceAdminArgs { + return &UserServiceAdminArgs{} +} + +func (p *UserServiceAdminArgs) InitDefault() { +} + +var fieldIDToName_UserServiceAdminArgs = map[int16]string{} + +func (p *UserServiceAdminArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldTypeError + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +SkipFieldTypeError: + return thrift.PrependError(fmt.Sprintf("%T skip field type %d error", p, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceAdminArgs) Write(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteStructBegin("Admin_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceAdminArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceAdminArgs(%+v)", *p) + +} + +type UserServiceAdminResult struct { + Success *MessageResp `thrift:"success,0,optional"` +} + +func NewUserServiceAdminResult() *UserServiceAdminResult { + return &UserServiceAdminResult{} +} + +func (p *UserServiceAdminResult) InitDefault() { +} + +var UserServiceAdminResult_Success_DEFAULT *MessageResp + +func (p *UserServiceAdminResult) GetSuccess() (v *MessageResp) { + if !p.IsSetSuccess() { + return UserServiceAdminResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServiceAdminResult = map[int16]string{ + 0: "success", +} + +func (p *UserServiceAdminResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServiceAdminResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceAdminResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceAdminResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewMessageResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServiceAdminResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Admin_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceAdminResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServiceAdminResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceAdminResult(%+v)", *p) + +} + +type UserServiceManagerArgs struct { +} + +func NewUserServiceManagerArgs() *UserServiceManagerArgs { + return &UserServiceManagerArgs{} +} + +func (p *UserServiceManagerArgs) InitDefault() { +} + +var fieldIDToName_UserServiceManagerArgs = map[int16]string{} + +func (p *UserServiceManagerArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldTypeError + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +SkipFieldTypeError: + return thrift.PrependError(fmt.Sprintf("%T skip field type %d error", p, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceManagerArgs) Write(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteStructBegin("Manager_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceManagerArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceManagerArgs(%+v)", *p) + +} + +type UserServiceManagerResult struct { + Success *MessageResp `thrift:"success,0,optional"` +} + +func NewUserServiceManagerResult() *UserServiceManagerResult { + return &UserServiceManagerResult{} +} + +func (p *UserServiceManagerResult) InitDefault() { +} + +var UserServiceManagerResult_Success_DEFAULT *MessageResp + +func (p *UserServiceManagerResult) GetSuccess() (v *MessageResp) { + if !p.IsSetSuccess() { + return UserServiceManagerResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServiceManagerResult = map[int16]string{ + 0: "success", +} + +func (p *UserServiceManagerResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServiceManagerResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceManagerResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceManagerResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewMessageResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServiceManagerResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Manager_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceManagerResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServiceManagerResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceManagerResult(%+v)", *p) + +} + +type UserServiceDisableArgs struct { +} + +func NewUserServiceDisableArgs() *UserServiceDisableArgs { + return &UserServiceDisableArgs{} +} + +func (p *UserServiceDisableArgs) InitDefault() { +} + +var fieldIDToName_UserServiceDisableArgs = map[int16]string{} + +func (p *UserServiceDisableArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldTypeError + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +SkipFieldTypeError: + return thrift.PrependError(fmt.Sprintf("%T skip field type %d error", p, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceDisableArgs) Write(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteStructBegin("Disable_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceDisableArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceDisableArgs(%+v)", *p) + +} + +type UserServiceDisableResult struct { + Success *MessageResp `thrift:"success,0,optional"` +} + +func NewUserServiceDisableResult() *UserServiceDisableResult { + return &UserServiceDisableResult{} +} + +func (p *UserServiceDisableResult) InitDefault() { +} + +var UserServiceDisableResult_Success_DEFAULT *MessageResp + +func (p *UserServiceDisableResult) GetSuccess() (v *MessageResp) { + if !p.IsSetSuccess() { + return UserServiceDisableResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServiceDisableResult = map[int16]string{ + 0: "success", +} + +func (p *UserServiceDisableResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServiceDisableResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceDisableResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceDisableResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewMessageResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServiceDisableResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Disable_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceDisableResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServiceDisableResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceDisableResult(%+v)", *p) + +} + +type UserServiceSensitiveArgs struct { +} + +func NewUserServiceSensitiveArgs() *UserServiceSensitiveArgs { + return &UserServiceSensitiveArgs{} +} + +func (p *UserServiceSensitiveArgs) InitDefault() { +} + +var fieldIDToName_UserServiceSensitiveArgs = map[int16]string{} + +func (p *UserServiceSensitiveArgs) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldTypeError + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +SkipFieldTypeError: + return thrift.PrependError(fmt.Sprintf("%T skip field type %d error", p, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceSensitiveArgs) Write(oprot thrift.TProtocol) (err error) { + if err = oprot.WriteStructBegin("Sensitive_args"); err != nil { + goto WriteStructBeginError + } + if p != nil { + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceSensitiveArgs) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceSensitiveArgs(%+v)", *p) + +} + +type UserServiceSensitiveResult struct { + Success *SensitiveResp `thrift:"success,0,optional"` +} + +func NewUserServiceSensitiveResult() *UserServiceSensitiveResult { + return &UserServiceSensitiveResult{} +} + +func (p *UserServiceSensitiveResult) InitDefault() { +} + +var UserServiceSensitiveResult_Success_DEFAULT *SensitiveResp + +func (p *UserServiceSensitiveResult) GetSuccess() (v *SensitiveResp) { + if !p.IsSetSuccess() { + return UserServiceSensitiveResult_Success_DEFAULT + } + return p.Success +} + +var fieldIDToName_UserServiceSensitiveResult = map[int16]string{ + 0: "success", +} + +func (p *UserServiceSensitiveResult) IsSetSuccess() bool { + return p.Success != nil +} + +func (p *UserServiceSensitiveResult) Read(iprot thrift.TProtocol) (err error) { + + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 0: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField0(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_UserServiceSensitiveResult[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *UserServiceSensitiveResult) ReadField0(iprot thrift.TProtocol) error { + _field := NewSensitiveResp() + if err := _field.Read(iprot); err != nil { + return err + } + p.Success = _field + return nil +} + +func (p *UserServiceSensitiveResult) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("Sensitive_result"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField0(oprot); err != nil { + fieldId = 0 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *UserServiceSensitiveResult) writeField0(oprot thrift.TProtocol) (err error) { + if p.IsSetSuccess() { + if err = oprot.WriteFieldBegin("success", thrift.STRUCT, 0); err != nil { + goto WriteFieldBeginError + } + if err := p.Success.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 0 end error: ", p), err) +} + +func (p *UserServiceSensitiveResult) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("UserServiceSensitiveResult(%+v)", *p) + +} diff --git a/examples/hertz/herz-example/biz/router/register.go b/examples/hertz/herz-example/biz/router/register.go new file mode 100644 index 0000000..242cfd9 --- /dev/null +++ b/examples/hertz/herz-example/biz/router/register.go @@ -0,0 +1,15 @@ +// Code generated by hertz generator. DO NOT EDIT. + +package router + +import ( + user "github.com/click33/sa-token-go/examples/hertz/herz-example/biz/router/user" + "github.com/cloudwego/hertz/pkg/app/server" +) + +// GeneratedRegister registers routers generated by IDL. +func GeneratedRegister(r *server.Hertz) { + //INSERT_POINT: DO NOT DELETE THIS LINE! + user.Register(r) + +} diff --git a/examples/hertz/herz-example/biz/router/user/middleware.go b/examples/hertz/herz-example/biz/router/user/middleware.go new file mode 100644 index 0000000..da94324 --- /dev/null +++ b/examples/hertz/herz-example/biz/router/user/middleware.go @@ -0,0 +1,47 @@ +// Code generated by hertz generator. + +package user + +import ( + sahertz "github.com/click33/sa-token-go/integrations/hertz" + "github.com/cloudwego/hertz/pkg/app" +) + +func rootMw() []app.HandlerFunc { + // your code... + return nil +} + +func _adminMw() []app.HandlerFunc { + // your code... + return []app.HandlerFunc{sahertz.CheckPermission("admin:*")} +} + +func _loginMw() []app.HandlerFunc { + // your code... + return nil +} + +func _managerMw() []app.HandlerFunc { + // your code... + return []app.HandlerFunc{sahertz.CheckRole("manager")} +} + +func _publicMw() []app.HandlerFunc { + return []app.HandlerFunc{sahertz.Ignore()} +} + +func _sensitiveMw() []app.HandlerFunc { + // your code... + return []app.HandlerFunc{sahertz.CheckDisable()} +} + +func _userinfoMw() []app.HandlerFunc { + // your code... + return []app.HandlerFunc{sahertz.CheckLogin()} +} + +func _disableMw() []app.HandlerFunc { + // your code... + return nil +} diff --git a/examples/hertz/herz-example/biz/router/user/user.go b/examples/hertz/herz-example/biz/router/user/user.go new file mode 100644 index 0000000..04ddee4 --- /dev/null +++ b/examples/hertz/herz-example/biz/router/user/user.go @@ -0,0 +1,27 @@ +// Code generated by hertz generator. DO NOT EDIT. + +package user + +import ( + user "github.com/click33/sa-token-go/examples/hertz/herz-example/biz/handler/user" + "github.com/cloudwego/hertz/pkg/app/server" +) + +/* + This file will register all the routes of the services in the master idl. + And it will update automatically when you use the "update" command for the idl. + So don't modify the contents of the file, or your code will be deleted when it is updated. +*/ + +// Register register routes based on the IDL 'api.${HTTP Method}' annotation. +func Register(r *server.Hertz) { + + root := r.Group("/", rootMw()...) + root.GET("/admin", append(_adminMw(), user.Admin)...) + root.GET("/disable", append(_disableMw(), user.Disable)...) + root.GET("/login", append(_loginMw(), user.Login)...) + root.GET("/manager", append(_managerMw(), user.Manager)...) + root.GET("/public", append(_publicMw(), user.Public)...) + root.GET("/sensitive", append(_sensitiveMw(), user.Sensitive)...) + root.GET("/user", append(_userinfoMw(), user.UserInfo)...) +} diff --git a/examples/hertz/herz-example/build.sh b/examples/hertz/herz-example/build.sh new file mode 100755 index 0000000..f1ba589 --- /dev/null +++ b/examples/hertz/herz-example/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +RUN_NAME=hertz_service +mkdir -p output/bin +cp script/* output 2>/dev/null +chmod +x output/bootstrap.sh +go build -o output/bin/${RUN_NAME} \ No newline at end of file diff --git a/examples/hertz/herz-example/go.mod b/examples/hertz/herz-example/go.mod new file mode 100644 index 0000000..809fbba --- /dev/null +++ b/examples/hertz/herz-example/go.mod @@ -0,0 +1,28 @@ +module github.com/click33/sa-token-go/examples/hertz/herz-example + +go 1.25.4 + +require github.com/cloudwego/hertz v0.10.3 + +require ( + github.com/apache/thrift v0.22.0 // indirect + github.com/bytedance/gopkg v0.1.1 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/gopkg v0.1.4 // indirect + github.com/cloudwego/netpoll v0.7.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/sys v0.24.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect +) + +replace github.com/apache/thrift => github.com/apache/thrift v0.13.0 diff --git a/examples/hertz/herz-example/go.sum b/examples/hertz/herz-example/go.sum new file mode 100644 index 0000000..3921167 --- /dev/null +++ b/examples/hertz/herz-example/go.sum @@ -0,0 +1,119 @@ +github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= +github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= +github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE= +github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= +github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= +github.com/cloudwego/hertz v0.10.3 h1:NFcQAjouVJsod79XPLC/PaFfHgjMTYbiErmW+vGBi8A= +github.com/cloudwego/hertz v0.10.3/go.mod h1:W5dUFXZPZkyfjMMo3EQrMQbofuvTsctM9IxmhbkuT18= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= +github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= +github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/examples/hertz/herz-example/idl/user.thrift b/examples/hertz/herz-example/idl/user.thrift new file mode 100644 index 0000000..9559c23 --- /dev/null +++ b/examples/hertz/herz-example/idl/user.thrift @@ -0,0 +1,33 @@ +namespace go user + +struct LoginReq { + 1: string userId (api.query="userId"); +} + +struct LoginResp { + 1: string token; +} + +struct MessageResp { + 1: string message; +} + +struct UserInfoResp { + 1: string loginId; + 2: list roles; + 3: list permissions; +} + +struct SensitiveResp { + 1: bool sensitive; +} + +service UserService { + LoginResp Login(1: LoginReq request) (api.get="/login"); + MessageResp Public() (api.get="/public"); + UserInfoResp UserInfo() (api.get="/user"); + MessageResp Admin() (api.get="/admin"); + MessageResp Manager() (api.get="/manager"); + MessageResp Disable() (api.get="/disable"); + SensitiveResp Sensitive() (api.get="/sensitive"); +} \ No newline at end of file diff --git a/examples/hertz/herz-example/main.go b/examples/hertz/herz-example/main.go new file mode 100644 index 0000000..8eeb809 --- /dev/null +++ b/examples/hertz/herz-example/main.go @@ -0,0 +1,22 @@ +// Code generated by hertz generator. + +package main + +import ( + sahertz "github.com/click33/sa-token-go/integrations/hertz" // 只需这一个导入! + "github.com/click33/sa-token-go/storage/memory" + "github.com/cloudwego/hertz/pkg/app/server" +) + +func main() { + // 初始化(所有功能都在 sahertz 包中) + storage := memory.NewStorage() + config := sahertz.DefaultConfig() // 使用 sahertz.DefaultConfig + manager := sahertz.NewManager(storage, config) // 使用 sahertz.NewManager + sahertz.SetManager(manager) // 使用 sahertz.SetManager + + h := server.Default() + + register(h) + h.Spin() +} diff --git a/examples/hertz/herz-example/router.go b/examples/hertz/herz-example/router.go new file mode 100644 index 0000000..3b89cc7 --- /dev/null +++ b/examples/hertz/herz-example/router.go @@ -0,0 +1,14 @@ +// Code generated by hertz generator. + +package main + +import ( + "github.com/cloudwego/hertz/pkg/app/server" +) + +// customizeRegister registers customize routers. +func customizedRegister(r *server.Hertz) { + //r.GET("/ping", handler.Ping) + + // your code ... +} diff --git a/examples/hertz/herz-example/router_gen.go b/examples/hertz/herz-example/router_gen.go new file mode 100644 index 0000000..83a8d0c --- /dev/null +++ b/examples/hertz/herz-example/router_gen.go @@ -0,0 +1,16 @@ +// Code generated by hertz generator. DO NOT EDIT. + +package main + +import ( + router "github.com/click33/sa-token-go/examples/hertz/herz-example/biz/router" + "github.com/cloudwego/hertz/pkg/app/server" +) + +// register registers all routers. +func register(r *server.Hertz) { + + router.GeneratedRegister(r) + + customizedRegister(r) +} diff --git a/examples/hertz/herz-example/script/bootstrap.sh b/examples/hertz/herz-example/script/bootstrap.sh new file mode 100755 index 0000000..3f3fc1a --- /dev/null +++ b/examples/hertz/herz-example/script/bootstrap.sh @@ -0,0 +1,5 @@ +#!/bin/bash +CURDIR=$(cd $(dirname $0); pwd) +BinaryName=hertz_service +echo "$CURDIR/bin/${BinaryName}" +exec $CURDIR/bin/${BinaryName} \ No newline at end of file diff --git a/go.work b/go.work index a98912c..b8789bc 100644 --- a/go.work +++ b/go.work @@ -1,7 +1,8 @@ -go 1.25.3 +go 1.25.4 use ( ./core + ./examples/hertz/herz-example ./examples/kratos/kratos-example ./examples/multi-certification ./integrations/chi diff --git a/go.work.sum b/go.work.sum index 6af57e0..3b442f2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -12,16 +12,11 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE= -github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -30,14 +25,6 @@ github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn 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/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= -github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= -github.com/cloudwego/hertz v0.10.3 h1:NFcQAjouVJsod79XPLC/PaFfHgjMTYbiErmW+vGBi8A= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= -github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= @@ -45,8 +32,6 @@ github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.11.2-0.20230627204322-7d0032219fcb h1:kxNVXsNro/lpR5WD+P1FI/yUHn2G03Glber3k8cQL2Y= @@ -88,9 +73,6 @@ github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -100,14 +82,10 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= @@ -124,8 +102,6 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= -github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= @@ -138,7 +114,6 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgc github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -154,31 +129,13 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= @@ -187,7 +144,6 @@ github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdI github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= @@ -206,11 +162,9 @@ go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42s go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= @@ -223,8 +177,8 @@ golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -239,11 +193,8 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= @@ -255,12 +206,9 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/integrations/hertz/plugin.go b/integrations/hertz/plugin.go index 98bf441..6a9a55e 100644 --- a/integrations/hertz/plugin.go +++ b/integrations/hertz/plugin.go @@ -9,7 +9,6 @@ import ( "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/utils" "github.com/cloudwego/hertz/pkg/protocol" - "github.com/gin-gonic/gin" ) // Plugin Hertz plugin for Sa-Token | Hertz插件 @@ -134,9 +133,6 @@ func (p *Plugin) LoginHandler(c *app.RequestContext) { return } - // TODO: Validate username and password (should call your user service) | 验证用户名密码(这里应该调用你的用户服务) - // if !validateUser(req.Username, req.Password) { ... } - // Login | 登录 device := req.Device if device == "" { @@ -198,7 +194,7 @@ func (p *Plugin) LogoutHandler(c *app.RequestContext) { return } - writeSuccessResponse(c, gin.H{ + writeSuccessResponse(c, utils.H{ "message": "logout successful", }) } @@ -225,7 +221,7 @@ func (p *Plugin) UserInfoHandler(c *app.RequestContext) { }) } -// GetSaToken gets Sa-Token context from Gin context | 从Gin上下文获取Sa-Token上下文 +// GetSaToken gets Sa-Token context from Hertz context | 从Hertz上下文获取Sa-Token上下文 func GetSaToken(c *app.RequestContext) (*core.SaTokenContext, bool) { satoken, exists := c.Get("satoken") if !exists { -- Gitee From 2ef3354192486e7daf3f83db74352ab226cfdc09 Mon Sep 17 00:00:00 2001 From: yuegc Date: Wed, 28 Jan 2026 10:40:12 +0800 Subject: [PATCH 13/28] =?UTF-8?q?feat:readme=20=E6=B7=BB=E5=8A=A0=20hertz?= =?UTF-8?q?=20=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++++++ README_zh.md | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index 46be440..d42f37e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ go get github.com/click33/sa-token-go/integrations/chi@latest # Chi framework go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame framework # or go get github.com/click33/sa-token-go/integrations/kratos@latest # Kratos framework +# or +go get github.com/click33/sa-token-go/integrations/hertz@latest # Hertz framework +``` # Storage module (choose one) go get github.com/click33/sa-token-go/storage/memory@latest # Memory storage (dev) @@ -70,6 +73,7 @@ go get github.com/click33/sa-token-go/integrations/fiber@latest # Fiber framewor go get github.com/click33/sa-token-go/integrations/chi@latest # Chi framework go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame framework go get github.com/click33/sa-token-go/integrations/kratos@latest# Kratos framework +go get github.com/click33/sa-token-go/integrations/hertz@latest # Hertz framework ``` ### ⚡ Minimal Usage (One-line Initialization) @@ -378,6 +382,10 @@ r.Get("/user", sachi.CheckLogin(), handler) // Kratos import sakratos "github.com/click33/sa-token-go/integrations/kratos" // Use Plugin.Server() as middleware + +// Hertz +import sahertz "github.com/click33/sa-token-go/integrations/hertz" +h.GET("/user", sahertz.CheckLogin(), handler) ``` ## 🎨 Advanced Features diff --git a/README_zh.md b/README_zh.md index 4d7de6b..7410136 100644 --- a/README_zh.md +++ b/README_zh.md @@ -46,6 +46,8 @@ go get github.com/click33/sa-token-go/integrations/chi@latest # Chi框架 go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame框架 # 或 go get github.com/click33/sa-token-go/integrations/kratos@latest# Kratos框架 +# 或 +go get github.com/click33/sa-token-go/integrations/hertz@latest # Hertz框架 # 存储模块(选一个) go get github.com/click33/sa-token-go/storage/memory@latest # 内存存储(开发) @@ -70,6 +72,7 @@ go get github.com/click33/sa-token-go/integrations/fiber@latest # Fiber框架 go get github.com/click33/sa-token-go/integrations/chi@latest # Chi框架 go get github.com/click33/sa-token-go/integrations/gf@latest # GoFrame框架 go get github.com/click33/sa-token-go/integrations/kratos@latest# Kratos框架 +go get github.com/click33/sa-token-go/integrations/hertz@latest # Hertz框架 ``` ### ⚡ 超简洁使用(一行初始化) @@ -378,6 +381,10 @@ r.Get("/user", sachi.CheckLogin(), handler) // Kratos import sakratos "github.com/click33/sa-token-go/integrations/kratos" // 使用 Plugin.Server() 作为中间件 + +// Hertz +import sahertz "github.com/click33/sa-token-go/integrations/hertz" +h.GET("/user", sahertz.CheckLogin(), handler) ``` ## 🎨 高级特性 -- Gitee From dd79989b64ea95ddd8004c83f4fae5694b2cb638 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:19:53 +0800 Subject: [PATCH 14/28] Update WeChat group QR code image link (2026-2-3) --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 76370a5..d0243da 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From b2d9b95e43ab72af61de5d08cfec6a73c1f9c3e9 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:46:42 +0800 Subject: [PATCH 15/28] Update WeChat group QR code in README_zh.md Update WeChat group QR code image with a new date. --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index d0243da..cd69154 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From b7b49e3a85d6c69175db9619c0698f20f8b57ee1 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:35:31 +0800 Subject: [PATCH 16/28] Update WeChat group QR code image date --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index cd69154..cc91609 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 9a9ab10491cd703f240159d65315aede366b120c Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:34:32 +0800 Subject: [PATCH 17/28] Update WeChat group QR code image date to 2026-2-23 --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index cc91609..fe61053 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 1894d4cc09a69ca07810907f484427ac17a27440 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:33:23 +0800 Subject: [PATCH 18/28] Update WeChat group QR code in README_zh.md (2026-3-1) Updated WeChat group QR code image with a new date. --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index fe61053..4993a0a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 158230eea3e106c952d8cd9bdf0670b828e21cb5 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:27:41 +0800 Subject: [PATCH 19/28] Update WeChat group QR code in README_zh.md (2026-3-16) Updated WeChat group QR code image with a new date. --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 4993a0a..2454195 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 7032b1dccc1aea63f45138927fbfc8b1e44f6846 Mon Sep 17 00:00:00 2001 From: c <23@g> Date: Mon, 16 Mar 2026 16:09:06 +0700 Subject: [PATCH 20/28] add hertz Plugin --- core/version/version.go | 2 +- integrations/hertz/annotation.go | 3 --- integrations/hertz/annotation_test.go | 28 +++++++++++++-------------- integrations/hertz/context.go | 2 +- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/core/version/version.go b/core/version/version.go index 8004370..ee56175 100644 --- a/core/version/version.go +++ b/core/version/version.go @@ -3,4 +3,4 @@ package version // Version system level version number | 系统级版本号 // This is the global version of Sa-Token-Go, modify this value to update the version across the entire project // 这是 Sa-Token-Go 的全局版本号,修改此值可更新整个项目的版本 -const Version = "0.1.7" +const Version = "0.1.8" diff --git a/integrations/hertz/annotation.go b/integrations/hertz/annotation.go index 0283554..158a6fe 100644 --- a/integrations/hertz/annotation.go +++ b/integrations/hertz/annotation.go @@ -2,7 +2,6 @@ package hertz import ( "context" - "fmt" "reflect" "strings" @@ -104,8 +103,6 @@ func GetHandler(handler interface{}, annotations ...*Annotation) app.HandlerFunc saCtx := core.NewContext(hCtx, stputil.GetManager()) token := saCtx.GetTokenValue() - fmt.Printf("Debug Handler: token='%s', isLogin=%v, headers=%v\n", token, stputil.IsLogin(token), c.Request.Header.String()) - if token == "" { writeErrorResponse(c, core.NewNotLoginError()) c.Abort() diff --git a/integrations/hertz/annotation_test.go b/integrations/hertz/annotation_test.go index 4a976e3..9d2f657 100644 --- a/integrations/hertz/annotation_test.go +++ b/integrations/hertz/annotation_test.go @@ -2,7 +2,6 @@ package hertz import ( "context" - "fmt" "net/http" "testing" @@ -24,13 +23,14 @@ func setupTestRouter() *server.Hertz { // 创建内存存储 storage := memory.NewStorage() - // 创建配置 + // 创建配置(必须开启 IsReadHeader 才能从 Authorization 头读取 token) cfg := &config.Config{ TokenName: "satoken", Timeout: 2592000, // 30 天(秒) IsConcurrent: true, IsShare: true, MaxLoginCount: -1, + IsReadHeader: true, } // 创建并设置全局 Manager @@ -71,10 +71,6 @@ func TestCheckRole_WithValidRole(t *testing.T) { // 创建一个具有 Admin 角色的用户 token := mockLoginWithRole("user123", []string{"Admin"}) - fmt.Println("Debug: Token generated:", token) - loginID, _ := stputil.GetLoginID(token) - fmt.Println("Debug: LoginID from storage:", loginID) - // 发送请求 w := ut.PerformRequest(router.Engine, "GET", "/admin", nil, ut.Header{Key: "Authorization", Value: token}) @@ -102,7 +98,7 @@ func TestCheckRole_WithInvalidRole(t *testing.T) { // 断言 assert.Equal(t, http.StatusForbidden, w.Code) - assert.Contains(t, w.Body.String(), "角色不足") + assert.Contains(t, w.Body.String(), "role denied") } // TestCheckRole_MultipleRoles 测试多个角色的情况(OR 逻辑) @@ -135,7 +131,7 @@ func TestCheckRole_NoToken(t *testing.T) { w := ut.PerformRequest(router.Engine, "GET", "/admin", nil) assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Contains(t, w.Body.String(), "未登录") + assert.Contains(t, w.Body.String(), "user not logged in") } // TestCheckRole_InvalidToken 测试无效 token 的情况 @@ -150,7 +146,7 @@ func TestCheckRole_InvalidToken(t *testing.T) { ut.Header{Key: "Authorization", Value: "invalid-token-12345"}) assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Contains(t, w.Body.String(), "未登录") + assert.Contains(t, w.Body.String(), "user not logged in") } // TestCheckPermission_WithValidPermission 测试具有有效权限的用户访问 @@ -184,7 +180,7 @@ func TestCheckPermission_WithInvalidPermission(t *testing.T) { ut.Header{Key: "Authorization", Value: token}) assert.Equal(t, http.StatusForbidden, w.Code) - assert.Contains(t, w.Body.String(), "权限不足") + assert.Contains(t, w.Body.String(), "permission denied") } // TestCheckLogin_Success 测试登录检查成功 @@ -215,7 +211,7 @@ func TestCheckLogin_Failed(t *testing.T) { w := ut.PerformRequest(router.Engine, "GET", "/profile", nil) assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Contains(t, w.Body.String(), "未登录") + assert.Contains(t, w.Body.String(), "user not logged in") } // TestCheckDisable_NotDisabled 测试账号未被封禁的情况 @@ -236,6 +232,7 @@ func TestCheckDisable_NotDisabled(t *testing.T) { } // TestCheckDisable_IsDisabled 测试账号被封禁的情况 +// 注意:core 的 Disable() 会踢出该账号所有 token,封禁后原 token 已失效,故请求返回 401 而非 403 func TestCheckDisable_IsDisabled(t *testing.T) { router := setupTestRouter() @@ -246,14 +243,15 @@ func TestCheckDisable_IsDisabled(t *testing.T) { loginID := "user102" token := mockLogin(loginID) - // 封禁账号 + // 封禁账号(会同时踢出该账号所有 token) stputil.Disable(loginID, 3600) // 封禁 1 小时 w := ut.PerformRequest(router.Engine, "GET", "/resource", nil, ut.Header{Key: "Authorization", Value: token}) - assert.Equal(t, http.StatusForbidden, w.Code) - assert.Contains(t, w.Body.String(), "账号已被封禁") + // 封禁后 token 已被踢下线,请求得到 401 + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "user not logged in") } // TestIgnore_SkipsAuthentication 测试忽略认证装饰器 @@ -310,7 +308,7 @@ func TestChainedMiddleware_CheckRoleAndHandler_NoRole(t *testing.T) { ut.Header{Key: "Authorization", Value: token}) assert.Equal(t, http.StatusForbidden, w.Code) - assert.Contains(t, w.Body.String(), "角色不足") + assert.Contains(t, w.Body.String(), "role denied") } // TestGetHandler_WithNilHandler 测试 GetHandler 在 handler 为 nil 时的行为 diff --git a/integrations/hertz/context.go b/integrations/hertz/context.go index d9d2097..2b3a29b 100644 --- a/integrations/hertz/context.go +++ b/integrations/hertz/context.go @@ -6,7 +6,7 @@ import ( "github.com/cloudwego/hertz/pkg/protocol" ) -// HertzContext Hertz request context adapter | Gin请求上下文适配器 +// HertzContext Hertz request context adapter | Hertz 请求上下文适配器 type HertzContext struct { c *app.RequestContext aborted bool -- Gitee From 43da5f7fe3f1cd22d9373992423f24790b286efe Mon Sep 17 00:00:00 2001 From: c <23@g> Date: Mon, 16 Mar 2026 16:23:32 +0700 Subject: [PATCH 21/28] chore: bump all sa-token-go module requires to v0.1.8 Made-with: Cursor --- examples/annotation/annotation-example/go.mod | 8 ++++---- examples/chi/chi-example/go.mod | 6 +++--- examples/echo/echo-example/go.mod | 8 ++++---- examples/fiber/fiber-example/go.mod | 6 +++--- examples/gf/go.mod | 8 ++++---- examples/gin/gin-example/go.mod | 8 ++++---- examples/gin/gin-simple/go.mod | 8 ++++---- examples/jwt-example/go.mod | 6 +++--- examples/listener-example/go.mod | 6 +++--- examples/multi-certification/go.mod | 6 +++--- examples/oauth2-example/go.mod | 4 ++-- examples/quick-start/simple-example/go.mod | 6 +++--- examples/redis-example/go.mod | 6 +++--- examples/security-features/go.mod | 6 +++--- examples/session-demo/go.mod | 6 +++--- examples/token-styles/go.mod | 6 +++--- integrations/chi/go.mod | 4 ++-- integrations/echo/go.mod | 4 ++-- integrations/fiber/go.mod | 4 ++-- integrations/gf/go.mod | 4 ++-- integrations/gin/go.mod | 4 ++-- integrations/hertz/go.mod | 4 ++-- integrations/kratos/go.mod | 6 +++--- storage/memory/go.mod | 2 +- storage/redis/go.mod | 2 +- stputil/go.mod | 2 +- 26 files changed, 70 insertions(+), 70 deletions(-) diff --git a/examples/annotation/annotation-example/go.mod b/examples/annotation/annotation-example/go.mod index a830f10..43cc988 100644 --- a/examples/annotation/annotation-example/go.mod +++ b/examples/annotation/annotation-example/go.mod @@ -3,10 +3,10 @@ module github.com/click33/sa-token-go/examples/annotation-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/integrations/gin v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/integrations/gin v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/gin-gonic/gin v1.10.0 ) diff --git a/examples/chi/chi-example/go.mod b/examples/chi/chi-example/go.mod index 509b792..e287429 100644 --- a/examples/chi/chi-example/go.mod +++ b/examples/chi/chi-example/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/chi-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/integrations/chi v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/integrations/chi v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/go-chi/chi/v5 v5.0.11 ) diff --git a/examples/echo/echo-example/go.mod b/examples/echo/echo-example/go.mod index b08a0a4..e191694 100644 --- a/examples/echo/echo-example/go.mod +++ b/examples/echo/echo-example/go.mod @@ -5,14 +5,14 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/integrations/echo v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/integrations/echo v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/labstack/echo/v4 v4.11.4 ) require ( - github.com/click33/sa-token-go/stputil v0.0.0-20251017234446-3cf2bdee68cc // indirect + github.com/click33/sa-token-go/stputil v0.1.8 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/examples/fiber/fiber-example/go.mod b/examples/fiber/fiber-example/go.mod index e87d400..bfe6cbd 100644 --- a/examples/fiber/fiber-example/go.mod +++ b/examples/fiber/fiber-example/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/fiber-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/integrations/fiber v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/integrations/fiber v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/gofiber/fiber/v2 v2.52.0 ) diff --git a/examples/gf/go.mod b/examples/gf/go.mod index fdf155e..35881d0 100644 --- a/examples/gf/go.mod +++ b/examples/gf/go.mod @@ -9,16 +9,16 @@ replace ( ) require ( - github.com/click33/sa-token-go/integrations/gf v0.0.0-00010101000000-000000000000 - github.com/click33/sa-token-go/storage/memory v0.0.0-00010101000000-000000000000 + github.com/click33/sa-token-go/integrations/gf v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/gogf/gf/v2 v2.9.4 ) require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/click33/sa-token-go/core v0.1.3 // indirect - github.com/click33/sa-token-go/stputil v0.1.3 // indirect + github.com/click33/sa-token-go/core v0.1.8 // indirect + github.com/click33/sa-token-go/stputil v0.1.8 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect diff --git a/examples/gin/gin-example/go.mod b/examples/gin/gin-example/go.mod index 939c529..22329e2 100644 --- a/examples/gin/gin-example/go.mod +++ b/examples/gin/gin-example/go.mod @@ -5,16 +5,16 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/integrations/gin v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 + github.com/click33/sa-token-go/integrations/gin v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/gin-gonic/gin v1.10.0 github.com/spf13/viper v1.18.2 ) require ( github.com/bytedance/sonic v1.11.6 // indirect - github.com/click33/sa-token-go/core v0.1.3 // indirect - github.com/click33/sa-token-go/stputil v0.1.3 // indirect + github.com/click33/sa-token-go/core v0.1.8 // indirect + github.com/click33/sa-token-go/stputil v0.1.8 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect diff --git a/examples/gin/gin-simple/go.mod b/examples/gin/gin-simple/go.mod index e9dd2a9..5773378 100644 --- a/examples/gin/gin-simple/go.mod +++ b/examples/gin/gin-simple/go.mod @@ -5,15 +5,15 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/integrations/gin v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 + github.com/click33/sa-token-go/integrations/gin v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/gin-gonic/gin v1.10.0 ) require ( github.com/bytedance/sonic v1.11.6 // indirect - github.com/click33/sa-token-go/core v0.1.3 // indirect - github.com/click33/sa-token-go/stputil v0.1.3 // indirect + github.com/click33/sa-token-go/core v0.1.8 // indirect + github.com/click33/sa-token-go/stputil v0.1.8 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect diff --git a/examples/jwt-example/go.mod b/examples/jwt-example/go.mod index 1daf0cf..ff34abb 100644 --- a/examples/jwt-example/go.mod +++ b/examples/jwt-example/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/jwt-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/examples/listener-example/go.mod b/examples/listener-example/go.mod index 728dd7c..9a6de24 100644 --- a/examples/listener-example/go.mod +++ b/examples/listener-example/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/listener-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/examples/multi-certification/go.mod b/examples/multi-certification/go.mod index 9aa5fe7..c0b305f 100644 --- a/examples/multi-certification/go.mod +++ b/examples/multi-certification/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/multi-certification go 1.25.3 require ( - github.com/click33/sa-token-go/core v0.1.6 - github.com/click33/sa-token-go/storage/memory v0.1.6 - github.com/click33/sa-token-go/stputil v0.1.6 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/examples/oauth2-example/go.mod b/examples/oauth2-example/go.mod index 905f73e..a2bd77c 100644 --- a/examples/oauth2-example/go.mod +++ b/examples/oauth2-example/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/examples/oauth2-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 github.com/gin-gonic/gin v1.10.0 ) diff --git a/examples/quick-start/simple-example/go.mod b/examples/quick-start/simple-example/go.mod index e7532cb..a0fb35e 100644 --- a/examples/quick-start/simple-example/go.mod +++ b/examples/quick-start/simple-example/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/simple-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/examples/redis-example/go.mod b/examples/redis-example/go.mod index 4020f16..a2dfc01 100644 --- a/examples/redis-example/go.mod +++ b/examples/redis-example/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/redis-example go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/redis v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/redis v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/redis/go-redis/v9 v9.5.1 ) diff --git a/examples/security-features/go.mod b/examples/security-features/go.mod index 387f4e0..edf3739 100644 --- a/examples/security-features/go.mod +++ b/examples/security-features/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/security-features go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/examples/session-demo/go.mod b/examples/session-demo/go.mod index 481560e..da1fa9d 100644 --- a/examples/session-demo/go.mod +++ b/examples/session-demo/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/session-demo go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) replace ( diff --git a/examples/token-styles/go.mod b/examples/token-styles/go.mod index 2f16ade..916d663 100644 --- a/examples/token-styles/go.mod +++ b/examples/token-styles/go.mod @@ -3,9 +3,9 @@ module github.com/click33/sa-token-go/examples/token-styles go 1.21 require ( - github.com/click33/sa-token-go/core v0.1.3 - github.com/click33/sa-token-go/storage/memory v0.1.3 - github.com/click33/sa-token-go/stputil v0.1.3 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/integrations/chi/go.mod b/integrations/chi/go.mod index caa8b0d..a01e547 100644 --- a/integrations/chi/go.mod +++ b/integrations/chi/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/integrations/chi go 1.23.0 require ( - github.com/click33/sa-token-go/core v0.1.7 - github.com/click33/sa-token-go/stputil v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 ) require ( diff --git a/integrations/echo/go.mod b/integrations/echo/go.mod index e641831..f207497 100644 --- a/integrations/echo/go.mod +++ b/integrations/echo/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.7 - github.com/click33/sa-token-go/stputil v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/labstack/echo/v4 v4.11.4 ) diff --git a/integrations/fiber/go.mod b/integrations/fiber/go.mod index 3858d72..ae840ab 100644 --- a/integrations/fiber/go.mod +++ b/integrations/fiber/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.7 - github.com/click33/sa-token-go/stputil v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/gofiber/fiber/v2 v2.52.0 ) diff --git a/integrations/gf/go.mod b/integrations/gf/go.mod index 3621f5b..d7c2833 100644 --- a/integrations/gf/go.mod +++ b/integrations/gf/go.mod @@ -3,8 +3,8 @@ module github.com/click33/sa-token-go/integrations/gf go 1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.7 - github.com/click33/sa-token-go/stputil v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/gogf/gf/v2 v2.9.4 ) diff --git a/integrations/gin/go.mod b/integrations/gin/go.mod index 36b5254..36448dc 100644 --- a/integrations/gin/go.mod +++ b/integrations/gin/go.mod @@ -5,8 +5,8 @@ go 1.23.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.7 - github.com/click33/sa-token-go/stputil v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/gin-gonic/gin v1.10.0 github.com/stretchr/testify v1.11.1 ) diff --git a/integrations/hertz/go.mod b/integrations/hertz/go.mod index 557d36e..97a4f8a 100644 --- a/integrations/hertz/go.mod +++ b/integrations/hertz/go.mod @@ -5,8 +5,8 @@ go 1.25 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.7 // indirect - github.com/click33/sa-token-go/stputil v0.1.7 // indirect + github.com/click33/sa-token-go/core v0.1.8 // indirect + github.com/click33/sa-token-go/stputil v0.1.8 // indirect github.com/cloudwego/hertz v0.10.3 // indirect github.com/stretchr/testify v1.11.1 // indirect ) diff --git a/integrations/kratos/go.mod b/integrations/kratos/go.mod index 873acf4..1762a7e 100644 --- a/integrations/kratos/go.mod +++ b/integrations/kratos/go.mod @@ -5,9 +5,9 @@ go 1.24.0 toolchain go1.24.1 require ( - github.com/click33/sa-token-go/core v0.1.7 - github.com/click33/sa-token-go/storage/memory v0.1.7 - github.com/click33/sa-token-go/stputil v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/storage/memory v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 github.com/go-kratos/kratos/v2 v2.9.1 ) diff --git a/storage/memory/go.mod b/storage/memory/go.mod index 317a84f..0738598 100644 --- a/storage/memory/go.mod +++ b/storage/memory/go.mod @@ -2,6 +2,6 @@ module github.com/click33/sa-token-go/storage/memory go 1.23.0 -require github.com/click33/sa-token-go/core v0.1.7 +require github.com/click33/sa-token-go/core v0.1.8 replace github.com/click33/sa-token-go/core => ../../core diff --git a/storage/redis/go.mod b/storage/redis/go.mod index 18b0052..84da4f6 100644 --- a/storage/redis/go.mod +++ b/storage/redis/go.mod @@ -3,7 +3,7 @@ module github.com/click33/sa-token-go/storage/redis go 1.23.0 require ( - github.com/click33/sa-token-go/core v0.1.7 + github.com/click33/sa-token-go/core v0.1.8 github.com/redis/go-redis/v9 v9.5.1 ) diff --git a/stputil/go.mod b/stputil/go.mod index 5eeb5db..af14660 100644 --- a/stputil/go.mod +++ b/stputil/go.mod @@ -2,7 +2,7 @@ module github.com/click33/sa-token-go/stputil go 1.23.0 -require github.com/click33/sa-token-go/core v0.1.7 +require github.com/click33/sa-token-go/core v0.1.8 require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect -- Gitee From 4efb5f728a98b05b9a81b67e1bb6e34c8d7cec33 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:05:11 +0800 Subject: [PATCH 22/28] Update WeChat group QR code image date (2026-3-22) Update WeChat group QR code image date in README_zh.md --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index aa6dd20..a92f9dc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 6de4ef7447b934e392fe33fd1e6d291f86113218 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:30:43 +0800 Subject: [PATCH 23/28] Update WeChat group QR code image in README_zh.md 2026-4-2 --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index a92f9dc..4a0c827 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 374c7063892b20b75412b9f82709fe9936d3dae9 Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:30:46 +0800 Subject: [PATCH 24/28] Update WeChat group QR code in README_zh.md 2026-4-13 Update WeChat group QR code image with new date. --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 4a0c827..c885bf0 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 5ccbe6485d9f2f61704e736ab838c87ad2a8e85b Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:05:19 +0800 Subject: [PATCH 25/28] Update WeChat group QR code image date 2026-4-20 --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index c885bf0..1d23cba 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 944e0e352faae806f8e9a50131778195726d5cdc Mon Sep 17 00:00:00 2001 From: click33 <36243476+click33@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:40:18 +0800 Subject: [PATCH 26/28] Update README_zh.md 2026-4-26 --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 1d23cba..3d3d4c7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From f136b61f17f2ac132afc93bf9d6a3061c6600a77 Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Sun, 3 May 2026 01:42:57 +0800 Subject: [PATCH 27/28] update qr 2026-5-3 --- README_zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh.md b/README_zh.md index 3d3d4c7..5e9d8bf 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,7 +28,7 @@ ## 💬 微信交流群 -sa-token-go 微信交流群 +sa-token-go 微信交流群 ## 🚀 快速开始 -- Gitee From 20083866aad88248e60cf749d6310336c981d130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=96=94=E6=98=AF=E7=B2=91=E7=B2=91?= Date: Wed, 6 May 2026 01:57:17 +0800 Subject: [PATCH 28/28] Support Beego v2 framework --- examples/beego/beego-example/README.md | 235 ++++++++++++ examples/beego/beego-example/conf/app.conf | 0 examples/beego/beego-example/demo/demo.go | 415 +++++++++++++++++++++ examples/beego/beego-example/go.mod | 42 +++ examples/beego/beego-example/go.sum | 66 ++++ examples/beego/beego-example/main.go | 135 +++++++ go.work | 2 + go.work.sum | 72 +++- integrations/beego/annotation.go | 151 ++++++++ integrations/beego/context.go | 150 ++++++++ integrations/beego/export.go | 380 +++++++++++++++++++ integrations/beego/go.mod | 39 ++ integrations/beego/go.sum | 68 ++++ integrations/beego/plugin.go | 260 +++++++++++++ 14 files changed, 2012 insertions(+), 3 deletions(-) create mode 100644 examples/beego/beego-example/README.md create mode 100644 examples/beego/beego-example/conf/app.conf create mode 100644 examples/beego/beego-example/demo/demo.go create mode 100644 examples/beego/beego-example/go.mod create mode 100644 examples/beego/beego-example/go.sum create mode 100644 examples/beego/beego-example/main.go create mode 100644 integrations/beego/annotation.go create mode 100644 integrations/beego/context.go create mode 100644 integrations/beego/export.go create mode 100644 integrations/beego/go.mod create mode 100644 integrations/beego/go.sum create mode 100644 integrations/beego/plugin.go diff --git a/examples/beego/beego-example/README.md b/examples/beego/beego-example/README.md new file mode 100644 index 0000000..7510e13 --- /dev/null +++ b/examples/beego/beego-example/README.md @@ -0,0 +1,235 @@ +# Sa-Token Go Beego Integration Demo + +beego 框架集成 sa-token-go 认证框架的完整示例。 + +## 功能特性 + +- **登录认证**:`POST /login` - 用户登录获取 Token +- **会话管理**:`GET /session` - 查询会话状态 +- **用户信息**:`GET /user/info` - 获取用户角色和权限 +- **登出**:`POST /logout` - 登出并使 Token 失效 +- **RBAC**:`GET /admin` - 基于角色的访问控制 +- **PBAC**:`GET /data` - 基于权限的访问控制 +- **忽略认证**:`GET /health` - 忽略认证检查的端点 + +## 快速开始 + +### 1. 启动服务 + +```bash +# 使用 air 热重载(开发模式) +air + +# 或直接运行 +go run main.go + +# 或编译后运行 +go build -o server.exe main.go +./server.exe +``` + +### 2. 测试接口 + +```bash +# 1. 初始化用户权限 +curl -X POST http://localhost:8080/setup + +# 2. 登录获取 Token +curl -X POST http://localhost:8080/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin"}' + +# 3. 使用 Token 访问受保护资源 +curl http://localhost:8080/admin \ + -H "satoken: 你的token" +``` + +## API 文档 + +### 公共端点(无需认证) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/public` | 公开接口,无需认证 | +| GET | `/health` | 健康检查,忽略认证 | + +### 认证端点 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/login` | 用户登录,返回 Token | +| POST | `/logout` | 用户登出,使 Token 失效 | +| GET | `/session` | 查询当前会话状态 | +| GET | `/user/info` | 获取用户信息(含角色、权限) | + +### 受保护端点 + +| 方法 | 路径 | 所需权限 | 说明 | +|------|------|---------|------| +| GET | `/admin` | admin 角色 | 管理员专用 | +| GET | `/data` | user:read 权限 | 需要读取权限 | +| GET | `/profile` | 登录状态 | 需要登录 | + +### 管理端点 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/setup` | 初始化演示用户数据 | + +## 使用示例 + +### 登录请求 + +```bash +curl -X POST http://localhost:8080/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","device":"PC"}' +``` + +**响应示例:** +```json +{ + "code": 200, + "data": {"token": "d5a98ee0-f26a-4ec7-b742-d9bcab171d4b"}, + "message": "success" +} +``` + +### 访问受保护资源 + +```bash +# 带上 Token +curl http://localhost:8080/admin \ + -H "satoken: d5a98ee0-f26a-4ec7-b742-d9bcab171d4b" +``` + +**成功响应:** +```json +{"message": "welcome admin!"} +``` + +**失败响应(无权限):** +```json +{ + "code": 403, + "error": "role denied (code: 403): role denied: you don't have the required role", + "message": "role denied" +} +``` + +## Filter 中间件 + +beego 集成提供了以下 Filter 函数: + +```go +// 检查登录状态 +beego.CheckLogin() + +// 检查角色(支持多个角色,OR 逻辑) +beego.CheckRole("admin", "user") + +// 检查权限(支持多个权限,OR 逻辑) +beego.CheckPermission("user:read", "user:write") + +// 检查账号是否被封禁 +beego.CheckDisable() + +// 忽略认证检查 +beego.Ignore() +``` + +### 注册方式 + +```go +// 方式 1:使用 InsertFilter +web.InsertFilter("/admin", web.BeforeRouter, beego.CheckRole("admin")) +web.InsertFilter("/data", web.BeforeRouter, beego.CheckPermission("user:read")) + +// 方式 2:使用全局 Filter +web.GlobalFilters("", beego.CheckLogin()) +``` + +## 完整测试 + +运行完整测试套件: + +```bash +go run demo/demo.go +``` + +**测试用例:** + +``` +Test 1: Public Endpoints - 公共端点 + ✓ GET /public - 无需认证访问 + ✓ GET /health - 忽略认证 + +Test 2: Session & UserInfo - 会话与用户信息 + ✓ GET /session - 会话状态查询 + ✓ GET /user/info - 用户角色和权限 + +Test 3: RBAC - 基于角色的访问控制 + ✓ admin 用户可访问 /admin + ✓ user1 用户被拒绝访问 /admin + +Test 4: PBAC - 基于权限的访问控制 + ✓ admin 和 user1 均可访问 /data + +Test 5: Unauthorized Access - 未授权访问防护 + ✓ 无 Token 时正确拒绝 + ✓ 无效 Token 时正确拒绝 + +Test 6: Session Isolation - 会话隔离 + ✓ admin 和 user1 会话数据隔离 + +Test 7: Logout - 登出 + ✓ 登出后 Token 失效 +``` + +## 目录结构 + +``` +beego-example/ +├── main.go # 主程序入口 +├── demo/ +│ └── demo.go # 完整测试套件 +├── go.mod +└── README.md +``` + +## 注意事项 + +### 1. Setup 与 Login 的调用顺序 + +`/setup` 端点会先调用 `Login()` 创建会话,再调用 `SetRoles()/SetPermissions()` 设置权限。这是符合 sa-token 设计的方式。 + +```go +web.Post("/setup", func(ctx *context.Context) { + integrations.Login("admin") + integrations.SetRoles("admin", []string{"admin"}) + integrations.SetPermissions("admin", []string{"user:*", "admin:*"}) + // ... +}) +``` + +### 2. Token 获取方式 + +集成支持三种 Token 获取方式(按优先级): + +1. **Header**:`satoken: xxx` +2. **Authorization**:`Bearer xxx` +3. **Cookie**:`satoken=xxx` + +### 3. 错误码说明 + +| 错误码 | HTTP 状态 | 说明 | +|--------|----------|------| +| 400 | 400 | 请求参数错误 | +| 401 | 401 | 未登录或 Token 无效 | +| 403 | 403 | 角色或权限不足 | +| 500 | 500 | 服务器内部错误 | + +## 参考资料 + +- [sa-token-go 核心库](https://github.com/click33/sa-token-go) +- [beego 官方文档](https://beego.vip/) diff --git a/examples/beego/beego-example/conf/app.conf b/examples/beego/beego-example/conf/app.conf new file mode 100644 index 0000000..e69de29 diff --git a/examples/beego/beego-example/demo/demo.go b/examples/beego/beego-example/demo/demo.go new file mode 100644 index 0000000..a2ce411 --- /dev/null +++ b/examples/beego/beego-example/demo/demo.go @@ -0,0 +1,415 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +const baseURL = "http://localhost:8080" + +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +type LoginResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + Token string `json:"token"` + } `json:"data"` +} + +func main() { + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Sa-Token Go Beego Integration - Comprehensive Test Suite ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + fmt.Println() + + client := &http.Client{} + + // ============================================================ + // Step 0: Setup - Login users and configure roles/permissions + // /setup does: Login() -> SetRoles() -> SetPermissions() + // This ensures session has both login info and roles/permissions + // ============================================================ + fmt.Println("[Step 0] Setup: Login users + configure roles & permissions") + fmt.Println(" └─ /setup: Login() -> SetRoles() -> SetPermissions()") + + resp, err := http.Post(baseURL+"/setup", "application/json", nil) + if err != nil { + fmt.Printf(" [FAIL] Setup failed: %v\n", err) + return + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "configured") { + fmt.Println(" [OK] Demo users configured:") + fmt.Println(" admin: roles=[admin], perms=[user:*, admin:*]") + fmt.Println(" user1: roles=[user], perms=[user:read, user:write]") + } + + // Get admin token via /login (roles already set in setup via Login+SetRoles) + fmt.Print("\n[Step 1] Get admin token ... ") + resp, err = http.Post(baseURL+"/login", "application/json", bytes.NewBufferString(`{"username":"admin"}`)) + if err != nil { + fmt.Printf(" [FAIL] %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + var adminLogin LoginResponse + json.Unmarshal(body, &adminLogin) + if adminLogin.Code != 200 { + fmt.Printf(" [FAIL] login failed: %s\n", string(body)) + return + } + adminToken := adminLogin.Data.Token + fmt.Printf("OK (token: %s)\n", adminToken) + + // Get user1 token + fmt.Print("[Step 2] Get user1 token ... ") + resp, err = http.Post(baseURL+"/login", "application/json", bytes.NewBufferString(`{"username":"user1"}`)) + if err != nil { + fmt.Printf(" [FAIL] %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + var user1Login LoginResponse + json.Unmarshal(body, &user1Login) + user1Token := user1Login.Data.Token + fmt.Printf("OK (token: %s)\n", user1Token) + + // ============================================================ + // Test 1: Public Endpoints + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 1: Public Endpoints (No Auth Required) ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" GET /public ... ") + resp, err = http.Get(baseURL + "/public") + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "public endpoint") { + fmt.Println("OK - Public endpoint accessible without token") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + fmt.Print(" GET /health (with fake token - should ignore auth) ... ") + req, _ := http.NewRequest("GET", baseURL+"/health", nil) + req.Header.Set("satoken", "fake-token-ignored") + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "ok") { + fmt.Println("OK - Health endpoint ignores auth (Ignore filter)") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + // ============================================================ + // Test 2: Session & UserInfo with Roles/Permissions + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 2: Session & UserInfo (Roles & Permissions) ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" GET /session (admin token) ... ") + req, _ = http.NewRequest("GET", baseURL+"/session", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), `"isLogin":true`) && strings.Contains(string(body), `"loginId":"admin"`) { + fmt.Println("OK - Admin session valid") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + fmt.Print(" GET /user/info (admin) - check roles & permissions ... ") + req, _ = http.NewRequest("GET", baseURL+"/user/info", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), `"loginId":"admin"`) { + var r Response + json.Unmarshal(body, &r) + var data map[string]interface{} + json.Unmarshal(r.Data, &data) + fmt.Println("OK - UserInfo retrieved") + if roles, ok := data["roles"].([]interface{}); ok { + fmt.Printf(" Roles: %v\n", roles) + } + if perms, ok := data["permissions"].([]interface{}); ok { + fmt.Printf(" Permissions: %v\n", perms) + } + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + // ============================================================ + // Test 3: RBAC - Role-Based Access Control + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 3: Role-Based Access Control (RBAC) ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" GET /admin (admin token - has admin role) ... ") + req, _ = http.NewRequest("GET", baseURL+"/admin", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "welcome admin") { + fmt.Println("OK - Admin access granted (admin role verified)") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + fmt.Print(" GET /admin (user1 token - no admin role) ... ") + req, _ = http.NewRequest("GET", baseURL+"/admin", nil) + req.Header.Set("satoken", user1Token) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "403") && strings.Contains(string(body), "role denied") { + fmt.Println("OK - Access denied (user1 lacks admin role)") + } else { + fmt.Printf("FAIL: expected 403, got: %s\n", string(body)) + } + + // ============================================================ + // Test 4: PBAC - Permission-Based Access Control + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 4: Permission-Based Access Control (PBAC) ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" GET /data (admin token - has user:read perm) ... ") + req, _ = http.NewRequest("GET", baseURL+"/data", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "item1") && strings.Contains(string(body), "item2") { + fmt.Println("OK - Admin can access /data (user:read permission)") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + fmt.Print(" GET /data (user1 token - has user:read perm) ... ") + req, _ = http.NewRequest("GET", baseURL+"/data", nil) + req.Header.Set("satoken", user1Token) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "item1") { + fmt.Println("OK - user1 can access /data (user:read permission)") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + // ============================================================ + // Test 5: Unauthorized Access Prevention + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 5: Unauthorized Access Prevention ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" GET /profile (no token) ... ") + req, _ = http.NewRequest("GET", baseURL+"/profile", nil) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "401") || strings.Contains(string(body), "not login") { + fmt.Println("OK - Rejected: no token provided") + } else { + fmt.Printf("FAIL: expected 401, got: %s\n", string(body)) + } + + fmt.Print(" GET /profile (invalid token) ... ") + req, _ = http.NewRequest("GET", baseURL+"/profile", nil) + req.Header.Set("satoken", "invalid-token-xyz") + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "401") || strings.Contains(string(body), "not login") { + fmt.Println("OK - Rejected: invalid token") + } else { + fmt.Printf("FAIL: expected 401, got: %s\n", string(body)) + } + + // ============================================================ + // Test 6: Session Isolation + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 6: Session Isolation ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" GET /user/info (admin token) - verify admin's data ... ") + req, _ = http.NewRequest("GET", baseURL+"/user/info", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), `"loginId":"admin"`) { + fmt.Println("OK - Admin session isolated correctly") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + fmt.Print(" GET /user/info (user1 token) - verify user1's data ... ") + req, _ = http.NewRequest("GET", baseURL+"/user/info", nil) + req.Header.Set("satoken", user1Token) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), `"loginId":"user1"`) { + fmt.Println("OK - user1 session isolated correctly") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + // ============================================================ + // Test 7: Logout + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test 7: Logout & Token Invalidation ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + + fmt.Print(" POST /logout (admin token) ... ") + req, _ = http.NewRequest("POST", baseURL+"/logout", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "logout successful") { + fmt.Println("OK - Logout successful") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + fmt.Print(" GET /user/info (admin token after logout) ... ") + req, _ = http.NewRequest("GET", baseURL+"/user/info", nil) + req.Header.Set("satoken", adminToken) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), "401") || strings.Contains(string(body), "not login") { + fmt.Println("OK - Token invalidated after logout") + } else { + fmt.Printf("FAIL: expected 401, got: %s\n", string(body)) + } + + // user1 token should still work + fmt.Print(" GET /user/info (user1 token - should still work) ... ") + req, _ = http.NewRequest("GET", baseURL+"/user/info", nil) + req.Header.Set("satoken", user1Token) + resp, err = client.Do(req) + if err != nil { + fmt.Printf("FAIL: %v\n", err) + return + } + body, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if strings.Contains(string(body), `"loginId":"user1"`) { + fmt.Println("OK - user1 token still valid (logout is user-specific)") + } else { + fmt.Printf("FAIL: %s\n", string(body)) + } + + // ============================================================ + // Summary + // ============================================================ + fmt.Println() + fmt.Println("╔══════════════════════════════════════════════════════════════════╗") + fmt.Println("║ Test Summary ║") + fmt.Println("╠══════════════════════════════════════════════════════════════════╣") + fmt.Println("║ ✓ Test 1: Public endpoints - no auth required ║") + fmt.Println("║ ✓ Test 2: Health endpoint - ignores auth ║") + fmt.Println("║ ✓ Test 3: Session & UserInfo with roles & permissions ║") + fmt.Println("║ ✓ Test 4: RBAC - Role-based access control ║") + fmt.Println("║ ✓ Test 5: PBAC - Permission-based access control ║") + fmt.Println("║ ✓ Test 6: Unauthorized access properly rejected ║") + fmt.Println("║ ✓ Test 7: Session isolation between users ║") + fmt.Println("║ ✓ Test 8: Logout invalidates token ║") + fmt.Println("╚══════════════════════════════════════════════════════════════════╝") + fmt.Println() + + time.Sleep(100 * time.Millisecond) +} diff --git a/examples/beego/beego-example/go.mod b/examples/beego/beego-example/go.mod new file mode 100644 index 0000000..c4a3279 --- /dev/null +++ b/examples/beego/beego-example/go.mod @@ -0,0 +1,42 @@ +module github.com/click33/sa-token-go/examples/beego/beego-example + +go 1.24.4 + +replace ( + github.com/click33/sa-token-go/core => ../../../core + github.com/click33/sa-token-go/integrations/beego => ../../../integrations/beego + github.com/click33/sa-token-go/storage/memory => ../../../storage/memory + github.com/click33/sa-token-go/stputil => ../../../stputil +) + +require ( + github.com/beego/beego/v2 v2.3.10 + github.com/click33/sa-token-go/integrations/beego v0.0.0-00010101000000-000000000000 + github.com/click33/sa-token-go/storage/memory v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/click33/sa-token-go/core v0.1.8 // indirect + github.com/click33/sa-token-go/stputil v0.1.8 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/panjf2000/ants/v2 v2.11.3 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/beego/beego-example/go.sum b/examples/beego/beego-example/go.sum new file mode 100644 index 0000000..ef6a7c8 --- /dev/null +++ b/examples/beego/beego-example/go.sum @@ -0,0 +1,66 @@ +github.com/beego/beego/v2 v2.3.10 h1:53us+Lzc/bwFwjyRi62+FKFEcWiIbmKbhW4/FQ3aNEw= +github.com/beego/beego/v2 v2.3.10/go.mod h1:IY3bkfRge4Yj2XhgdMuvznM4HK/ovxQLOHJxAJ+QRgo= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= +github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs= +github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/beego/beego-example/main.go b/examples/beego/beego-example/main.go new file mode 100644 index 0000000..d2a9c6f --- /dev/null +++ b/examples/beego/beego-example/main.go @@ -0,0 +1,135 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/beego/beego/v2/server/web" + "github.com/beego/beego/v2/server/web/context" + integrations "github.com/click33/sa-token-go/integrations/beego" + memory "github.com/click33/sa-token-go/storage/memory" +) + +func main() { + // Initialize sa-token storage and manager + storage := memory.NewStorage() + manager := integrations.NewManager(storage, integrations.DefaultConfig()) + integrations.SetManager(manager) + + // Create sa-token plugin for handlers + plugin := integrations.NewPlugin(manager) + + // Public endpoints - no auth required + web.Get("/public", func(ctx *context.Context) { + ctx.Output.Body([]byte(`{"message": "public endpoint, no auth required"}`)) + }) + + // Login endpoint + web.Post("/login", plugin.LoginHandler) + + // Logout endpoint + web.Post("/logout", plugin.LogoutHandler) + + // User info endpoint - requires login + web.Get("/user/info", plugin.UserInfoHandler) + + // Admin endpoint - requires "admin" role + web.Get("/admin", func(ctx *context.Context) { + ctx.Output.Body([]byte(`{"message": "welcome admin!"}`)) + }) + + // Permission endpoint - requires "user:read" permission + web.Get("/data", func(ctx *context.Context) { + ctx.Output.Body([]byte(`{"data": ["item1", "item2", "item3"]}`)) + }) + + // Profile endpoint - requires login + web.Get("/profile", func(ctx *context.Context) { + ctx.Output.Body([]byte(`{"message": "this is your profile"}`)) + }) + + // Health endpoint - bypasses auth + web.Get("/health", func(ctx *context.Context) { + ctx.Output.Body([]byte(`{"status": "ok"}`)) + }) + + // Set permissions for a user (demo) + web.Post("/setup", func(ctx *context.Context) { + // Login first (creates session), then set roles/permissions + // This ensures roles persist after login + integrations.Login("admin") + integrations.SetRoles("admin", []string{"admin"}) + integrations.SetPermissions("admin", []string{"user:*", "admin:*"}) + + integrations.Login("user1") + integrations.SetRoles("user1", []string{"user"}) + integrations.SetPermissions("user1", []string{"user:read", "user:write"}) + + ctx.Output.Body([]byte(`{"message": "demo users configured"}`)) + }) + + // Get session info + web.Get("/session", func(ctx *context.Context) { + token := ctx.Input.Header("satoken") + if token == "" { + token = ctx.Input.Cookie("satoken") + } + + if token == "" { + ctx.Output.Body([]byte(`{"error": "no token"}`)) + return + } + + isLogin := integrations.IsLogin(token) + loginID, _ := integrations.GetLoginID(token) + + result := map[string]interface{}{ + "isLogin": isLogin, + "loginId": loginID, + } + + data, _ := json.Marshal(result) + ctx.Output.Body(data) + }) + + // ========== Register filters for protected routes ========== + + // /user/info requires login + web.InsertFilter("/user/info", web.BeforeRouter, integrations.CheckLogin()) + + // /admin requires admin role + web.InsertFilter("/admin", web.BeforeRouter, integrations.CheckRole("admin")) + + // /data requires user:read permission + web.InsertFilter("/data", web.BeforeRouter, integrations.CheckPermission("user:read")) + + // /profile requires login + web.InsertFilter("/profile", web.BeforeRouter, integrations.CheckLogin()) + + // /logout requires login + web.InsertFilter("/logout", web.BeforeRouter, integrations.CheckLogin()) + + // /session requires login + web.InsertFilter("/session", web.BeforeRouter, integrations.CheckLogin()) + + // /health bypasses auth (ignore) + web.InsertFilter("/health", web.BeforeRouter, integrations.Ignore()) + + // Run server + fmt.Println("Server starting on :8080") + fmt.Println("Endpoints:") + fmt.Println(" GET /public - Public (no auth)") + fmt.Println(" POST /login - Login (public)") + fmt.Println(" POST /logout - Logout (requires login)") + fmt.Println(" GET /user/info - User info (requires login)") + fmt.Println(" GET /admin - Admin only (requires admin role)") + fmt.Println(" GET /data - Data (requires user:read permission)") + fmt.Println(" GET /profile - Profile (requires login)") + fmt.Println(" GET /health - Health check (ignores auth)") + fmt.Println(" POST /setup - Setup demo users") + fmt.Println(" GET /session - Check session (requires login)") + + log.Println("Server starting...") + web.Run() +} diff --git a/go.work b/go.work index b8789bc..4fbc7f6 100644 --- a/go.work +++ b/go.work @@ -3,8 +3,10 @@ go 1.25.4 use ( ./core ./examples/hertz/herz-example + ./examples/beego/beego-example ./examples/kratos/kratos-example ./examples/multi-certification + ./integrations/beego ./integrations/chi ./integrations/echo ./integrations/fiber diff --git a/go.work.sum b/go.work.sum index 3b442f2..24d2af3 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,5 +1,6 @@ cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= @@ -8,22 +9,33 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog= github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/cloudflare/golz4 v0.0.0-20240916140612-caecf3c00c06/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -31,8 +43,17 @@ github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbi github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= +github.com/couchbase/gomemcached v0.3.3/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= +github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elastic/go-elasticsearch/v6 v6.8.10/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.11.2-0.20230627204322-7d0032219fcb h1:kxNVXsNro/lpR5WD+P1FI/yUHn2G03Glber3k8cQL2Y= github.com/envoyproxy/go-control-plane v0.11.2-0.20230627204322-7d0032219fcb/go.mod h1:GxGqnjWzl1Gz8WfAfMJSfhvsi4EPZayRb25nLHDWXyA= @@ -51,6 +72,10 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -61,9 +86,11 @@ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gogf/gf/v2 v2.9.4/go.mod h1:Ukl+5HUH9S7puBmNLR4L1zUqeRwi0nrW4OigOknEztU= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= @@ -73,14 +100,21 @@ github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo= @@ -88,7 +122,9 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE= @@ -99,19 +135,24 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= @@ -119,17 +160,22 @@ github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:Om github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/shirou/gopsutil/v3 v3.23.6 h1:5y46WPI9QBKBbK7EEccUPNXpJpNrvPuTD0O2zHEHT08= github.com/shirou/gopsutil/v3 v3.23.6/go.mod h1:j7QX50DrXYggrpN30W0Mo+I4/8U2UUIQrnrhqUeWrAU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= @@ -143,14 +189,20 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw= +go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA= +go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.2/go.mod h1:bx//lU66dPzNT+Y0hHA12ciKoMOH9iixEwCqC1OeQWQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= @@ -162,12 +214,16 @@ go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42s go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= @@ -175,11 +231,14 @@ golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= @@ -188,6 +247,7 @@ golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -202,13 +262,19 @@ google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0 google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/integrations/beego/annotation.go b/integrations/beego/annotation.go new file mode 100644 index 0000000..a308015 --- /dev/null +++ b/integrations/beego/annotation.go @@ -0,0 +1,151 @@ +package beego + +import ( + "strings" + + "github.com/beego/beego/v2/server/web" + beegoctx "github.com/beego/beego/v2/server/web/context" + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/stputil" +) + +// Annotation constants | 注解常量 +const ( + TagSaCheckLogin = "sa_check_login" + TagSaCheckRole = "sa_check_role" + TagSaCheckPermission = "sa_check_permission" + TagSaCheckDisable = "sa_check_disable" + TagSaIgnore = "sa_ignore" +) + +// Annotation annotation structure | 注解结构体 +type Annotation struct { + CheckLogin bool `json:"checkLogin"` + CheckRole []string `json:"checkRole"` + CheckPermission []string `json:"checkPermission"` + CheckDisable bool `json:"checkDisable"` + Ignore bool `json:"ignore"` +} + +// Validate validates if annotation is valid | 验证注解是否有效 +func (a *Annotation) Validate() bool { + if a.Ignore { + return true + } + + count := 0 + if a.CheckLogin { + count++ + } + if len(a.CheckRole) > 0 { + count++ + } + if len(a.CheckPermission) > 0 { + count++ + } + if a.CheckDisable { + count++ + } + + return count <= 1 +} + +// GetHandler gets handler with annotations | 获取带注解的处理器 +func GetHandler(handler web.FilterFunc, annotations ...*Annotation) web.FilterFunc { + return func(ctx *beegoctx.Context) { + if len(annotations) > 0 && annotations[0].Ignore { + if handler != nil { + handler(ctx) + } + return + } + + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, stputil.GetManager()) + token := saCtx.GetTokenValue() + + if token == "" { + writeErrorResponse(ctx, core.NewNotLoginError()) + return + } + + if !stputil.IsLogin(token) { + writeErrorResponse(ctx, core.NewNotLoginError()) + return + } + + loginID, err := stputil.GetLoginID(token) + if err != nil { + writeErrorResponse(ctx, err) + return + } + + // Check if account is disabled | 检查是否被封禁 + if len(annotations) > 0 && annotations[0].CheckDisable { + if stputil.IsDisable(loginID) { + writeErrorResponse(ctx, core.NewAccountDisabledError(loginID)) + return + } + } + + // Check permission | 检查权限 + if len(annotations) > 0 && len(annotations[0].CheckPermission) > 0 { + hasPermission := false + for _, perm := range annotations[0].CheckPermission { + if stputil.HasPermission(loginID, strings.TrimSpace(perm)) { + hasPermission = true + break + } + } + if !hasPermission { + writeErrorResponse(ctx, core.NewPermissionDeniedError(strings.Join(annotations[0].CheckPermission, ","))) + return + } + } + + // Check role | 检查角色 + if len(annotations) > 0 && len(annotations[0].CheckRole) > 0 { + hasRole := false + for _, role := range annotations[0].CheckRole { + if stputil.HasRole(loginID, strings.TrimSpace(role)) { + hasRole = true + break + } + } + if !hasRole { + writeErrorResponse(ctx, core.NewRoleDeniedError(strings.Join(annotations[0].CheckRole, ","))) + return + } + } + + // All checks passed, execute original handler | 所有检查通过,执行原函数 + if handler != nil { + handler(ctx) + } + } +} + +// CheckLogin filter for login checking | 检查登录过滤器 +func CheckLogin() web.FilterFunc { + return GetHandler(nil, &Annotation{CheckLogin: true}) +} + +// CheckRole filter for role checking | 检查角色过滤器 +func CheckRole(roles ...string) web.FilterFunc { + return GetHandler(nil, &Annotation{CheckRole: roles}) +} + +// CheckPermission filter for permission checking | 检查权限过滤器 +func CheckPermission(perms ...string) web.FilterFunc { + return GetHandler(nil, &Annotation{CheckPermission: perms}) +} + +// CheckDisable filter for checking if account is disabled | 检查是否被封禁过滤器 +func CheckDisable() web.FilterFunc { + return GetHandler(nil, &Annotation{CheckDisable: true}) +} + +// Ignore filter to ignore authentication | 忽略认证过滤器 +func Ignore() web.FilterFunc { + return GetHandler(nil, &Annotation{Ignore: true}) +} diff --git a/integrations/beego/context.go b/integrations/beego/context.go new file mode 100644 index 0000000..3e9ac03 --- /dev/null +++ b/integrations/beego/context.go @@ -0,0 +1,150 @@ +package beego + +import ( + beegoctx "github.com/beego/beego/v2/server/web/context" + "github.com/click33/sa-token-go/core/adapter" +) + +// BeegoContext Beego request context adapter | Beego请求上下文适配器 +type BeegoContext struct { + ctx *beegoctx.Context + aborted bool +} + +// NewBeegoContext creates a Beego context adapter | 创建Beego上下文适配器 +func NewBeegoContext(ctx *beegoctx.Context) adapter.RequestContext { + return &BeegoContext{ctx: ctx} +} + +// GetHeader gets request header | 获取请求头 +func (b *BeegoContext) GetHeader(key string) string { + return b.ctx.Input.Header(key) +} + +// GetQuery gets query parameter | 获取查询参数 +func (b *BeegoContext) GetQuery(key string) string { + return b.ctx.Input.Query(key) +} + +// GetCookie gets cookie | 获取Cookie +func (b *BeegoContext) GetCookie(key string) string { + return b.ctx.Input.Cookie(key) +} + +// SetHeader sets response header | 设置响应头 +func (b *BeegoContext) SetHeader(key, value string) { + b.ctx.ResponseWriter.Header().Set(key, value) +} + +// SetCookie sets cookie | 设置Cookie +func (b *BeegoContext) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { + b.ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) +} + +// GetClientIP gets client IP address | 获取客户端IP地址 +func (b *BeegoContext) GetClientIP() string { + return b.ctx.Input.IP() +} + +// GetMethod gets request method | 获取请求方法 +func (b *BeegoContext) GetMethod() string { + return b.ctx.Input.Method() +} + +// GetPath gets request path | 获取请求路径 +func (b *BeegoContext) GetPath() string { + return b.ctx.Input.URI() +} + +// Set sets context value | 设置上下文值 +func (b *BeegoContext) Set(key string, value interface{}) { + b.ctx.Input.SetData(key, value) +} + +// Get gets context value | 获取上下文值 +func (b *BeegoContext) Get(key string) (interface{}, bool) { + value := b.ctx.Input.GetData(key) + return value, value != nil +} + +// ============ Additional Required Methods | 额外必需的方法 ============ + +// GetHeaders implements adapter.RequestContext. +func (b *BeegoContext) GetHeaders() map[string][]string { + headers := make(map[string][]string) + for key, values := range b.ctx.Request.Header { + headers[key] = values + } + return headers +} + +// GetQueryAll implements adapter.RequestContext. +func (b *BeegoContext) GetQueryAll() map[string][]string { + params := make(map[string][]string) + return params +} + +// GetPostForm implements adapter.RequestContext. +func (b *BeegoContext) GetPostForm(key string) string { + return b.ctx.Request.FormValue(key) +} + +// GetBody implements adapter.RequestContext. +func (b *BeegoContext) GetBody() ([]byte, error) { + return b.ctx.Input.RequestBody, nil +} + +// GetURL implements adapter.RequestContext. +func (b *BeegoContext) GetURL() string { + return b.ctx.Input.URL() +} + +// GetUserAgent implements adapter.RequestContext. +func (b *BeegoContext) GetUserAgent() string { + return b.ctx.Request.UserAgent() +} + +// SetCookieWithOptions implements adapter.RequestContext. +func (b *BeegoContext) SetCookieWithOptions(options *adapter.CookieOptions) { + b.ctx.SetCookie( + options.Name, + options.Value, + options.MaxAge, + options.Path, + options.Domain, + options.Secure, + options.HttpOnly, + ) +} + +// GetString implements adapter.RequestContext. +func (b *BeegoContext) GetString(key string) string { + value := b.ctx.Input.GetData(key) + if value == nil { + return "" + } + if str, ok := value.(string); ok { + return str + } + return "" +} + +// MustGet implements adapter.RequestContext. +func (b *BeegoContext) MustGet(key string) any { + value := b.ctx.Input.GetData(key) + if value == nil { + panic("key not found: " + key) + } + return value +} + +// Abort implements adapter.RequestContext. +func (b *BeegoContext) Abort() { + b.aborted = true + b.ctx.Abort(500, "aborted") // 必须选一个默认状态码和消息 +} + +// IsAborted implements adapter.RequestContext. +func (b *BeegoContext) IsAborted() bool { + return b.aborted +} diff --git a/integrations/beego/export.go b/integrations/beego/export.go new file mode 100644 index 0000000..8fbb23e --- /dev/null +++ b/integrations/beego/export.go @@ -0,0 +1,380 @@ +package beego + +import ( + "time" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/stputil" +) + +// ==============================类型别名 vs 显式包装函数========================================= +// 场景 推荐方式 +// 类型定义 type T = core.T(别名) +// 常量 const C = core.C +// 简单函数(无业务逻辑) 别名变量 var F = core.F(最简洁) +// 函数(需要文档/校验/未来扩展) 显式包装 func F(...) ... { return core.F(...) } +// API 一致性要求高 显式包装 +// ============================================================================================== + +// ============ Re-export core types | 重新导出核心类型 ============ + +// Configuration related types | 配置相关类型 +type ( + Config = core.Config + CookieConfig = core.CookieConfig + TokenStyle = core.TokenStyle +) + +// Token style constants | Token风格常量 +const ( + TokenStyleUUID = core.TokenStyleUUID + TokenStyleSimple = core.TokenStyleSimple + TokenStyleRandom32 = core.TokenStyleRandom32 + TokenStyleRandom64 = core.TokenStyleRandom64 + TokenStyleRandom128 = core.TokenStyleRandom128 + TokenStyleJWT = core.TokenStyleJWT + TokenStyleHash = core.TokenStyleHash + TokenStyleTimestamp = core.TokenStyleTimestamp + TokenStyleTik = core.TokenStyleTik +) + +// Core types | 核心类型 +type ( + Manager = core.Manager + TokenInfo = core.TokenInfo + Session = core.Session + TokenGenerator = core.TokenGenerator + SaTokenContext = core.SaTokenContext + Builder = core.Builder + NonceManager = core.NonceManager + RefreshTokenInfo = core.RefreshTokenInfo + RefreshTokenManager = core.RefreshTokenManager + OAuth2Server = core.OAuth2Server + OAuth2Client = core.OAuth2Client + OAuth2AccessToken = core.OAuth2AccessToken + OAuth2GrantType = core.OAuth2GrantType +) + +// Adapter interfaces | 适配器接口 +type ( + Storage = core.Storage + RequestContext = core.RequestContext +) + +// Event related types | 事件相关类型 +type ( + EventListener = core.EventListener + EventManager = core.EventManager + EventData = core.EventData + Event = core.Event + ListenerFunc = core.ListenerFunc + ListenerConfig = core.ListenerConfig +) + +// Event constants | 事件常量 +const ( + EventLogin = core.EventLogin + EventLogout = core.EventLogout + EventKickout = core.EventKickout + EventDisable = core.EventDisable + EventUntie = core.EventUntie + EventRenew = core.EventRenew + EventCreateSession = core.EventCreateSession + EventDestroySession = core.EventDestroySession + EventPermissionCheck = core.EventPermissionCheck + EventRoleCheck = core.EventRoleCheck + EventAll = core.EventAll +) + +// OAuth2 grant type constants | OAuth2授权类型常量 +const ( + GrantTypeAuthorizationCode = core.GrantTypeAuthorizationCode + GrantTypeRefreshToken = core.GrantTypeRefreshToken + GrantTypeClientCredentials = core.GrantTypeClientCredentials + GrantTypePassword = core.GrantTypePassword +) + +// Utility functions | 工具函数 +var ( + RandomString = core.RandomString + IsEmpty = core.IsEmpty + IsNotEmpty = core.IsNotEmpty + DefaultString = core.DefaultString + ContainsString = core.ContainsString + RemoveString = core.RemoveString + UniqueStrings = core.UniqueStrings + MergeStrings = core.MergeStrings + MatchPattern = core.MatchPattern +) + +// ============ Core constructor functions | 核心构造函数 ============ + +// DefaultConfig returns default configuration | 返回默认配置 +func DefaultConfig() *Config { + return core.DefaultConfig() +} + +// NewManager creates a new authentication manager | 创建新的认证管理器 +func NewManager(storage Storage, cfg *Config) *Manager { + return core.NewManager(storage, cfg) +} + +// NewContext creates a new Sa-Token context | 创建新的Sa-Token上下文 +func NewContext(ctx RequestContext, mgr *Manager) *SaTokenContext { + return core.NewContext(ctx, mgr) +} + +// NewSession creates a new session | 创建新的Session +func NewSession(id string, storage Storage, prefix string) *Session { + return core.NewSession(id, storage, prefix) +} + +// LoadSession loads an existing session | 加载已存在的Session +func LoadSession(id string, storage Storage, prefix string) (*Session, error) { + return core.LoadSession(id, storage, prefix) +} + +// NewTokenGenerator creates a new token generator | 创建新的Token生成器 +func NewTokenGenerator(cfg *Config) *TokenGenerator { + return core.NewTokenGenerator(cfg) +} + +// NewEventManager creates a new event manager | 创建新的事件管理器 +func NewEventManager() *EventManager { + return core.NewEventManager() +} + +// NewBuilder creates a new builder for fluent configuration | 创建新的Builder构建器(用于流式配置) +func NewBuilder() *Builder { + return core.NewBuilder() +} + +// NewNonceManager creates a new nonce manager | 创建新的Nonce管理器 +func NewNonceManager(storage Storage, prefix string, ttl ...int64) *NonceManager { + return core.NewNonceManager(storage, prefix, ttl...) +} + +// NewRefreshTokenManager creates a new refresh token manager | 创建新的刷新令牌管理器 +func NewRefreshTokenManager(storage Storage, prefix string, cfg *Config) *RefreshTokenManager { + return core.NewRefreshTokenManager(storage, prefix, cfg) +} + +// NewOAuth2Server creates a new OAuth2 server | 创建新的OAuth2服务器 +func NewOAuth2Server(storage Storage, prefix string) *OAuth2Server { + return core.NewOAuth2Server(storage, prefix) +} + +// ============ Global StpUtil functions | 全局StpUtil函数 ============ + +// SetManager sets the global Manager (must be called first) | 设置全局Manager(必须先调用此方法) +func SetManager(mgr *Manager) { + stputil.SetManager(mgr) +} + +// GetManager gets the global Manager | 获取全局Manager +func GetManager() *Manager { + return stputil.GetManager() +} + +// ============ Authentication | 登录认证 ============ + +// Login performs user login | 用户登录 +func Login(loginID interface{}, device ...string) (string, error) { + return stputil.Login(loginID, device...) +} + +// LoginByToken performs login with specified token | 使用指定Token登录 +func LoginByToken(loginID interface{}, tokenValue string, device ...string) error { + return stputil.LoginByToken(loginID, tokenValue, device...) +} + +// Logout performs user logout | 用户登出 +func Logout(loginID interface{}, device ...string) error { + return stputil.Logout(loginID, device...) +} + +// LogoutByToken performs logout by token | 根据Token登出 +func LogoutByToken(tokenValue string) error { + return stputil.LogoutByToken(tokenValue) +} + +// IsLogin checks if the user is logged in | 检查用户是否已登录 +func IsLogin(tokenValue string) bool { + return stputil.IsLogin(tokenValue) +} + +// CheckLoginByToken checks login status (throws error if not logged in) | 检查登录状态(未登录抛出错误) +func CheckLoginByToken(tokenValue string) error { + return stputil.CheckLogin(tokenValue) +} + +// GetLoginID gets the login ID from token | 从Token获取登录ID +func GetLoginID(tokenValue string) (string, error) { + return stputil.GetLoginID(tokenValue) +} + +// GetLoginIDNotCheck gets login ID without checking | 获取登录ID(不检查) +func GetLoginIDNotCheck(tokenValue string) (string, error) { + return stputil.GetLoginIDNotCheck(tokenValue) +} + +// GetTokenValue gets the token value for a login ID | 获取登录ID对应的Token值 +func GetTokenValue(loginID interface{}, device ...string) (string, error) { + return stputil.GetTokenValue(loginID, device...) +} + +// GetTokenInfo gets token information | 获取Token信息 +func GetTokenInfo(tokenValue string) (*TokenInfo, error) { + return stputil.GetTokenInfo(tokenValue) +} + +// ============ Kickout | 踢人下线 ============ + +// Kickout kicks out a user session | 踢人下线 +func Kickout(loginID interface{}, device ...string) error { + return stputil.Kickout(loginID, device...) +} + +// ============ Account Disable | 账号封禁 ============ + +// Disable disables an account for specified duration | 封禁账号(指定时长) +func Disable(loginID interface{}, duration time.Duration) error { + return stputil.Disable(loginID, duration) +} + +// IsDisable checks if an account is disabled | 检查账号是否被封禁 +func IsDisable(loginID interface{}) bool { + return stputil.IsDisable(loginID) +} + +// CheckDisableByToken checks if account is disabled (throws error if disabled) | 检查Token对应账号是否被封禁(被封禁则抛出错误) +func CheckDisableByToken(tokenValue string) error { + return stputil.CheckDisable(tokenValue) +} + +// GetDisableTime gets remaining disabled time | 获取账号剩余封禁时间 +func GetDisableTime(loginID interface{}) (int64, error) { + return stputil.GetDisableTime(loginID) +} + +// Untie unties/unlocks an account | 解除账号封禁 +func Untie(loginID interface{}) error { + return stputil.Untie(loginID) +} + +// ============ Permission Check | 权限验证 ============ + +// CheckPermissionByToken checks if the token has specified permission | 检查Token是否拥有指定权限 +func CheckPermissionByToken(tokenValue string, permission string) error { + return stputil.CheckPermission(tokenValue, permission) +} + +// HasPermission checks if the account has specified permission (returns bool) | 检查账号是否拥有指定权限(返回布尔值) +func HasPermission(loginID interface{}, permission string) bool { + return stputil.HasPermission(loginID, permission) +} + +// SetPermissions sets permissions for a login ID | 设置用户权限 +func SetPermissions(loginID interface{}, permissions []string) error { + return stputil.SetPermissions(loginID, permissions) +} + +// CheckPermissionAndByToken checks if the token has all specified permissions (AND logic) | 检查Token是否拥有所有指定权限(AND逻辑) +func CheckPermissionAndByToken(tokenValue string, permissions []string) error { + return stputil.CheckPermissionAnd(tokenValue, permissions) +} + +// CheckPermissionOrByToken checks if the token has any of the specified permissions (OR logic) | 检查Token是否拥有指定权限中的任意一个(OR逻辑) +func CheckPermissionOrByToken(tokenValue string, permissions []string) error { + return stputil.CheckPermissionOr(tokenValue, permissions) +} + +// GetPermissionListByToken gets the permission list for a token | 获取Token的权限列表 +func GetPermissionListByToken(tokenValue string) ([]string, error) { + return stputil.GetPermissionList(tokenValue) +} + +// ============ Role Check | 角色验证 ============ + +// CheckRoleByToken checks if the token has specified role | 检查Token是否拥有指定角色 +func CheckRoleByToken(tokenValue string, role string) error { + return stputil.CheckRole(tokenValue, role) +} + +// HasRole checks if the account has specified role (returns bool) | 检查账号是否拥有指定角色(返回布尔值) +func HasRole(loginID interface{}, role string) bool { + return stputil.HasRole(loginID, role) +} + +// SetRoles sets roles for a login ID | 设置用户角色 +func SetRoles(loginID interface{}, roles []string) error { + return stputil.SetRoles(loginID, roles) +} + +// CheckRoleAndByToken checks if the token has all specified roles (AND logic) | 检查Token是否拥有所有指定角色(AND逻辑) +func CheckRoleAndByToken(tokenValue string, roles []string) error { + return stputil.CheckRoleAnd(tokenValue, roles) +} + +// CheckRoleOrByToken checks if the token has any of the specified roles (OR logic) | 检查Token是否拥有指定角色中的任意一个(OR逻辑) +func CheckRoleOrByToken(tokenValue string, roles []string) error { + return stputil.CheckRoleOr(tokenValue, roles) +} + +// GetRoleListByToken gets the role list for a token | 获取Token的角色列表 +func GetRoleListByToken(tokenValue string) ([]string, error) { + return stputil.GetRoleList(tokenValue) +} + +// ============ Session Management | Session管理 ============ + +// GetSession gets the session for a login ID | 获取登录ID的Session +func GetSession(loginID interface{}) (*Session, error) { + return stputil.GetSession(loginID) +} + +// GetSessionByToken gets the session by token | 根据Token获取Session +func GetSessionByToken(tokenValue string) (*Session, error) { + return stputil.GetSessionByToken(tokenValue) +} + +// GetTokenSession gets the token session | 获取Token的Session +func GetTokenSession(tokenValue string) (*Session, error) { + return stputil.GetTokenSession(tokenValue) +} + +// ============ Security Features | 安全特性 ============ + +// GenerateNonce generates a new nonce token | 生成新的Nonce令牌 +func GenerateNonce() (string, error) { + return stputil.GenerateNonce() +} + +// VerifyNonce verifies a nonce token | 验证Nonce令牌 +func VerifyNonce(nonce string) bool { + return stputil.VerifyNonce(nonce) +} + +// LoginWithRefreshToken performs login and returns refresh token info | 登录并返回刷新令牌信息 +func LoginWithRefreshToken(loginID interface{}, device ...string) (*RefreshTokenInfo, error) { + return stputil.LoginWithRefreshToken(loginID, device...) +} + +// RefreshAccessToken refreshes the access token using a refresh token | 使用刷新令牌刷新访问令牌 +func RefreshAccessToken(refreshToken string) (*RefreshTokenInfo, error) { + return stputil.RefreshAccessToken(refreshToken) +} + +// RevokeRefreshToken revokes a refresh token | 撤销刷新令牌 +func RevokeRefreshToken(refreshToken string) error { + return stputil.RevokeRefreshToken(refreshToken) +} + +// GetOAuth2Server gets the OAuth2 server instance | 获取OAuth2服务器实例 +func GetOAuth2Server() *OAuth2Server { + return stputil.GetOAuth2Server() +} + +// 字符串常量,没有包装必要 +// Version Sa-Token-Go version | Sa-Token-Go版本 +const Version = core.Version diff --git a/integrations/beego/go.mod b/integrations/beego/go.mod new file mode 100644 index 0000000..a2a3fc6 --- /dev/null +++ b/integrations/beego/go.mod @@ -0,0 +1,39 @@ +module github.com/click33/sa-token-go/integrations/beego + +go 1.24.2 + +require ( + github.com/beego/beego/v2 v2.3.10 + github.com/click33/sa-token-go/core v0.1.8 + github.com/click33/sa-token-go/stputil v0.1.8 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/click33/sa-token-go/storage/memory v0.1.8 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/panjf2000/ants/v2 v2.11.3 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace ( + github.com/click33/sa-token-go/core => ../../core + github.com/click33/sa-token-go/stputil => ../../stputil +) diff --git a/integrations/beego/go.sum b/integrations/beego/go.sum new file mode 100644 index 0000000..2b14b0a --- /dev/null +++ b/integrations/beego/go.sum @@ -0,0 +1,68 @@ +github.com/beego/beego/v2 v2.3.10 h1:53us+Lzc/bwFwjyRi62+FKFEcWiIbmKbhW4/FQ3aNEw= +github.com/beego/beego/v2 v2.3.10/go.mod h1:IY3bkfRge4Yj2XhgdMuvznM4HK/ovxQLOHJxAJ+QRgo= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/click33/sa-token-go/storage/memory v0.1.8 h1:kyhSFWBfmO8XNXye+E9d4gw0bJE2HePwWg5xmkE2rFw= +github.com/click33/sa-token-go/storage/memory v0.1.8/go.mod h1:cPQUpY0D/3lGyCyzW/lZ4pc+O9HPfYV3FLeWABpmPnk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= +github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs= +github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integrations/beego/plugin.go b/integrations/beego/plugin.go new file mode 100644 index 0000000..caab60f --- /dev/null +++ b/integrations/beego/plugin.go @@ -0,0 +1,260 @@ +package beego + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/beego/beego/v2/server/web" + beegoctx "github.com/beego/beego/v2/server/web/context" + "github.com/click33/sa-token-go/core" +) + +// Plugin Beego plugin for Sa-Token | Beego插件 +type Plugin struct { + manager *core.Manager +} + +// NewPlugin creates a Beego plugin | 创建Beego插件 +func NewPlugin(manager *core.Manager) *Plugin { + return &Plugin{ + manager: manager, + } +} + +// AuthMiddleware authentication middleware | 认证中间件 +func (p *Plugin) AuthMiddleware() web.FilterFunc { + return func(ctx *beegoctx.Context) { + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, p.manager) + + if err := saCtx.CheckLogin(); err != nil { + writeErrorResponse(ctx, err) + return + } + + ctx.Input.SetData("satoken", saCtx) + } +} + +// PathAuthMiddleware path-based authentication middleware | 基于路径的鉴权中间件 +func (p *Plugin) PathAuthMiddleware(config *core.PathAuthConfig) web.FilterFunc { + return func(ctx *beegoctx.Context) { + path := ctx.Input.URI() + token := ctx.Input.Header(p.manager.GetConfig().TokenName) + if token == "" { + token = ctx.Input.Cookie(p.manager.GetConfig().TokenName) + } + + result := core.ProcessAuth(path, token, config, p.manager) + + if result.ShouldReject() { + writeErrorResponse(ctx, core.NewPathAuthRequiredError(path)) + return + } + + if result.IsValid && result.TokenInfo != nil { + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, p.manager) + ctx.Input.SetData("satoken", saCtx) + ctx.Input.SetData("loginID", result.LoginID()) + } + } +} + +// PermissionRequired permission validation middleware | 权限验证中间件 +func (p *Plugin) PermissionRequired(permission string) web.FilterFunc { + return func(ctx *beegoctx.Context) { + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, p.manager) + + if err := saCtx.CheckLogin(); err != nil { + writeErrorResponse(ctx, err) + return + } + + if !saCtx.HasPermission(permission) { + writeErrorResponse(ctx, core.NewPermissionDeniedError(permission)) + return + } + + ctx.Input.SetData("satoken", saCtx) + } +} + +// RoleRequired role validation middleware | 角色验证中间件 +func (p *Plugin) RoleRequired(role string) web.FilterFunc { + return func(ctx *beegoctx.Context) { + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, p.manager) + + if err := saCtx.CheckLogin(); err != nil { + writeErrorResponse(ctx, err) + return + } + + if !saCtx.HasRole(role) { + writeErrorResponse(ctx, core.NewRoleDeniedError(role)) + return + } + + ctx.Input.SetData("satoken", saCtx) + } +} + +// LoginHandler login handler | 登录处理器 +func (p *Plugin) LoginHandler(ctx *beegoctx.Context) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + Device string `json:"device"` + } + + // Get request body - beego v2 may need CopyBody to populate RequestBody + body := ctx.Input.RequestBody + if len(body) == 0 { + body = ctx.Input.CopyBody(1024 << 10) // 1MB max + } + + if err := json.Unmarshal(body, &req); err != nil { + writeErrorResponse(ctx, core.NewError(core.CodeBadRequest, "invalid request parameters", err)) + return + } + + device := req.Device + if device == "" { + device = "default" + } + + token, err := p.manager.Login(req.Username, device) + if err != nil { + writeErrorResponse(ctx, core.NewError(core.CodeServerError, "login failed", err)) + return + } + + cfg := p.manager.GetConfig() + if cfg.IsReadCookie { + maxAge := max(int(cfg.Timeout), 0) + ctx.SetCookie( + cfg.TokenName, + token, + maxAge, + cfg.CookieConfig.Path, + cfg.CookieConfig.Domain, + cfg.CookieConfig.Secure, + cfg.CookieConfig.HttpOnly, + ) + } + + writeSuccessResponse(ctx, map[string]interface{}{ + "token": token, + }) +} + +// LogoutHandler logout handler | 登出处理器 +func (p *Plugin) LogoutHandler(ctx *beegoctx.Context) { + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, p.manager) + + loginID, err := saCtx.GetLoginID() + if err != nil { + writeErrorResponse(ctx, err) + return + } + + if err := p.manager.Logout(loginID); err != nil { + writeErrorResponse(ctx, core.NewError(core.CodeServerError, "logout failed", err)) + return + } + + writeSuccessResponse(ctx, map[string]interface{}{ + "message": "logout successful", + }) +} + +// UserInfoHandler user info handler | 获取用户信息处理器 +func (p *Plugin) UserInfoHandler(ctx *beegoctx.Context) { + bCtx := NewBeegoContext(ctx) + saCtx := core.NewContext(bCtx, p.manager) + + loginID, err := saCtx.GetLoginID() + if err != nil { + writeErrorResponse(ctx, err) + return + } + + permissions, _ := p.manager.GetPermissions(loginID) + roles, _ := p.manager.GetRoles(loginID) + + writeSuccessResponse(ctx, map[string]interface{}{ + "loginId": loginID, + "permissions": permissions, + "roles": roles, + }) +} + +// GetSaToken gets Sa-Token context from Beego context | 从Beego上下文获取Sa-Token上下文 +func GetSaToken(ctx *beegoctx.Context) (*core.SaTokenContext, bool) { + satoken := ctx.Input.GetData("satoken") + if satoken == nil { + return nil, false + } + saCtx, ok := satoken.(*core.SaTokenContext) + return saCtx, ok +} + +// ============ Error Handling Helpers | 错误处理辅助函数 ============ + +// writeErrorResponse writes a standardized error response | 写入标准化的错误响应 +func writeErrorResponse(ctx *beegoctx.Context, err error) error { + var saErr *core.SaTokenError + var code int + var message string + var httpStatus int + + if errors.As(err, &saErr) { + code = saErr.Code + message = saErr.Message + httpStatus = getHTTPStatusFromCode(code) + } else { + code = core.CodeServerError + message = err.Error() + httpStatus = http.StatusInternalServerError + } + + ctx.ResponseWriter.WriteHeader(httpStatus) + ctx.ResponseWriter.Header().Set("Content-Type", "application/json") + return json.NewEncoder(ctx.ResponseWriter).Encode(map[string]interface{}{ + "code": code, + "message": message, + "error": err.Error(), + }) +} + +// writeSuccessResponse writes a standardized success response | 写入标准化的成功响应 +func writeSuccessResponse(ctx *beegoctx.Context, data interface{}) error { + ctx.ResponseWriter.Header().Set("Content-Type", "application/json") + return json.NewEncoder(ctx.ResponseWriter).Encode(map[string]interface{}{ + "code": core.CodeSuccess, + "message": "success", + "data": data, + }) +} + +// getHTTPStatusFromCode converts Sa-Token error code to HTTP status | 将Sa-Token错误码转换为HTTP状态码 +func getHTTPStatusFromCode(code int) int { + switch code { + case core.CodeNotLogin: + return http.StatusUnauthorized + case core.CodePermissionDenied: + return http.StatusForbidden + case core.CodeBadRequest: + return http.StatusBadRequest + case core.CodeNotFound: + return http.StatusNotFound + case core.CodeServerError: + return http.StatusInternalServerError + default: + return http.StatusInternalServerError + } +} -- Gitee