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
相关产品推荐
相关产品推荐

