logo
Public
0
0
WeChat Login
fix(auth): 修复 OAuth2 单次消费与存储一致性

auth

The auth module provides authentication, authorization, session management, and token security for github.com/darkit/gin. It wraps the underlying sa-token-go-style core with a project-friendly API that works at three levels:

  • Engine-level: initialize auth once with gin.WithAuth(...)
  • Request-level: use c.Auth() inside handlers
  • Global / standalone: use SetGlobalManager(...), StpLogic, or a locally created Manager

Purpose and capabilities

auth is designed to solve the runtime concerns of identity state management, not user data modeling. Its core capabilities are:

  • issue and validate access tokens
  • map tokens to login identities and devices
  • maintain user sessions
  • manage permissions and roles
  • support forced logout and account disablement
  • provide refresh token, nonce, and OAuth2 helpers
  • support pluggable storage backends

What it does not do:

  • define your user table or password workflow
  • implement business-specific permission loading logic for you
  • expose HTTP login endpoints automatically
  • act as a full SSO / IAM platform

Features

  • Simple handler API via AuthContext
  • Centralized state machine via Manager
  • Global static API for lightweight or legacy integration
  • Multi-domain auth instances via StpLogic
  • Memory and Redis storage adapters
  • Refresh token support with access-token rotation
  • Nonce support for anti-replay flows
  • OAuth2 authorization server primitives
  • Permission and role checks with AND / OR semantics
  • Device-aware login sessions
  • Concurrent-login policies and login-count limits
  • Auto-renew support for active tokens

Integration patterns

1. Engine-level integration

Use gin.WithAuth(auth.AuthConfig{...}) when auth is part of the application runtime. This creates a Manager during engine initialization and makes request auth available through c.Auth().

package main import ( "time" ginx "github.com/darkit/gin" "github.com/darkit/gin/auth" ) func main() { e := ginx.New( ginx.WithAuth(auth.AuthConfig{ Secret: "replace-me", Expiry: 24 * time.Hour, TokenStyle: auth.TokenStyleJWT, }), ) e.POST("/login", func(c *ginx.Context) { token, err := c.Auth().Login("user-1001", "web") if err != nil { c.InternalError(err.Error()) return } c.Success(ginx.H{"token": token}) }) _ = e.Run(":8080") }

2. Request-level integration

Inside a handler, c.Auth() returns an *auth.AuthContext backed by the engine manager and current request. This is the most convenient mode for business handlers.

func handleProfile(c *gin.Context) { if err := c.Auth().CheckLogin(); err != nil { c.Unauthorized(err.Error()) return } loginID, err := c.Auth().LoginID() if err != nil { c.Unauthorized(err.Error()) return } if err := c.Auth().CheckAnyPermission("user:read", "profile:read"); err != nil { c.Forbidden(err.Error()) return } c.Success(gin.H{"login_id": loginID}) }

3. Global integration

Use the global API when you want process-wide auth access without holding a manager reference. You must initialize it first.

cfg := auth.DefaultAuthConfig() mgr := auth.NewManager(auth.NewMemoryStorage(), &cfg) auth.SetGlobalManager(mgr) defer auth.CloseGlobalManager() token, err := auth.Login("user-1001", "web") if err != nil { panic(err) } ok := auth.IsLogin(token) _ = ok

4. Local manager integration

If you want middleware or auth logic without using global state, create and hold your own manager.

cfg := auth.DefaultAuthConfig() storage := auth.NewMemoryStorage() mgr := auth.NewManager(storage, &cfg) builder := auth.NewMiddlewareBuilder(mgr) router.Use(builder.AuthRequired()) router.GET("/admin", builder.RoleRequired("admin"), adminHandler)

5. Multi-domain integration with StpLogic

Use StpLogic when one process hosts multiple auth domains that must not share manager state.

cfg := auth.DefaultAuthConfig() adminMgr := auth.NewManager(auth.NewMemoryStorage(), &cfg) userMgr := auth.NewManager(auth.NewMemoryStorage(), &cfg) adminLogic := auth.NewStpLogic(adminMgr) userLogic := auth.NewStpLogic(userMgr) adminToken, _ := adminLogic.Login("admin-1", "web") userToken, _ := userLogic.Login("user-1", "app") _, _ = adminToken, userToken

Core concepts

AuthContext

AuthContext is the request-scoped facade. It extracts the token from the incoming request, delegates to Manager, and offers a compact API for:

  • login / logout
  • login-state checks
  • permission and role checks
  • disable checks
  • session access
  • refresh-token exchange

Token extraction follows manager config in this order:

  1. configured header (TokenName)
  2. Authorization: Bearer <token>
  3. cookie (if enabled)
  4. query parameter
  5. form body (if enabled)

Manager

Manager is the module’s core runtime object. All auth state eventually flows through it, whether called from AuthContext, global helpers, middleware, or StpLogic.

