我有一个 ASP.NET Core API 控制器端点,需要:
我想对此端点执行集成测试。
我无法发送同时具有经过身份验证的用户和必要的防伪令牌/cookie 和身份验证 cookie 的请求。因此,端点在到达处理程序之前会继续返回
Bad Request
。
如何在需要身份验证和防伪令牌验证的端点上执行集成测试?
为了帮助解决这个问题,我创建了一个示例应用程序来演示我遇到的问题。
示例应用程序有一个 API 控制器,带有四个 POST 端点:
1。未经身份验证(匿名)端点 - 不需要防伪验证
[AllowAnonymous]
[HttpPost("Anonymous/{name}")]
public IActionResult AnonymousPost(string name)
{
return Ok(name);
}
2。经过身份验证的端点 - 不需要防伪验证
[HttpPost("Authenticated/{name}")]
public IActionResult AuthenticatedPost(string name)
{
return Ok(name);
}
3.未经身份验证(匿名)端点 - 需要防伪验证
[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost("Anonymous/Antiforgery/{name}")]
public IActionResult AnonymousAntiforgeryPost(string name)
{
return Ok(name);
}
4。经过身份验证的端点 - 需要防伪验证
[ValidateAntiForgeryToken]
[HttpPost("Authenticated/Antiforgery/{name}")]
public IActionResult AuthenticatedAntiforgeryPost(string name)
{
return Ok(name);
}
端点#4,需要经过身份验证的用户以及防伪令牌的验证,是我无法成功测试的端点。
应用程序使用 cookie 身份验证并需要经过身份验证的用户。
// Add authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
}).AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.LoginPath = new PathString("/Login");
}).AddTwoFactorRememberMeCookie();
// Add authorization
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
测试项目配置测试认证方案
public static IWebHostBuilder ConfigureTestAuthenticationScheme(this IWebHostBuilder builder, string scheme)
{
ArgumentNullException.ThrowIfNull(builder);
return builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestScheme", options => { });
});
}
TestAuthHandler
继承自 AuthenticatonHandler
并重写 HandleAuthenticateAsync
方法。
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
Claim[] claims =
[
new Claim(ClaimTypes.Name, "testuser"),
new Claim(ClaimTypes.NameIdentifier, "testuser")
];
ClaimsIdentity identity = new (claims, "Test");
ClaimsPrincipal principal = new (identity);
AuthenticationTicket ticket = new (principal, "TestScheme");
AuthenticateResult result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
可以为测试创建经过身份验证的客户端,如下所示
public HttpClient GetAuthenticatedClient(CookieContainerHandler? cookieHandler = default)
{
cookieHandler ??= new();
string testScheme = "TestScheme";
HttpClient client = WithWebHostBuilder(builder =>
{
builder.ConfigureTestAuthenticationScheme(testScheme);
})
.CreateDefaultClient(cookieHandler);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(scheme: testScheme);
return client;
}
ValidateAntiForgeryToken
属性用于需要防伪令牌验证的两个端点(#3 和 #4)。
测试项目在 AntiforgeryController
上添加了
IWebHostBuilder
,它返回一个包含有效防伪令牌的 JSON 对象。
public static IWebHostBuilder ConfigureAntiforgeryTokenResource(this IWebHostBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
return builder.ConfigureTestServices((services) =>
{
services.AddControllers()
.AddApplicationPart(typeof(AntiforgeryTokenController).Assembly);
});
}
public IActionResult GetAntiforgeryTokens(
[FromServices] IAntiforgery antiforgery,
[FromServices] IOptions<AntiforgeryOptions> options)
{
ArgumentNullException.ThrowIfNull(antiforgery);
ArgumentNullException.ThrowIfNull(options);
AntiforgeryTokenSet tokens = antiforgery.GetTokens(HttpContext);
AntiforgeryTokens model = new()
{
CookieName = options.Value!.Cookie!.Name!,
CookieValue = tokens.CookieToken!,
FormFieldName = options.Value.FormFieldName,
HeaderName = tokens.HeaderName!,
RequestToken = tokens.RequestToken!
};
return Json(model);
}
CustomWebApplicationFactory
提供了一种方法 GetAntiForgeryTokensAsync
,用于在测试方法中 ping AntiforgeryTokenController
。
public async Task<AntiforgeryTokens> GetAntiforgeryTokensAsync(
Func<HttpClient>? httpClientFactory = null,
CancellationToken cancellationToken = default)
{
using HttpClient httpClient = httpClientFactory?.Invoke() ?? CreateDefaultClient();
AntiforgeryTokens? tokens = await httpClient.GetFromJsonAsync<AntiforgeryTokens>(
AntiforgeryTokenController.GetTokensUri,
cancellationToken);
return tokens!;
}
我能够成功测试前三个端点,但是,当我需要测试第四个端点时,需要经过身份验证的用户和防伪令牌验证,则会返回
Bad Request
。
1。测试未经身份验证(匿名)的端点 - 不需要防伪验证
public async Task Unauthenticated_request_to_anonymous_endpoint_returns_ok()
{
// Arrange
HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/anonymous/testname", UriKind.Relative)
};
// Act
HttpResponseMessage response = await _client.SendAsync(message);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
2。测试经过身份验证的端点 - 不需要防伪验证
public async Task Authenticated_request_to_autheticated_endpoint_returns_ok()
{
// Arrange
HttpClient client = _factory.GetAuthenticatedClient();
HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/authenticated/testname", UriKind.Relative)
};
// Act
HttpResponseMessage response = await client.SendAsync(message);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
3.测试未经身份验证(匿名)的端点 - 需要防伪验证
public async Task Unauthenticated_request_to_anonymous_antiforgery_endpoint_with_tokens_returns_ok()
{
// Arrange
AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync();
CookieContainerHandler cookieHandler = new();
cookieHandler.Container.Add(
_factory.Server.BaseAddress,
new Cookie(tokens.CookieName, tokens.CookieValue));
HttpClient client = _factory.CreateDefaultClient(cookieHandler);
client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/anonymous/antiforgery/testname", UriKind.Relative)
};
// Act
HttpResponseMessage response = await client.SendAsync(message);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
4。测试经过身份验证的端点 - 需要防伪验证
我读过浏览器会自动从服务器响应标头中提取任何 cookie,并将它们附加到下一个请求。为了通过 CSRF 进行成功验证,需要对此进行模拟。因此,此测试调用
GetAuthenticationCookies
方法,该方法登录应用程序并从响应中提取身份验证 cookie。
public async Task<List<string>> GetAuthenticationCookies(CookieContainerHandler cookieHandler, AntiforgeryTokens tokens)
{
CancellationToken cancellationToken = new CancellationTokenSource().Token;
HttpClient client = _factory.CreateDefaultClient(cookieHandler);
Uri uri = new($"{client.BaseAddress!.AbsoluteUri}login");
Dictionary<string, string> postData = new()
{
{ "Input.UserName", "testuser" },
{ "Input.Password", "password" },
{ tokens!.FormFieldName, tokens.RequestToken }
};
HttpContent formContent = new FormUrlEncodedContent(postData);
HttpResponseMessage response = await client.PostAsync(uri, formContent, cancellationToken);
return response.Headers.GetValues("Set-Cookie").ToList();
}
然后,除了防伪令牌之外,测试还将这些 cookie 添加到客户端请求标头中。
public async Task Authenticated_request_to_authenticated_antiforgery_endpoint_with_tokens_returns_ok()
{
// Arrange
AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync();
CookieContainerHandler cookieHandler = new();
cookieHandler.Container.Add(
_factory.Server.BaseAddress,
new Cookie(tokens.CookieName, tokens.CookieValue));
HttpClient client = _factory.GetAuthenticatedClient(cookieHandler);
List<string> cookies = await GetAuthenticationCookies(cookieHandler, tokens);
client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
client.DefaultRequestHeaders.Add("Cookie", cookies);
HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/authenticated/antiforgery/testname", UriKind.Relative)
};
// Act
HttpResponseMessage response = await client.SendAsync(message);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
不幸的是,此测试返回
Bad Request
,并且永远不会到达处理程序。
您遇到的错误来自这样一个事实:在您的测试中,防伪令牌是在未经身份验证的情况下获取的,因此当向您的 API 发出经过身份验证的请求时,防伪令牌无效。
解决此问题的一种方法是更改 GetAntiforgeryTokensAsync 以使用基于参数的经过身份验证的客户端:
public async Task<AntiforgeryTokens> GetAntiforgeryTokensAsync(
Func<HttpClient>? httpClientFactory = null,
bool isAuthenticated = false,
CancellationToken cancellationToken = default)
{
using HttpClient httpClient = isAuthenticated
? this.GetAuthenticatedClient()
: (httpClientFactory?.Invoke() ?? CreateDefaultClient());
AntiforgeryTokens? tokens = await httpClient.GetFromJsonAsync<AntiforgeryTokens>(
AntiforgeryTokenController.GetTokensUri,
cancellationToken);
return tokens!;
}
然后您可以更改需要经过身份验证的防伪令牌的测试来设置此参数
[Fact]
public async Task Authenticated_request_to_authenticated_antiforgery_endpoint_with_tokens_returns_ok()
{
// Arrange
List<string> cookies = await GetAuthenticationCookies();
AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync(isAuthenticated: true);
CookieContainerHandler cookieHandler = new();
cookieHandler.Container.Add(
_factory.Server.BaseAddress,
new Cookie(tokens.CookieName, tokens.CookieValue));
HttpClient client = _factory.GetAuthenticatedClient(cookieHandler);
client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
client.DefaultRequestHeaders.Add("Cookie", cookies);
HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/authenticated/antiforgery/testname", UriKind.Relative)
};
// Act
HttpResponseMessage response = await client.SendAsync(message);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}