mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
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:
@@ -18,6 +18,7 @@ type httpAppConfig struct {
|
||||
}
|
||||
|
||||
type httpRedisConfig struct {
|
||||
Enabled bool `config:"enabled" env:"REDIS_ENABLED" default:"false"`
|
||||
Addrs []string `config:"addrs" env:"REDIS_ADDR"`
|
||||
Username string `config:"username" env:"REDIS_USERNAME"`
|
||||
Password string `config:"password" env:"REDIS_PASSWORD" secret:"true"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-contrib/sessions/redis"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
|
||||
@@ -33,36 +34,12 @@ func BuildEngineWithConfig(appCfg httpAppConfig, redisCfg httpRedisConfig) (*gin
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(corsMiddleware())
|
||||
|
||||
addrs := redisCfg.Addrs
|
||||
sessionAddr := "localhost:6379"
|
||||
if len(addrs) > 0 {
|
||||
sessionAddr = addrs[0]
|
||||
}
|
||||
|
||||
sessionSecret := appCfg.SessionSecret
|
||||
if sessionSecret == "" {
|
||||
sessionSecret = "wavelet-default-session-secret"
|
||||
}
|
||||
|
||||
sessionStore, err := redis.NewStoreWithDB(
|
||||
redisCfg.MinIdleConn,
|
||||
"tcp",
|
||||
sessionAddr,
|
||||
redisCfg.Username,
|
||||
redisCfg.Password,
|
||||
strconv.Itoa(redisCfg.DB),
|
||||
[]byte(sessionSecret),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置 Session Redis Key 前缀
|
||||
if redisCfg.KeyPrefix != "" {
|
||||
if err := redis.SetKeyPrefix(sessionStore, redisCfg.KeyPrefix+"session:"); err != nil {
|
||||
log.Printf("[API] set session key prefix failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
sessionStore := initSessionStore(sessionSecret, redisCfg)
|
||||
|
||||
sessionCookieName := appCfg.SessionCookieName
|
||||
if sessionCookieName == "" {
|
||||
@@ -95,3 +72,32 @@ func BuildEngineWithConfig(appCfg httpAppConfig, redisCfg httpRedisConfig) (*gin
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func initSessionStore(sessionSecret string, redisCfg httpRedisConfig) sessions.Store {
|
||||
if !redisCfg.Enabled || len(redisCfg.Addrs) == 0 {
|
||||
return cookie.NewStore([]byte(sessionSecret))
|
||||
}
|
||||
|
||||
sessionAddr := redisCfg.Addrs[0]
|
||||
store, err := redis.NewStoreWithDB(
|
||||
redisCfg.MinIdleConn,
|
||||
"tcp",
|
||||
sessionAddr,
|
||||
redisCfg.Username,
|
||||
redisCfg.Password,
|
||||
strconv.Itoa(redisCfg.DB),
|
||||
[]byte(sessionSecret),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[driver_http] init redis session store failed, fallback to cookie store: %v\n", err)
|
||||
return cookie.NewStore([]byte(sessionSecret))
|
||||
}
|
||||
|
||||
if redisCfg.KeyPrefix != "" {
|
||||
if err := redis.SetKeyPrefix(store, redisCfg.KeyPrefix+"session:"); err != nil {
|
||||
log.Printf("[API] set session key prefix failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package driver_http
|
||||
|
||||
import (
|
||||
"Wavelet/core/extpoints"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/response"
|
||||
"context"
|
||||
@@ -24,8 +25,31 @@ import (
|
||||
var (
|
||||
apiPrefixMu sync.RWMutex
|
||||
apiPrefix = "/api/v1"
|
||||
|
||||
whitelistMu sync.RWMutex
|
||||
whitelistPatterns []string
|
||||
)
|
||||
|
||||
// SetWhitelist configures global whitelist patterns for HTTP routes.
|
||||
func SetWhitelist(patterns []string) {
|
||||
whitelistMu.Lock()
|
||||
defer whitelistMu.Unlock()
|
||||
whitelistPatterns = make([]string, len(patterns))
|
||||
copy(whitelistPatterns, patterns)
|
||||
}
|
||||
|
||||
// IsPathWhitelisted checks if the given path matches any registered whitelist pattern.
|
||||
func IsPathWhitelisted(path string) bool {
|
||||
whitelistMu.RLock()
|
||||
defer whitelistMu.RUnlock()
|
||||
for _, pattern := range whitelistPatterns {
|
||||
if extpoints.MatchPathPattern(pattern, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setAPIPrefix(prefix string) {
|
||||
if prefix == "" {
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -205,3 +206,40 @@ func TestCORSAllowedOriginReadsConfigOncePerCacheWindow(t *testing.T) {
|
||||
t.Errorf("expected 1 config load across 3 requests, got %d", cache.loads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEngineWithCookieFallback(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
appCfg := httpAppConfig{
|
||||
SessionSecret: "test-secret",
|
||||
SessionCookieName: "wavelet_session",
|
||||
SessionAge: 3600,
|
||||
}
|
||||
redisCfg := httpRedisConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
engine, err := BuildEngineWithConfig(appCfg, redisCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error with cookie fallback, got: %v", err)
|
||||
}
|
||||
|
||||
engine.GET("/set-session", func(c *gin.Context) {
|
||||
sess := sessions.Default(c)
|
||||
sess.Set("test_user_id", uint64(12345))
|
||||
_ = sess.Save()
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/set-session", nil)
|
||||
w := httptest.NewRecorder()
|
||||
engine.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
setCookie := w.Header().Get("Set-Cookie")
|
||||
if setCookie == "" {
|
||||
t.Fatal("expected Set-Cookie header in response, got none")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,12 +177,13 @@ func (p *Plugin) Start(ctx context.Context) error {
|
||||
var err error
|
||||
p.engine, err = BuildEngineWithConfig(appCfg, redisCfg)
|
||||
if err != nil {
|
||||
p.engine = gin.New()
|
||||
p.engine, _ = BuildEngine()
|
||||
}
|
||||
}
|
||||
|
||||
// Mount routes collected in Context RouterExtension
|
||||
if p.coreCtx != nil && p.coreCtx.Router() != nil {
|
||||
SetWhitelist(p.coreCtx.Router().Whitelist())
|
||||
for _, rd := range p.coreCtx.Router().Routes() {
|
||||
allHandlers := make([]gin.HandlerFunc, 0, len(rd.Middlewares)+len(rd.Handlers))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user