refreshTokenRouteWiring_audit_test.go 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. package handler
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "os"
  7. "regexp"
  8. "strings"
  9. "testing"
  10. "perms-system-server/internal/handler/pub"
  11. "perms-system-server/internal/middleware"
  12. "perms-system-server/internal/response"
  13. "perms-system-server/internal/svc"
  14. "perms-system-server/internal/testutil"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. "github.com/zeromicro/go-zero/core/stores/redis"
  18. )
  19. func init() { response.Setup() }
  20. // ---------------------------------------------------------------------------
  21. // 覆盖目标:
  22. // M-B (audit):HTTP /api/auth/refreshToken 路由必须挂载 RefreshTokenRateLimit
  23. // 中间件(IP 维度),配额用尽后对同 IP 请求必须返回 429 "请求过于频繁"。
  24. //
  25. // 本文件从两个独立角度交叉验证:
  26. // 1. 静态 wiring:读取 routes.go 源码,断言 /auth/refreshToken 路由块内出现
  27. // serverCtx.RefreshTokenRateLimit(防止有人误删中间件却骗过运行时);
  28. // 2. 行为验证:用同一条中间件组合链路(= 生产代码的 rest.WithMiddlewares 展开)
  29. // 直接发 HTTP 请求,达到 quota 后必须 429。
  30. // ---------------------------------------------------------------------------
  31. // TC-0832: 静态 wiring 检查 —— routes.go 中 /auth/refreshToken 必须显式绑定
  32. // RefreshTokenRateLimit 中间件。任何人无意中删掉这一行,本用例即红。
  33. func TestRoutes_RefreshTokenRateLimitWired(t *testing.T) {
  34. // 读 routes.go 源码
  35. raw, err := os.ReadFile("./routes.go")
  36. require.NoError(t, err, "必须能读到 internal/handler/routes.go")
  37. src := string(raw)
  38. // 先定位 /auth/refreshToken 的路由块,再在块内检查中间件引用
  39. // 语义等价于:rest.WithMiddlewares([]rest.Middleware{serverCtx.RefreshTokenRateLimit}, ... "/auth/refreshToken" ...)
  40. re := regexp.MustCompile(`(?s)rest\.WithMiddlewares\(\s*\[\]rest\.Middleware\{([^}]*)\}[^)]*?"/auth/refreshToken"`)
  41. m := re.FindStringSubmatch(src)
  42. require.NotEmpty(t, m, "routes.go 里 /auth/refreshToken 必须位于 rest.WithMiddlewares(...) 包裹块中;未匹配说明中间件被剥离")
  43. assert.Contains(t, m[1], "serverCtx.RefreshTokenRateLimit",
  44. "M-B:/auth/refreshToken 路由的中间件列表必须包含 RefreshTokenRateLimit")
  45. }
  46. // TC-0833: 行为验证 —— 复用生产中间件定义,quota=1 的窗口内同 IP 第 2 次必须 429。
  47. func TestRefreshTokenRoute_RateLimit_EnforcedOnSameIP(t *testing.T) {
  48. cfg := testutil.GetTestConfig()
  49. svcCtx := svc.NewServiceContext(cfg)
  50. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  51. // 构造与 routes.go 等价的中间件链:RefreshTokenRateLimit → RefreshTokenHandler
  52. // 这里故意使用 quota=1 的新实例,避免污染生产 limiter,同时保持行为完全一致。
  53. rl := middleware.NewRateLimitMiddleware(
  54. rds, 60, 1,
  55. cfg.CacheRedis.KeyPrefix+":rl:refresh:wiring:"+testutil.UniqueId(),
  56. false, /* behindProxy: 与默认配置一致,本测试用 RemoteAddr */
  57. )
  58. inner := pub.RefreshTokenHandler(svcCtx)
  59. wrapped := rl.Handle(func(w http.ResponseWriter, r *http.Request) {
  60. inner.ServeHTTP(w, r)
  61. })
  62. // 固定 RemoteAddr,两次请求同 IP 不同端口,必须共享同一限流桶。
  63. doRequest := func(remoteAddr string) (*httptest.ResponseRecorder, response.Body) {
  64. req := httptest.NewRequest(http.MethodPost, "/api/auth/refreshToken", strings.NewReader("{}"))
  65. req.Header.Set("Content-Type", "application/json")
  66. req.RemoteAddr = remoteAddr
  67. rr := httptest.NewRecorder()
  68. wrapped(rr, req)
  69. var body response.Body
  70. _ = json.Unmarshal(rr.Body.Bytes(), &body)
  71. return rr, body
  72. }
  73. // 第 1 次:放行,进入 RefreshTokenHandler 后因缺 Authorization 返回 401(业务层)
  74. _, body1 := doRequest("198.51.100.7:40001")
  75. assert.NotEqual(t, 429, body1.Code, "首次请求必须放行,由业务层决定返回码;实际 code=%d msg=%q", body1.Code, body1.Msg)
  76. // 第 2 次:同 IP 不同端口,必须被限流拦截,返回 429 "请求过于频繁..."
  77. _, body2 := doRequest("198.51.100.7:40002")
  78. assert.Equal(t, 429, body2.Code,
  79. "M-B:/api/auth/refreshToken 必须受 IP 维度限流保护;quota=1 时第 2 次必须 429。实际 code=%d msg=%q", body2.Code, body2.Msg)
  80. assert.Contains(t, body2.Msg, "过于频繁",
  81. "429 的业务文案必须是用户可读的限流提示,而不是原始 limiter 错误")
  82. // 不同 IP 必须不受影响,证明限流是 per-IP 而不是全局。
  83. _, body3 := doRequest("203.0.113.9:55555")
  84. assert.NotEqual(t, 429, body3.Code, "不同 IP 必须独立计数;不应被前一 IP 的 burst 牵连,实际 code=%d", body3.Code)
  85. }