You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# MVC应用集成测试中正确实现用户登出的方法

问题背景

我在为ASP.NET Core MVC应用编写集成测试,需要覆盖「用户登出后再次登录」的业务流程。目前单独的登录功能测试可以正常运行,测试时注入了预置匹配用户条目的内存数据库,可正常通过的登录测试代码如下:

[Fact]
public async Task D_LoginTest()
{
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = true
        });
    var initResponse = await client.GetAsync("/Identity/Account/Login");
    var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);

    var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Login");
    postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());
    var formModel = new Dictionary<string, string>
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
        { "Input.Email", "test@example.com" },
        { "Input.Password", "pas3w0!rRd" }
    };
    postRequest.Content = new FormUrlEncodedContent(formModel);
    var response = await client.SendAsync(postRequest);
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

在测试用例中追加登出逻辑后,测试执行失败返回400 Bad Request错误,修改后的测试代码如下:

[Fact]
public async Task D_LoginTest()
{
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = true
        });
    var initResponse = await client.GetAsync("/Identity/Account/Login");
    var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);

    var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Login");
    postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());
    var formModel = new Dictionary<string, string>
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
        { "Input.Email", "test@example.com" },
        { "Input.Password", "pas3w0!rRd" }
    };
    postRequest.Content = new FormUrlEncodedContent(formModel);
    var response = await client.SendAsync(postRequest);
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    var postRequestLogout = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Logout");
    postRequestLogout.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());
    var postRequestLougoutForm = new Dictionary<string, string>
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
    };
    postRequestLogout.Content = new FormUrlEncodedContent(postRequestLougoutForm);
    var logoutAnswer = await client.SendAsync(postRequestLogout);
    logoutAnswer.EnsureSuccessStatusCode();
    Console.WriteLine(logoutAnswer.StatusCode);
    Assert.Equal(HttpStatusCode.OK, logoutAnswer.StatusCode);
}

对应的错误堆栈信息:

Failed amaranth.Tests.AdminControllerTests.D_LoginTest [23 ms]
  Error Message:
   System.Net.Http.HttpRequestException : Response status code does not indicate success: 400 (Bad Request).
  Stack Trace:
     at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
   at amaranth.Tests.AdminControllerTests.D_LoginTest() in /path/to/project/dir/amaranth.Tests/IntegrationTests/AdminControllerTests.cs:line 308
--- End of stack trace from previous location ---

调试错误截图:
调试错误截图

项目中默认Razor Page脚手架生成的Logout.cshtml.cs代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace amaranth.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LogoutModel : PageModel
    {
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;

        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger)
        {
            _signInManager = signInManager;
            _logger = logger;
        }

        public void OnGet()
        {
        }

        public async Task<IActionResult> OnPost(string returnUrl = null)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("User logged out.");
            if (returnUrl != null)
            {
                return LocalRedirect(returnUrl);
            }
            else
            {
                return Page();
            }
        }
    }
}

登出逻辑为MVC默认脚手架生成的Razor Page实现,登出请求始终返回400错误,需要定位现有写法的问题,以及集成测试中正确实现用户登出操作的方案。


问题原因

400错误的核心原因是防伪令牌(Anti-Forgery Token)校验失败:

  • 你在登录前从Login页面提取了一次防伪令牌(包含Cookie值和表单字段值),登录成功后,服务端更新了用户身份上下文,对应的防伪令牌Cookie和表单值已经刷新
  • 发起登出POST请求时,仍然复用了登录前拿到的旧防伪令牌,ASP.NET Core防伪校验机制验证不通过,直接返回400状态码

另外手动给请求头追加Cookie的写法属于冗余逻辑:WebApplicationFactory创建的HttpClient默认自带Cookie容器,会自动维护请求/响应过程中的Cookie,不需要手动往请求头塞Cookie值,手动追加反而可能因为Cookie值过期、覆盖导致额外异常。


正确实现方案

登出前需要先访问登出页面,重新提取当前上下文下最新的防伪令牌,再发起POST登出请求,修正后的测试代码如下:

[Fact]
public async Task D_LoginTest()
{
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = true
        });
    
    // 执行登录流程
    var initLoginResponse = await client.GetAsync("/Identity/Account/Login");
    var loginAntiForgery = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initLoginResponse);

    var loginRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Login");
    var loginForm = new Dictionary<string, string>
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, loginAntiForgery.fieldValue },
        { "Input.Email", "test@example.com" },
        { "Input.Password", "pas3w0!rRd" }
    };
    loginRequest.Content = new FormUrlEncodedContent(loginForm);
    var loginResponse = await client.SendAsync(loginRequest);
    loginResponse.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);

    // 关键:访问登出页面,重新获取最新的防伪令牌,不要复用登录前的旧值
    var initLogoutResponse = await client.GetAsync("/Identity/Account/Logout");
    var logoutAntiForgery = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initLogoutResponse);

    // 发起登出POST请求
    var logoutRequest = new HttpRequestMessage(HttpMethod.Post, "/Identity/Account/Logout");
    var logoutForm = new Dictionary<string, string>
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, logoutAntiForgery.fieldValue },
    };
    logoutRequest.Content = new FormUrlEncodedContent(logoutForm);
    var logoutResponse = await client.SendAsync(logoutRequest);
    logoutResponse.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.OK, logoutResponse.StatusCode);
}

注意:如果后续需要测试登出后再次登录的流程,同样需要在第二次登录前重新访问Login页面,提取最新的防伪令牌,不能复用之前的旧令牌值。


内容的提问来源于stack exchange,提问作者ChristianO'Connor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 12:42:17