package middleware import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" ) func newCSRFTestRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() store := cookie.NewStore([]byte("test-secret")) r.Use(sessions.Sessions("test_session", store)) r.Use(CSRFProtect()) r.GET("/form", func(c *gin.Context) { c.String(http.StatusOK, "TOKEN="+c.GetString(CSRFContextKey)) }) r.HEAD("/form", func(c *gin.Context) { c.String(http.StatusOK, "TOKEN="+c.GetString(CSRFContextKey)) }) r.POST("/action", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) return r } // tokenFromForm 使用给定的会话 Cookie 执行 GET /form,并返回 // 签发的 CSRF 令牌及(可能更新的)会话 Cookie。 func tokenFromForm(t *testing.T, r *gin.Engine, sessionCookie string) (token, cookie string) { t.Helper() req := httptest.NewRequest(http.MethodGet, "/form", nil) if sessionCookie != "" { req.Header.Set("Cookie", sessionCookie) } w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("GET /form: status = %d, want 200", w.Code) } body := w.Body.String() const prefix = "TOKEN=" if !strings.HasPrefix(body, prefix) { t.Fatalf("GET /form: unexpected body %q", body) } token = strings.TrimPrefix(body, prefix) cookie = w.Header().Get("Set-Cookie") return token, cookie } func postAction(r *gin.Engine, sessionCookie, token string, useHeader bool) *httptest.ResponseRecorder { form := url.Values{} if !useHeader { form.Set(CSRFFieldName, token) } req := httptest.NewRequest(http.MethodPost, "/action", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if sessionCookie != "" { req.Header.Set("Cookie", sessionCookie) } if useHeader { req.Header.Set(CSRFHeaderName, token) } w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func TestCSRFTokenIssuedOnGET(t *testing.T) { r := newCSRFTestRouter() token, cookie := tokenFromForm(t, r, "") if token == "" { t.Fatal("expected a token to be issued on GET") } if !strings.Contains(cookie, "test_session=") { t.Fatalf("expected session cookie to be set, got %q", cookie) } // 相同会话的第二次 GET 必须返回相同的令牌。 token2, _ := tokenFromForm(t, r, cookie) if token2 != token { t.Fatalf("token changed between requests: %q vs %q", token, token2) } } func TestCSRFPostRejectedWithoutToken(t *testing.T) { r := newCSRFTestRouter() _, cookie := tokenFromForm(t, r, "") w := postAction(r, cookie, "", false) if w.Code != http.StatusForbidden { t.Fatalf("POST without token: status = %d, want 403", w.Code) } } func TestCSRFPostRejectedWithWrongToken(t *testing.T) { r := newCSRFTestRouter() _, cookie := tokenFromForm(t, r, "") w := postAction(r, cookie, "bogus-token", false) if w.Code != http.StatusForbidden { t.Fatalf("POST with wrong token: status = %d, want 403", w.Code) } } func TestCSRFPostRejectedWithoutSession(t *testing.T) { r := newCSRFTestRouter() // 没有先前的 GET:无会话,也未签发令牌。 w := postAction(r, "", "some-token", false) if w.Code != http.StatusForbidden { t.Fatalf("POST without session: status = %d, want 403", w.Code) } } func TestCSRFPostAcceptedWithFormField(t *testing.T) { r := newCSRFTestRouter() token, cookie := tokenFromForm(t, r, "") w := postAction(r, cookie, token, false) if w.Code != http.StatusOK { t.Fatalf("POST with valid token: status = %d, want 200 (body: %s)", w.Code, w.Body.String()) } } func TestCSRFPostAcceptedWithHeader(t *testing.T) { r := newCSRFTestRouter() token, cookie := tokenFromForm(t, r, "") w := postAction(r, cookie, token, true) if w.Code != http.StatusOK { t.Fatalf("POST with token in header: status = %d, want 200 (body: %s)", w.Code, w.Body.String()) } } func TestCSRFSafeMethodsPassWithoutToken(t *testing.T) { r := newCSRFTestRouter() // GET 和 HEAD 已注册路由;OPTIONS 未注册(gin 不会自动注册), // 因此会落入 noRoute——但在所有情况下,CSRF 中间件本身都不得以 403 拒绝。 for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions} { req := httptest.NewRequest(method, "/form", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code == http.StatusForbidden { t.Fatalf("%s /form: status = 403, CSRF middleware must not reject safe methods", method) } } }