feat(router): add whitelist mechanism for http driver and auth plugin

- implement route whitelist registration and wildcard matching in RouterExtension
- add cookie store session fallback when Redis is disabled in driver_http
- actively register public auth endpoints to whitelist in auth plugin
- update user handlers to persist session and clear cookie on logout
- document router whitelist mechanism in AGENTS.md and new-api skill
This commit is contained in:
ryan
2026-08-29 11:39:13 +08:00
parent 53ae3007d0
commit e0f2309520
17 changed files with 411 additions and 32 deletions
+31
View File
@@ -5,6 +5,7 @@ package auth
import (
"Wavelet/core/contracts"
"Wavelet/core/extpoints"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/response"
"Wavelet/pkg/trace"
@@ -12,10 +13,35 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"sync"
"github.com/gin-gonic/gin"
)
var (
whitelistMu sync.RWMutex
whitelist []string
)
// RegisterWhitelist registers route patterns that bypass mandatory authentication.
func RegisterWhitelist(patterns ...string) {
whitelistMu.Lock()
defer whitelistMu.Unlock()
whitelist = append(whitelist, patterns...)
}
// IsWhitelisted checks if the specified path matches the auth whitelist.
func IsWhitelisted(path string) bool {
whitelistMu.RLock()
defer whitelistMu.RUnlock()
for _, pattern := range whitelist {
if extpoints.MatchPathPattern(pattern, path) {
return true
}
}
return false
}
func hashToken(token string) string {
h := sha256.New()
h.Write([]byte(token))
@@ -112,6 +138,11 @@ func GetUserFromRequest(c *gin.Context) (*contracts.UserDTO, error) {
// LoginRequired 返回登录鉴权中间件,校验 Access Token 或 Session
func LoginRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if IsWhitelisted(c.Request.URL.Path) {
c.Next()
return
}
_, span := trace.Start(c.Request.Context(), "LoginRequired")
defer span.End()
+18
View File
@@ -122,6 +122,24 @@ func (p *Plugin) Apply(ctx *core.Context) error {
core.Provide[contracts.AuthService](ctx, p.authSvc)
core.Provide[contracts.AuthRegistry](ctx, p.authRegistry)
// 2.1 Register Public / Auth Whitelist Endpoints
publicEndpoints := []string{
"/api/v1/oauth/sources",
"/api/v1/oauth/login",
"/api/v1/oauth/*/authorize",
"/api/v1/oauth/:source/authorize",
"/api/v1/oauth/callback",
"/api/v1/user/login",
"/api/v1/user/register",
"/api/v1/user/send-email-code",
"/api/v1/cap/challenge",
"/api/v1/cap/redeem",
"/healthz",
"/metrics",
}
RegisterWhitelist(publicEndpoints...)
ctx.Router().RegisterWhitelist(publicEndpoints...)
// 3. Register HTTP Routes
oauthGroup := ctx.Router().Group("/api/v1/oauth")
{
@@ -365,3 +365,37 @@ func TestLoginStateContextParityWithLegacyImplementation(t *testing.T) {
}
})
}
func TestAuthWhitelistMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx := core.NewContext(context.Background())
p := auth.New()
require.NoError(t, p.Apply(ctx))
svc, err := core.Inject[contracts.AuthService](ctx)
require.NoError(t, err)
mw, ok := svc.RequireAuthMiddleware().(gin.HandlerFunc)
require.True(t, ok)
engine := newSessionEngine()
engine.Use(mw)
engine.POST("/api/v1/user/login", func(c *gin.Context) {
c.JSON(http.StatusOK, response.OK("login-ok"))
})
engine.GET("/api/v1/secret-profile", func(c *gin.Context) {
c.JSON(http.StatusOK, response.OK("profile-ok"))
})
// 1. Whitelisted route /api/v1/user/login passes through without auth
w1 := httptest.NewRecorder()
req1, _ := http.NewRequest(http.MethodPost, "/api/v1/user/login", nil)
engine.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
// 2. Non-whitelisted route /api/v1/secret-profile gets 401 Unauthorized
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/api/v1/secret-profile", nil)
engine.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusUnauthorized, w2.Code)
}