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 0000000..5008ddf Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/IMG_3976.JPG b/docs/IMG_3976.JPG deleted file mode 100644 index 00e77f2..0000000 Binary files a/docs/IMG_3976.JPG and /dev/null differ diff --git a/docs/guide/path-auth.md b/docs/guide/path-auth.md new file mode 100644 index 0000000..b32e390 --- /dev/null +++ b/docs/guide/path-auth.md @@ -0,0 +1,551 @@ +# Path-Based Authentication + +Path-based authentication allows you to configure which paths require authentication and which paths are excluded, providing flexible access control for your application. + +## Features + +- **Ant-style wildcard patterns** - Support for `/**`, `/*`, `*.html` patterns +- **Include/Exclude configuration** - Fine-grained control over which paths need authentication +- **Custom validators** - Optional login ID validation functions +- **Framework integration** - Works seamlessly with all supported frameworks +- **Token extraction** - Automatically extracts tokens from headers and cookies + +## Pattern Matching + +The path matching supports Ant-style wildcards: + +- `/**` - Matches all paths +- `/api/**` - Matches all paths starting with `/api/` +- `/api/*` - Matches single-level paths under `/api/` (e.g., `/api/user`, but not `/api/user/profile`) +- `*.html` - Matches paths ending with `.html` +- `/exact` - Exact path match + +### Pattern Examples + +```go +// Match all paths +"/**" + +// Match all API paths +"/api/**" + +// Match single-level API paths +"/api/*" + +// Match static files +"*.html" +"*.css" +"*.js" + +// Match specific paths +"/login" +"/logout" +"/public/**" +``` + +## Usage + +### Basic Configuration + +The simplest way to use path-based authentication is through middleware: + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gin" + "github.com/click33/sa-token-go/storage/memory" +) + +func main() { + // Initialize manager + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + TokenName("Authorization"). + Timeout(86400). + Build() + + // Create path authentication configuration + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). // Paths that require authentication + SetExclude([]string{"/api/public/**"}) // Paths excluded from authentication + + // Create plugin and use middleware + plugin := gin.NewPlugin(manager) + r := gin.Default() + + // Apply path authentication middleware + r.Use(plugin.PathAuthMiddleware(config)) + + // Your routes + r.GET("/api/user/info", getUserInfo) + r.GET("/api/public/status", getStatus) // This path is excluded + + r.Run(":8080") +} +``` + +### Multiple Include/Exclude Patterns + +You can specify multiple patterns for more complex scenarios: + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{ + "/api/**", // All API paths + "/admin/**", // All admin paths + "/user/profile", // Specific user profile path + }). + SetExclude([]string{ + "/api/public/**", // Public API paths + "/api/auth/login", // Login endpoint + "/api/auth/register", // Register endpoint + "*.html", // Static HTML files + "*.css", // CSS files + "*.js", // JavaScript files + }) +``` + +### With Custom Validator + +You can add custom validation logic for login IDs: + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}). + SetValidator(func(loginID string) bool { + // Custom validation logic + // For example, check if user is banned + if loginID == "banned_user" { + return false + } + + // Check if user account is active + // You can query your database here + // return isUserActive(loginID) + + return true + }) +``` + +### Complete Example with Gin + +```go +package main + +import ( + "net/http" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gin" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gin-gonic/gin" +) + +func main() { + // Initialize Sa-Token manager + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + TokenName("Authorization"). + Timeout(86400). + Build() + + // Configure path authentication + pathAuthConfig := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{ + "/api/auth/login", + "/api/auth/register", + "/api/public/**", + }) + + // Create Gin router + r := gin.Default() + + // Create plugin + plugin := gin.NewPlugin(manager) + + // Apply path authentication middleware + r.Use(plugin.PathAuthMiddleware(pathAuthConfig)) + + // Public routes (excluded from auth) + r.POST("/api/auth/login", plugin.LoginHandler) + r.POST("/api/auth/register", registerHandler) + r.GET("/api/public/status", getStatus) + + // Protected routes (require authentication) + api := r.Group("/api") + { + api.GET("/user/info", getUserInfo) + api.GET("/user/profile", getUserProfile) + api.POST("/user/update", updateUser) + } + + r.Run(":8080") +} + +func getUserInfo(c *gin.Context) { + // Get login ID from context (set by PathAuthMiddleware) + loginID, exists := c.Get("loginID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "loginID": loginID, + "message": "User info retrieved", + }) +} + +func getUserProfile(c *gin.Context) { + loginID, _ := c.Get("loginID") + c.JSON(http.StatusOK, gin.H{ + "loginID": loginID, + "profile": "User profile data", + }) +} + +func updateUser(c *gin.Context) { + loginID, _ := c.Get("loginID") + c.JSON(http.StatusOK, gin.H{ + "loginID": loginID, + "message": "User updated", + }) +} + +func registerHandler(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Registration successful"}) +} + +func getStatus(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} +``` + +### Using ProcessAuth Directly + +If you need more control, you can use `ProcessAuth` directly in your handlers: + +```go +import "github.com/click33/sa-token-go/core" + +func customHandler(c *gin.Context) { + path := c.Request.URL.Path + token := c.GetHeader("Authorization") + if token == "" { + token, _ = c.Cookie("Authorization") + } + + config := core.NewPathAuthConfig().SetInclude([]string{"/api/**"}) + result := core.ProcessAuth(path, token, config, manager) + + if result.ShouldReject() { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "path authentication required", + "path": path, + }) + c.Abort() + return + } + + // Use result.LoginID() to get the login ID + loginID := result.LoginID() + if loginID == "" { + // Token is valid but loginID not available + // You might need to get it another way + } + + // Continue with your logic + c.JSON(http.StatusOK, gin.H{"loginID": loginID}) +} +``` + +## Framework Examples + +### Gin + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gin" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gin-gonic/gin" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := gin.NewPlugin(manager) + r := gin.Default() + r.Use(plugin.PathAuthMiddleware(config)) + + r.Run(":8080") +} +``` + +### Echo + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/echo" + "github.com/click33/sa-token-go/storage/memory" + "github.com/labstack/echo/v4" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := echo.NewPlugin(manager) + e := echo.New() + e.Use(plugin.PathAuthMiddleware(config)) + + e.Start(":8080") +} +``` + +### Fiber + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/fiber" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gofiber/fiber/v2" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := fiber.NewPlugin(manager) + app := fiber.New() + app.Use(plugin.PathAuthMiddleware(config)) + + app.Listen(":8080") +} +``` + +### Chi + +```go +package main + +import ( + "net/http" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/chi" + "github.com/click33/sa-token-go/storage/memory" + "github.com/go-chi/chi/v5" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := chi.NewPlugin(manager) + r := chi.NewRouter() + r.Use(plugin.PathAuthMiddleware(config)) + + http.ListenAndServe(":8080", r) +} +``` + +### GoFrame + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gf" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gogf/gf/v2/frame/g" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := gf.NewPlugin(manager) + s := g.Server() + s.Use(plugin.PathAuthMiddleware(config)) + + s.Run() +} +``` + +### Kratos + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/kratos" + "github.com/click33/sa-token-go/storage/memory" + "github.com/go-kratos/kratos/v2" + "github.com/go-kratos/kratos/v2/transport/http" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := kratos.NewPlugin(manager) + + httpSrv := http.NewServer( + http.Middleware( + plugin.PathAuthMiddleware(config), + ), + ) + + app := kratos.New( + kratos.Server(httpSrv), + ) + + app.Run() +} +``` + +## Error Handling + +When path authentication fails, the middleware returns a standardized error: + +```go +// Error response format +{ + "code": 401, + "message": "path authentication required", + "error": "path authentication required: this path requires authentication", + "path": "/api/user/info" // Included in context +} +``` + +You can customize error handling: + +```go +// In your error handler +if err := core.GetErrorCode(err); err == core.CodePathAuthRequired { + // Handle path authentication error + path, _ := err.GetContext("path") + // Custom error response +} +``` + +## Best Practices + +1. **Order Matters**: Place path authentication middleware before other middleware that depends on authentication +2. **Specific First**: More specific patterns should be listed before general patterns +3. **Public Paths**: Always exclude authentication endpoints (login, register) from authentication +4. **Static Files**: Exclude static file paths (CSS, JS, images) for better performance +5. **Error Handling**: Provide clear error messages to help users understand authentication requirements + +## Common Scenarios + +### Scenario 1: API with Public and Private Endpoints + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{ + "/api/auth/**", // All auth endpoints + "/api/public/**", // Public API endpoints + }) +``` + +### Scenario 2: Admin Panel Protection + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/admin/**"}). + SetExclude([]string{ + "/admin/login", + "/admin/static/**", // Admin static files + }) +``` + +### Scenario 3: Multi-Tenant Application + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}). + SetValidator(func(loginID string) bool { + // Check tenant access + return checkTenantAccess(loginID) + }) +``` + +## API Reference + +### PathAuthConfig + +- `SetInclude(patterns []string) *PathAuthConfig` - Set paths that require authentication +- `SetExclude(patterns []string) *PathAuthConfig` - Set paths excluded from authentication +- `SetValidator(validator func(loginID string) bool) *PathAuthConfig` - Set custom login ID validator +- `Check(path string) bool` - Check if a path requires authentication + +### ProcessAuth + +```go +func ProcessAuth(path, tokenStr string, config *PathAuthConfig, mgr *Manager) *AuthResult +``` + +Processes authentication for a request path and returns an `AuthResult` containing: +- `NeedAuth bool` - Whether authentication is required +- `Token string` - The extracted token +- `TokenInfo *TokenInfo` - Token information if valid +- `IsValid bool` - Whether the token is valid + +### AuthResult + +- `ShouldReject() bool` - Check if the request should be rejected +- `LoginID() string` - Get the login ID from token info + +### Error Functions + +- `NewPathAuthRequiredError(path string) *SaTokenError` - Create path authentication required error +- `NewPathNotAllowedError(path string) *SaTokenError` - Create path not allowed error diff --git a/docs/guide/path-auth_zh.md b/docs/guide/path-auth_zh.md new file mode 100644 index 0000000..807d537 --- /dev/null +++ b/docs/guide/path-auth_zh.md @@ -0,0 +1,551 @@ +# 路径鉴权 + +路径鉴权允许您配置哪些路径需要鉴权,哪些路径被排除,为应用程序提供灵活的访问控制。 + +## 特性 + +- **Ant风格通配符模式** - 支持 `/**`、`/*`、`*.html` 等模式 +- **包含/排除配置** - 精细控制哪些路径需要鉴权 +- **自定义验证器** - 可选的登录ID验证函数 +- **框架集成** - 与所有支持的框架无缝协作 +- **Token提取** - 自动从请求头和Cookie中提取Token + +## 模式匹配 + +路径匹配支持Ant风格通配符: + +- `/**` - 匹配所有路径 +- `/api/**` - 匹配所有以 `/api/` 开头的路径 +- `/api/*` - 匹配 `/api/` 下的单级路径(例如 `/api/user`,但不匹配 `/api/user/profile`) +- `*.html` - 匹配以 `.html` 结尾的路径 +- `/exact` - 精确路径匹配 + +### 模式示例 + +```go +// 匹配所有路径 +"/**" + +// 匹配所有API路径 +"/api/**" + +// 匹配单级API路径 +"/api/*" + +// 匹配静态文件 +"*.html" +"*.css" +"*.js" + +// 匹配特定路径 +"/login" +"/logout" +"/public/**" +``` + +## 使用方法 + +### 基本配置 + +使用路径鉴权最简单的方式是通过中间件: + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gin" + "github.com/click33/sa-token-go/storage/memory" +) + +func main() { + // 初始化管理器 + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + TokenName("Authorization"). + Timeout(86400). + Build() + + // 创建路径鉴权配置 + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). // 需要鉴权的路径 + SetExclude([]string{"/api/public/**"}) // 排除鉴权的路径 + + // 创建插件并使用中间件 + plugin := gin.NewPlugin(manager) + r := gin.Default() + + // 应用路径鉴权中间件 + r.Use(plugin.PathAuthMiddleware(config)) + + // 您的路由 + r.GET("/api/user/info", getUserInfo) + r.GET("/api/public/status", getStatus) // 此路径被排除 + + r.Run(":8080") +} +``` + +### 多个包含/排除模式 + +您可以指定多个模式以实现更复杂的场景: + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{ + "/api/**", // 所有API路径 + "/admin/**", // 所有管理路径 + "/user/profile", // 特定用户资料路径 + }). + SetExclude([]string{ + "/api/public/**", // 公共API路径 + "/api/auth/login", // 登录端点 + "/api/auth/register", // 注册端点 + "*.html", // 静态HTML文件 + "*.css", // CSS文件 + "*.js", // JavaScript文件 + }) +``` + +### 使用自定义验证器 + +您可以添加自定义的登录ID验证逻辑: + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}). + SetValidator(func(loginID string) bool { + // 自定义验证逻辑 + // 例如,检查用户是否被封禁 + if loginID == "banned_user" { + return false + } + + // 检查用户账号是否激活 + // 您可以在这里查询数据库 + // return isUserActive(loginID) + + return true + }) +``` + +### Gin完整示例 + +```go +package main + +import ( + "net/http" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gin" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gin-gonic/gin" +) + +func main() { + // 初始化Sa-Token管理器 + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + TokenName("Authorization"). + Timeout(86400). + Build() + + // 配置路径鉴权 + pathAuthConfig := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{ + "/api/auth/login", + "/api/auth/register", + "/api/public/**", + }) + + // 创建Gin路由器 + r := gin.Default() + + // 创建插件 + plugin := gin.NewPlugin(manager) + + // 应用路径鉴权中间件 + r.Use(plugin.PathAuthMiddleware(pathAuthConfig)) + + // 公共路由(排除鉴权) + r.POST("/api/auth/login", plugin.LoginHandler) + r.POST("/api/auth/register", registerHandler) + r.GET("/api/public/status", getStatus) + + // 受保护的路由(需要鉴权) + api := r.Group("/api") + { + api.GET("/user/info", getUserInfo) + api.GET("/user/profile", getUserProfile) + api.POST("/user/update", updateUser) + } + + r.Run(":8080") +} + +func getUserInfo(c *gin.Context) { + // 从上下文获取登录ID(由PathAuthMiddleware设置) + loginID, exists := c.Get("loginID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "loginID": loginID, + "message": "用户信息已获取", + }) +} + +func getUserProfile(c *gin.Context) { + loginID, _ := c.Get("loginID") + c.JSON(http.StatusOK, gin.H{ + "loginID": loginID, + "profile": "用户资料数据", + }) +} + +func updateUser(c *gin.Context) { + loginID, _ := c.Get("loginID") + c.JSON(http.StatusOK, gin.H{ + "loginID": loginID, + "message": "用户已更新", + }) +} + +func registerHandler(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "注册成功"}) +} + +func getStatus(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} +``` + +### 直接使用 ProcessAuth + +如果您需要更多控制,可以在处理器中直接使用 `ProcessAuth`: + +```go +import "github.com/click33/sa-token-go/core" + +func customHandler(c *gin.Context) { + path := c.Request.URL.Path + token := c.GetHeader("Authorization") + if token == "" { + token, _ = c.Cookie("Authorization") + } + + config := core.NewPathAuthConfig().SetInclude([]string{"/api/**"}) + result := core.ProcessAuth(path, token, config, manager) + + if result.ShouldReject() { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "路径需要鉴权", + "path": path, + }) + c.Abort() + return + } + + // 使用 result.LoginID() 获取登录ID + loginID := result.LoginID() + if loginID == "" { + // Token有效但登录ID不可用 + // 您可能需要通过其他方式获取 + } + + // 继续您的逻辑 + c.JSON(http.StatusOK, gin.H{"loginID": loginID}) +} +``` + +## 框架示例 + +### Gin + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gin" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gin-gonic/gin" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := gin.NewPlugin(manager) + r := gin.Default() + r.Use(plugin.PathAuthMiddleware(config)) + + r.Run(":8080") +} +``` + +### Echo + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/echo" + "github.com/click33/sa-token-go/storage/memory" + "github.com/labstack/echo/v4" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := echo.NewPlugin(manager) + e := echo.New() + e.Use(plugin.PathAuthMiddleware(config)) + + e.Start(":8080") +} +``` + +### Fiber + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/fiber" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gofiber/fiber/v2" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := fiber.NewPlugin(manager) + app := fiber.New() + app.Use(plugin.PathAuthMiddleware(config)) + + app.Listen(":8080") +} +``` + +### Chi + +```go +package main + +import ( + "net/http" + + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/chi" + "github.com/click33/sa-token-go/storage/memory" + "github.com/go-chi/chi/v5" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := chi.NewPlugin(manager) + r := chi.NewRouter() + r.Use(plugin.PathAuthMiddleware(config)) + + http.ListenAndServe(":8080", r) +} +``` + +### GoFrame + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/gf" + "github.com/click33/sa-token-go/storage/memory" + "github.com/gogf/gf/v2/frame/g" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := gf.NewPlugin(manager) + s := g.Server() + s.Use(plugin.PathAuthMiddleware(config)) + + s.Run() +} +``` + +### Kratos + +```go +package main + +import ( + "github.com/click33/sa-token-go/core" + "github.com/click33/sa-token-go/integrations/kratos" + "github.com/click33/sa-token-go/storage/memory" + "github.com/go-kratos/kratos/v2" + "github.com/go-kratos/kratos/v2/transport/http" +) + +func main() { + manager := core.NewBuilder(). + Storage(memory.NewStorage()). + Build() + + config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}) + + plugin := kratos.NewPlugin(manager) + + httpSrv := http.NewServer( + http.Middleware( + plugin.PathAuthMiddleware(config), + ), + ) + + app := kratos.New( + kratos.Server(httpSrv), + ) + + app.Run() +} +``` + +## 错误处理 + +当路径鉴权失败时,中间件会返回标准化的错误: + +```go +// 错误响应格式 +{ + "code": 401, + "message": "path authentication required", + "error": "path authentication required: this path requires authentication", + "path": "/api/user/info" // 包含在上下文中 +} +``` + +您可以自定义错误处理: + +```go +// 在您的错误处理器中 +if err := core.GetErrorCode(err); err == core.CodePathAuthRequired { + // 处理路径鉴权错误 + path, _ := err.GetContext("path") + // 自定义错误响应 +} +``` + +## 最佳实践 + +1. **顺序很重要**:将路径鉴权中间件放在其他依赖认证的中间件之前 +2. **具体优先**:更具体的模式应该列在通用模式之前 +3. **公共路径**:始终将认证端点(登录、注册)排除在鉴权之外 +4. **静态文件**:排除静态文件路径(CSS、JS、图片)以提高性能 +5. **错误处理**:提供清晰的错误消息,帮助用户理解鉴权要求 + +## 常见场景 + +### 场景1:包含公共和私有端点的API + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{ + "/api/auth/**", // 所有认证端点 + "/api/public/**", // 公共API端点 + }) +``` + +### 场景2:管理面板保护 + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/admin/**"}). + SetExclude([]string{ + "/admin/login", + "/admin/static/**", // 管理后台静态文件 + }) +``` + +### 场景3:多租户应用 + +```go +config := core.NewPathAuthConfig(). + SetInclude([]string{"/api/**"}). + SetExclude([]string{"/api/public/**"}). + SetValidator(func(loginID string) bool { + // 检查租户访问权限 + return checkTenantAccess(loginID) + }) +``` + +## API 参考 + +### PathAuthConfig + +- `SetInclude(patterns []string) *PathAuthConfig` - 设置需要鉴权的路径 +- `SetExclude(patterns []string) *PathAuthConfig` - 设置排除鉴权的路径 +- `SetValidator(validator func(loginID string) bool) *PathAuthConfig` - 设置自定义登录ID验证器 +- `Check(path string) bool` - 检查路径是否需要鉴权 + +### ProcessAuth + +```go +func ProcessAuth(path, tokenStr string, config *PathAuthConfig, mgr *Manager) *AuthResult +``` + +处理请求路径的鉴权,返回包含以下信息的 `AuthResult`: +- `NeedAuth bool` - 是否需要鉴权 +- `Token string` - 提取的token +- `TokenInfo *TokenInfo` - 如果有效则包含token信息 +- `IsValid bool` - token是否有效 + +### AuthResult + +- `ShouldReject() bool` - 检查请求是否应该被拒绝 +- `LoginID() string` - 从token信息中获取登录ID + +### 错误函数 + +- `NewPathAuthRequiredError(path string) *SaTokenError` - 创建路径需要鉴权错误 +- `NewPathNotAllowedError(path string) *SaTokenError` - 创建路径不允许访问错误 diff --git a/integrations/chi/annotation.go b/integrations/chi/annotation.go index b8ec1d9..ffb0374 100644 --- a/integrations/chi/annotation.go +++ b/integrations/chi/annotation.go @@ -20,7 +20,6 @@ type Annotation struct { // GetHandler gets handler with annotations | 获取带注解的处理器 func GetHandler(handler http.Handler, annotations ...*Annotation) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if authentication should be ignored | 检查是否忽略认证 if len(annotations) > 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