| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- package middleware
- import (
- "strings"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/ip"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // SessionBindingContext 全局中间件:将请求的客户端 IP 与 User-Agent 注入
- // request context,供 token 签发路径(登录 / 刷新 / OAuth 回调)读取并写入会话绑定,
- // 同时作为审计日志、会话绑定校验的统一客户端 IP 来源。
- // IP 取值与 API Key IP 限制共用转发 IP 开关:开启时旧版原始转发头逻辑
- // 接管解析,关闭时使用 Gin 的 server.trusted_proxies 可信代理链。
- func SessionBindingContext(cfg *config.Config) gin.HandlerFunc {
- return func(c *gin.Context) {
- forwardedIPSettings := cfg.ForwardedClientIPSettings()
- ip.SetForwardedIPSettings(c, forwardedIPSettings.TrustForwardedIP, forwardedIPSettings.Headers)
- userAgent := normalizePersistentText(c.Request.UserAgent(), maxPersistentUserAgentBytes)
- c.Request.Header.Set("User-Agent", userAgent)
- binding := &service.SessionBinding{
- IP: ip.GetSecurityClientIP(c, forwardedIPSettings.TrustForwardedIP),
- UserAgent: userAgent,
- }
- c.Request = c.Request.WithContext(service.WithSessionBinding(c.Request.Context(), binding))
- c.Next()
- }
- }
- // requestSessionBinding 返回当前请求的会话指纹,优先取 SessionBindingContext
- // 注入的解析结果(保证与 token 签发路径取值一致);注入缺失时使用安全回退。
- func requestSessionBinding(c *gin.Context) *service.SessionBinding {
- if binding := service.SessionBindingFromContext(c.Request.Context()); binding != nil {
- return binding
- }
- return &service.SessionBinding{
- IP: ip.GetTrustedClientIP(c),
- UserAgent: normalizePersistentText(c.Request.UserAgent(), maxPersistentUserAgentBytes),
- }
- }
- // SecurityClientIP 返回当前请求用于安全敏感记录(审计日志等)的客户端 IP。
- // 与会话绑定、API Key IP 限制共用同一套客户端 IP 来源。
- func SecurityClientIP(c *gin.Context) string {
- if binding := service.SessionBindingFromContext(c.Request.Context()); binding != nil &&
- strings.TrimSpace(binding.IP) != "" {
- return binding.IP
- }
- return ip.GetTrustedClientIP(c)
- }
- // enforceSessionBinding 校验 access token 的会话指纹(IP/UA 绑定)。
- // 指纹不匹配时:撤销该会话家族的所有 refresh token、写入审计安全事件、返回 401。
- // 返回 false 表示请求已被中断。
- //
- // 兼容性:claims.BindingHash 为空(功能上线前签发的旧 token)时放行,
- // 该会话在下一次 refresh 轮转时会自动获得绑定。
- func enforceSessionBinding(
- c *gin.Context,
- authService *service.AuthService,
- settingService *service.SettingService,
- auditService *service.AuditLogService,
- claims *service.JWTClaims,
- ) bool {
- if settingService == nil || !settingService.IsSessionBindingEnabled(c.Request.Context()) {
- return true
- }
- if claims == nil || claims.BindingHash == "" {
- return true
- }
- binding := requestSessionBinding(c)
- current := binding.Hash()
- if current == "" || current == claims.BindingHash {
- return true
- }
- if authService != nil {
- _ = authService.RevokeSessionFamily(c.Request.Context(), claims.SessionID)
- }
- if auditService != nil {
- uid := claims.UserID
- path := c.FullPath()
- if path == "" {
- path = c.Request.URL.Path
- }
- auditService.Record(&service.AuditLog{
- ActorUserID: &uid,
- ActorEmail: claims.Email,
- ActorRole: claims.Role,
- AuthMethod: service.AuditAuthMethodJWT,
- Action: service.AuditActionSessionBindingMismatch,
- Method: c.Request.Method,
- Path: path,
- ClientIP: binding.IP,
- UserAgent: normalizePersistentText(c.Request.UserAgent(), maxPersistentUserAgentBytes),
- StatusCode: 401,
- })
- }
- AbortWithError(c, 401, "SESSION_BINDING_MISMATCH", "Session network fingerprint changed, please login again")
- return false
- }
|