Behind a reverse proxy (e.g. nginx with X-Real-IP), admin audit logs and session IP/UA binding always recorded 127.0.0.1 because they hardcoded the gin trusted_proxies chain, while API key IP restriction already honored the "trust forwarded client IP" system setting. - add ip.GetSecurityClientIP(c, trustForwarded) as the single source of truth for security-sensitive client IP selection; API key auth middlewares (main + google) refactored onto it with zero behavior change - SessionBindingContext(cfg) now resolves the client IP via the same toggle and injects it into the request context; token issuance, binding enforcement and its mismatch audit record all read the injected value, so issue/verify can never diverge - audit log middleware and audit-log clear trace record the same security client IP (middleware.SecurityClientIP), falling back to the trusted proxy chain when the injection is absent - settings UI hint (zh/en) documents the broadened toggle scope and the one-time re-login after toggling while session binding is enabled With the toggle off (default) behavior is byte-for-byte unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
//go:build unit
|
|
|
|
package middleware
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// 反代场景:RemoteAddr 为 127.0.0.1,真实客户端 IP 在 X-Real-IP 中。
|
|
// 会话绑定注入与审计 IP 必须与 API Key IP 限制共用「信任反代传递的客户端 IP」开关语义。
|
|
func TestSessionBindingContextHonorsTrustForwardedToggle(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
trustForwarded bool
|
|
wantIP string
|
|
}{
|
|
{name: "trust disabled records proxy address", trustForwarded: false, wantIP: "127.0.0.1"},
|
|
{name: "trust enabled records forwarded client IP", trustForwarded: true, wantIP: "1.2.3.4"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.SetTrustForwardedIPForAPIKeyACL(tc.trustForwarded)
|
|
|
|
r := gin.New()
|
|
require.NoError(t, r.SetTrustedProxies(nil))
|
|
r.Use(SessionBindingContext(cfg))
|
|
r.GET("/t", func(c *gin.Context) {
|
|
binding := service.SessionBindingFromContext(c.Request.Context())
|
|
require.NotNil(t, binding)
|
|
require.Equal(t, tc.wantIP, binding.IP)
|
|
require.Equal(t, "test-agent", binding.UserAgent)
|
|
require.Equal(t, tc.wantIP, SecurityClientIP(c))
|
|
c.Status(200)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/t", nil)
|
|
req.RemoteAddr = "127.0.0.1:54321"
|
|
req.Header.Set("X-Real-IP", "1.2.3.4")
|
|
req.Header.Set("User-Agent", "test-agent")
|
|
r.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, 200, w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 未经过 SessionBindingContext 注入时(异常挂载顺序/单测直调),回退 trusted_proxies 链,
|
|
// 等价于开关关闭时的历史行为。
|
|
func TestSecurityClientIPFallsBackWithoutInjectedBinding(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
r := gin.New()
|
|
require.NoError(t, r.SetTrustedProxies(nil))
|
|
r.GET("/t", func(c *gin.Context) {
|
|
c.String(200, SecurityClientIP(c))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/t", nil)
|
|
req.RemoteAddr = "9.9.9.9:12345"
|
|
req.Header.Set("X-Real-IP", "1.2.3.4")
|
|
r.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, 200, w.Code)
|
|
require.Equal(t, "9.9.9.9", w.Body.String())
|
|
}
|
|
|
|
// requestSessionBinding 优先取注入值:开关开启时校验哈希必须基于注入的转发 IP 计算,
|
|
// 与 token 签发路径取值一致,否则同一客户端会被误判为指纹变化。
|
|
func TestRequestSessionBindingPrefersInjectedBinding(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
cfg := &config.Config{}
|
|
cfg.SetTrustForwardedIPForAPIKeyACL(true)
|
|
|
|
r := gin.New()
|
|
require.NoError(t, r.SetTrustedProxies(nil))
|
|
r.Use(SessionBindingContext(cfg))
|
|
r.GET("/t", func(c *gin.Context) {
|
|
issued := &service.SessionBinding{IP: "1.2.3.4", UserAgent: "test-agent"}
|
|
require.Equal(t, issued.Hash(), requestSessionBinding(c).Hash())
|
|
c.Status(200)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/t", nil)
|
|
req.RemoteAddr = "127.0.0.1:54321"
|
|
req.Header.Set("X-Real-IP", "1.2.3.4")
|
|
req.Header.Set("User-Agent", "test-agent")
|
|
r.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, 200, w.Code)
|
|
}
|