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

如何在IdentityServer4中结合Identity.Application与JWT认证

ASP.NET 6 授权/资源服务器身份识别问题

我正在实现一个兼具授权服务器与资源服务器功能的ASP.NET 6应用,已为独立部署的Angular SPA配置了带PKCE的授权码流,并添加了Identity Razor页面。

尝试了多个代码示例,仅以下代码有效,但必须为AuthorizeAttribute设置AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme:

services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        // 授权服务器基础URL
        // 服务配置阶段无法获取基础URL,因此硬编码
        options.Authority = _webHostEnvironment.IsDevelopment() ? "https://localhost:5000" : "https://threadbox.prod";
        // API资源
        options.Audience = "threadbox_api";
    });

此配置下,仅能在授权处理器(PermissionHandler.cs)中读取身份,其认证类型为AuthenticationTypes.Federation;而中间件(PermissionsMiddleware.cs)仅能识别认证类型为Identity.Application的身份。


Startup代码

services
    .AddDefaultIdentity<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
    })
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

var identityServerBuilder = services.AddIdentityServer();

if (_webHostEnvironment.IsDevelopment())
{
    identityServerBuilder.AddDeveloperSigningCredential();
}
else
{
    // 使用SSL证书
}

identityServerBuilder
    .AddAspNetIdentity<ApplicationUser>()
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = builder =>
        {
            builder.UseNpgsql(
                _configuration.GetConnectionString(AppSettings.ConnectionStrings.Development),
                // TODO: 程序集名称不应硬编码
                options => options.MigrationsAssembly("ThreadboxApi"));
        };

        options.EnableTokenCleanup = true;
    })
    .AddInMemoryIdentityResources(new List<IdentityResource>
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile()
    })
    .AddInMemoryApiScopes(new List<ApiScope>
    {
        new ApiScope("threadbox_api.access", "Threadbox API"),
    })
    .AddInMemoryApiResources(new List<ApiResource>
    {
        new ApiResource
        {
            Name = "threadbox_api",
            Scopes = { "threadbox_api.access" }
        }
    })
    .AddInMemoryClients(new List<Client>
    {
        new Client
        {
            ClientId = "angular_client",
            ClientName = "Threadbox",

            AllowedCorsOrigins =
            {
                _configuration[AppSettings.ClientBaseUrl]
            },

            AllowedGrantTypes = GrantTypes.Code,
            RequirePkce = true,
            RequireClientSecret = false,
            AllowOfflineAccess = true,

            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                IdentityServerConstants.StandardScopes.OfflineAccess,
                "threadbox_api.access"
            },

            RedirectUris =
            {
                _configuration[AppSettings.ClientBaseUrl] + "/authorization/sign-in-redirect-callback",
                _configuration[AppSettings.ClientBaseUrl] + "/authorization/sign-in-silent-callback"
            },

            PostLogoutRedirectUris =
            {
                _configuration[AppSettings.ClientBaseUrl] + "/authorization/sign-out-redirect-callback"
            },
        },
    });

services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        // 授权服务器基础URL
        // 服务配置阶段无法获取基础URL,因此硬编码
        options.Authority = _webHostEnvironment.IsDevelopment() ? "https://localhost:5000" : "https://threadbox.prod";
        // API资源
        options.Audience = "threadbox_api";
    });

services.AddAuthorization();

services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();

services.AddAutoMapper(Assembly.GetExecutingAssembly());

services.AddMediatR(configuration =>
{
    configuration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
});

services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddFluentValidationAutoValidation();

services.AddMvc();

services.ConfigureApplicationCookie(options =>
{
    // 禁用未授权HTTP请求的重定向
    options.Events.OnRedirectToLogin = context =>
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return Task.CompletedTask;
    };

    // 禁用禁止访问HTTP请求的重定向
    options.Events.OnRedirectToAccessDenied = context =>
    {
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        return Task.CompletedTask;
    };
});

app.UseExceptionHandler();

// 启用HTTP路由
app.UseRouting();

app.UseAuthentication();

app.UseMiddleware<PermissionsMiddleware>();
app.UseIdentityServer();
app.UseAuthorization();

PermissionsMiddleware代码

public class PermissionsMiddleware
{
    private readonly RequestDelegate _next;

    public PermissionsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, IdentityService identityService)
    {
        if (context.Request.Path == "/api/Boards/GetBoardsList")
        {
            var x = context.User.Identity.AuthenticationType;
        }

        if (context.User.Identity.AuthenticationType == "Identity.Application" || context.User.Identity.AuthenticationType == null)
        {
            await _next(context);
            return;
        }

        var userId = context.User.FindFirst("sub")?.Value;

        //if (userId == null)
        //{
        //    await _next(context);
        //    return;
        //}

        var permissionsIdentity = await identityService.GetUserPermissionsIdentity(userId);

        if (permissionsIdentity != null)
        {
            context.User.AddIdentity(permissionsIdentity);
        }

        await _next(context);
    }
}

PermissionHandler代码

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    public PermissionHandler()
    { }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        var hasPermission = context.User.HasClaim(PermissionContants.ClaimType, requirement.Permission);

        if (!hasPermission)
        {
            context.Fail();
            return Task.CompletedTask;
        }

        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 06:45:00