It is responsible for:

  • generating tokens
  • storing token/account/session mappings
  • enforcing concurrent-login policy
  • checking login state
  • managing permissions and roles
  • handling disable / untie / kickout flows
  • driving nonce, refresh token, events, and OAuth2 helpers

Session

A Session is per-login-identity state stored in the configured backend. It carries runtime metadata such as:

  • loginId
  • device
  • loginTime
  • permissions
  • roles

Permissions and roles are persisted in session storage and can also be lazy-loaded through configured loaders.

Token and TokenInfo

A token is the login credential returned to the client. The stored TokenInfo record currently includes:

  • LoginID
  • Device
  • CreateTime
  • ActiveTime
  • Tag (field exists, but token tag APIs are not supported)

Manager can also mark token state logically as:

  • KICK_OUT
  • BE_REPLACED

Storage options

Memory storage

auth.NewMemoryStorage() is the default when AuthConfig.Storage is nil.

Good for:

  • local development
  • tests
  • single-process services

Characteristics:

  • in-process map storage
  • TTL support
  • periodic cleanup goroutine
  • state is lost on process restart
  • not suitable for multi-instance deployments

Redis storage

auth.NewRedisStorage(redisURL) provides distributed storage.

Good for:

  • production deployments
  • horizontally scaled services
  • shared auth state across instances

Characteristics:

  • persistent external state
  • TTL delegated to Redis
  • scan-based key matching
  • supports SetKeepTTL
  • Clear() removes all keys reachable by the storage client and should be used with caution

Example:

storage, err := auth.NewRedisStorage("redis://localhost:6379/0") if err != nil { panic(err) } cfg := auth.DefaultAuthConfig() cfg.Storage = storage mgr := auth.NewManager(storage, &cfg) _ = mgr

Security features

Refresh token support

Manager.LoginWithRefreshToken(loginID, device) returns a token pair:

  • short-lived access token
  • long-lived refresh token

Manager.RefreshAccessToken(refreshToken) issues a new access token while preserving the login identity and device.

Important implementation details from the code:

  • refresh token metadata is JSON-serialized for storage compatibility
  • refresh token default TTL is 30 days
  • access token default fallback TTL is 2 hours if config timeout is zero
  • refreshed access tokens copy original token storage payload to preserve TokenInfo semantics

Nonce support

Nonce support is implemented through NonceManager and exposed via Manager.GenerateNonce() / VerifyNonce().

Properties:

  • cryptographically random 64-char hex value
  • one-time use
  • default TTL: 5 minutes
  • verification consumes the nonce

This is useful for anti-replay scenarios around sensitive requests.

OAuth2 support

Manager.GetOAuth2Server() returns a storage-backed OAuth2 authorization server helper. It supports:

  • client registration
  • authorization code generation
  • code exchange for token
  • access token validation
  • refresh-token exchange with rotation
  • token revocation

The implementation stores client registrations, authorization codes, access tokens, and refresh tokens in the same configured storage backend.

Quick start examples

Minimal login and access check

cfg := auth.DefaultAuthConfig() mgr := auth.NewManager(auth.NewMemoryStorage(), &cfg) token, err := mgr.Login("user-1001", "web") if err != nil { panic(err) } if !mgr.IsLogin(token) { panic("expected token to be valid") } loginID, err := mgr.GetLoginID(token) if err != nil { panic(err) } _ = loginID

Permissions and roles

_ = mgr.SetPermissions("user-1001", []string{"user:read", "user:*"}) _ = mgr.SetRoles("user-1001", []string{"admin"}) canRead := mgr.HasPermission("user-1001", "user:read") canWrite := mgr.HasPermission("user-1001", "user:write") isAdmin := mgr.HasRole("user-1001", "admin") _, _, _ = canRead, canWrite, isAdmin

Session access

sess, err := mgr.GetSession("user-1001") if err != nil { panic(err) } _ = sess

Refresh-token flow

pair, err := mgr.LoginWithRefreshToken("user-1001", "web") if err != nil { panic(err) } next, err := mgr.RefreshAccessToken(pair.RefreshToken) if err != nil { panic(err) } _ = next.AccessToken

Middleware protection

builder := auth.NewMiddlewareBuilder(mgr) r.Use(builder.AuthRequired()) r.GET("/reports", builder.PermRequired("report:read"), reportHandler) r.GET("/ops", builder.RoleRequired("admin", "ops"), opsHandler)

Design notes that matter in practice

  • AuthConfig.Validate() requires a non-empty Secret when TokenStyleJWT is selected.
  • AuthConfig.Storage is optional; memory storage is used by default.
  • NewManager(...) registers permission and role loaders if configured.
  • token tags are intentionally unsupported; use Session for custom metadata.
  • permission matching supports *, user:*, and segmented wildcard patterns such as user:*:view.
  • Kickout marks the token as kicked out while removing the account mapping.
  • Logout removes the token chain directly.

Related documents