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

如何为ASP.NET Core Web API测试添加模拟ClaimsIdentity

问题描述

我正在为API编写测试用例,需要验证角色不合法的用户请求时是否会返回403 Forbidden状态码。

最初我通过替换IPolicyEvaluator的默认实现来模拟ClaimsIdentity,代码如下:

public class FakePolicyEvaluator : IPolicyEvaluator
{
    public ClaimsIdentity? ClaimsIdentity { get; set; }

    public Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
    {
        var claimsPrincipal = new ClaimsPrincipal();

        if (ClaimsIdentity != null)
        {
            claimsPrincipal.AddIdentity(ClaimsIdentity);
        }

        var ticket = new AuthenticationTicket(claimsPrincipal, "test");
        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }

    public Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource)
    {
        var result = PolicyAuthorizationResult.Success();

        return Task.FromResult(result);
    }
}

我的控制器方法标记了[Authorize(Roles="SomeAllowedRole")]特性,但这个方案不生效,推测是因为AuthorizeAsync直接返回了PolicyAuthorizationResult.Success(),跳过了角色校验。

后来我考虑在FakePolicyEvaluator中注入原始的IPolicyEvaluator实现,让真实的授权逻辑生效,代码如下:

public class FakePolicyEvaluator : IPolicyEvaluator
{
    private readonly IPolicyEvaluator _realPolicyEvaluator;

    public FakePolicyEvaluator(IPolicyEvaluator realPolicyEvaluator)
    {
        _realPolicyEvaluator = realPolicyEvaluator;
    }

    public ClaimsIdentity? ClaimsIdentity { get; set; }

    public Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
    {
        var claimsPrincipal = new ClaimsPrincipal();

        if (ClaimsIdentity != null)
        {
            claimsPrincipal.AddIdentity(ClaimsIdentity);
        }

        context.User = claimsPrincipal;
        return _realPolicyEvaluator.AuthenticateAsync(policy, context);
    }

    public Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource)
    {
        return _realPolicyEvaluator.AuthorizeAsync(policy, authenticationResult, context, resource);
    }
}

但我不知道如何在服务集合中注册这个类,或者说如何获取原始的PolicyEvaluator实例并注入到Fake类中,求解决办法。


解决方案

1. 修正FakePolicyEvaluator的依赖逻辑

保持当前代码中依赖IPolicyEvaluator接口的写法,不要直接依赖具体的PolicyEvaluator类,这样更符合依赖注入原则,也方便后续注册。

2. 用装饰器模式注册FakePolicyEvaluator

要实现"替换原始实现但保留其逻辑",可以通过装饰器模式完成注册:先保留原始的IPolicyEvaluator服务,再将FakePolicyEvaluator注册为接口的实现,并注入原始实例。

在WebApplicationFactory中注册(集成测试场景)

如果使用WebApplicationFactory做集成测试,重写ConfigureWebHost方法完成注册:

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    public FakePolicyEvaluator? FakePolicyEvaluator { get; private set; }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // 找到并移除原始的IPolicyEvaluator注册
            var originalRegistration = services.SingleOrDefault(d => d.ServiceType == typeof(IPolicyEvaluator));
            if (originalRegistration != null)
            {
                services.Remove(originalRegistration);
                
                // 创建原始IPolicyEvaluator实例
                var realEvaluator = (IPolicyEvaluator)ActivatorUtilities.CreateInstance(ServiceProvider, originalRegistration.ImplementationType!);
                // 实例化Fake类并注入原始实现
                FakePolicyEvaluator = new FakePolicyEvaluator(realEvaluator);
                
                // 注册Fake类作为IPolicyEvaluator的实现
                services.AddSingleton<IPolicyEvaluator>(FakePolicyEvaluator);
            }
        });
    }
}

用Autofac等容器直接注册(项目使用第三方DI容器场景)

如果项目使用Autofac,可直接通过装饰器语法注册:

builder.RegisterType<PolicyEvaluator>().As<IPolicyEvaluator>();
builder.RegisterDecorator<FakePolicyEvaluator, IPolicyEvaluator>();

3. 测试时设置模拟角色

在测试方法中,获取FakePolicyEvaluator实例并设置无权限的角色:

using var factory = new CustomWebApplicationFactory<Startup>();
var client = factory.CreateClient();

// 设置无权限的用户角色
factory.FakePolicyEvaluator!.ClaimsIdentity = new ClaimsIdentity(new[]
{
    new Claim(ClaimTypes.Role, "UnauthorizedRole")
});

// 发起请求
var response = await client.GetAsync("/api/your-protected-endpoint");

// 验证返回状态码
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);

4. 更简单的替代方案:直接设置HttpContext.User

其实在集成测试中,不需要替换IPolicyEvaluator,可以直接通过自定义中间件设置HttpContext.User:

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    public ClaimsPrincipal? TestUser { get; set; }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // 添加测试用的认证方案
            services.AddAuthentication("TestAuth")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestAuth", _ => { });
        });

        // 添加中间件设置测试用户
        builder.UseMiddleware<TestUserMiddleware>();
    }

    // 中间件:将TestUser赋值给HttpContext.User
    private class TestUserMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly CustomWebApplicationFactory<TStartup> _factory;

        public TestUserMiddleware(RequestDelegate next, CustomWebApplicationFactory<TStartup> factory)
        {
            _next = next;
            _factory = factory;
        }

        public async Task Invoke(HttpContext context)
        {
            if (_factory.TestUser != null)
            {
                context.User = _factory.TestUser;
                context.Items["IsTestAuth"] = true;
            }
            await _next(context);
        }
    }

    // 测试用的认证处理器
    private class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
            : base(options, logger, encoder, clock) { }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (Context.Items.TryGetValue("IsTestAuth", out var value) && value is true)
            {
                var ticket = new AuthenticationTicket(Context.User, Scheme.Name);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            return Task.FromResult(AuthenticateResult.Fail("未设置测试用户"));
        }
    }
}

测试时直接设置TestUser即可:

using var factory = new CustomWebApplicationFactory<Startup>();
// 设置无权限用户
factory.TestUser = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
    new Claim(ClaimTypes.Role, "UnauthorizedRole")
}, "TestAuth"));

var client = factory.CreateClient();
var response = await client.GetAsync("/api/your-protected-endpoint");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);

内容的提问来源于stack exchange,提问作者pawelfelcyn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 11:55:21