ASP.NET Boilerplate 集成 IdentityServer4:基于 Abp.ZeroCore 构建 OpenID Connect 与 OAuth 2.0 认证授权服务

  • 后端
  • Web框架
  • 依赖注入
  • 认证鉴权

【免费下载链接】aspnetboilerplate

ASP.NET Boilerplate - Web Application Framework

项目地址: https://gitcode.com/gh_mirrors/as/aspnetboilerplate
点击查看 免费下载

本文以 ASP.NET Boilerplate 开源仓库中 Identity-Server-vNext.md 文档为骨架,完整讲解如何将 IdentityServer4(2.0+ 版本)集成进基于 ASP.NET Core + Module Zero(Abp.ZeroCore)的启动模板项目,涵盖 NuGet 包安装、模块依赖、Startup 管线配置、IdentityServer 资源与客户端定义、EF Core 持久化改造、JWT 认证中间件接入,以及通过 Console 客户端实测令牌颁发与受保护 API 调用的完整流程。读完本文,你将具备在 ABP 框架中快速搭建一个可签发 access token、支持用户密码模式(Resource Owner Password)与客户端凭证模式的单点登录(SSO)/认证服务器的实战能力。

注意:IdentityServer 已改变其开源许可模型,ASP.NET Boilerplate 官方文档明确建议新项目改用 OpenIddict 集成方案。本文面向需要维护或继续使用 IdentityServer4 集成的既有项目,并会给出两者选型建议。

IdentityServer4 与 ABP 集成的背景

IdentityServer4 是一个开源的 OpenID Connect 与 OAuth 2.0 框架(官方站点 identityserver.io)。将它接入应用后,你的应用可以成为一个认证 / 单点登录(SSO)服务器,并且能够为第三方客户端签发 access token,用于访问受保护的 Web API。

在 ASP.NET Boilerplate 中,Module Zero(Abp.ZeroCore)已经提供了多租户、用户管理、角色管理、权限体系等基础设施;在此基础上叠加 IdentityServer4,即可让 ABP 应用在"业务框架"之上天然拥有标准化的认证授权协议能力。本文描述的集成方式适用于 IdentityServer4 2.0+ 版本,仓库中的 NuGet 包说明(Nuget-Packages.md)进一步指出,带 vNext 后缀的包面向 IdentityServer4 4.x+ 版本。

前置条件

  • 已从启动模板创建基于 ASP.NET Core(含 Module Zero)的项目并成功运行,模板说明见 Startup-Template-Core。
  • 熟悉 ABP 模块系统(Module System) 的基本用法,因为集成第一步就是添加模块依赖。
  • 了解 IdentityServer4 自身的概念(IdentityResource、ApiResource、ApiScope、Client、GrantTypes),本文只讲解与 ABP 集成时额外需要做的事。

安装:两个 NuGet 包的分工

IdentityServer4 与 Abp.ZeroCore 的集成涉及两个 NuGet 包:

包名作用
Abp.ZeroCore.IdentityServer4.vNext主集成包,提供 AddAbpIdentityServer、AddAbpPersistedGrants 等 ABP 专用扩展方法与模块定义
Abp.ZeroCore.IdentityServer4.vNext.EntityFrameworkCoreEF Core 存储提供程序,用于把授权许可(consent)等数据持久化到数据库

由于 EF Core 包已经依赖主集成包,你只需把 Abp.ZeroCore.IdentityServer4.vNext.EntityFrameworkCore 安装到包含 DbContext 的项目(默认模板中是 .EntityFrameworkCore 项目)即可:

Install-Package Abp.ZeroCore.IdentityServer4.vNext.EntityFrameworkCore

在仓库中,该包对应的工程文件位于 src/Abp.ZeroCore.IdentityServer4.vNext.EntityFrameworkCore/Abp.ZeroCore.IdentityServer4.vNext.EntityFrameworkCore.csproj,其工程引用指向 Abp.ZeroCore.EntityFrameworkCore,目标框架为 net9.0,这印证了"EF Core 包依赖主集成包所在的项目链"这一事实。

添加模块依赖

安装完成后,在你的 ABP 模块类(一般是 EntityFrameworkCore 项目的模块)上添加 DependsOn 依赖:

[DependsOn(typeof(AbpZeroCoreIdentityServervNextEntityFrameworkCoreModule))]
public class MyModule : AbpModule
{
    //...
}

这样,IdentityServer4 的存储、ABP 集成模块会随应用启动一并初始化。

配置 IdentityServer:与标准用法的差异点

在 Abp.ZeroCore 中使用 IdentityServer4,其整体思路与独立使用 IdentityServer4 基本一致(更完整的机制请查阅 IdentityServer4 官方文档),ABP 集成只增加了少量粘合代码。下面按 Startup 类、资源/客户端配置类、DbContext 三个层面展开。

Startup 类:服务注册与中间件管线

在 ASP.NET Core 的 Startup 类中,需要把 IdentityServer 加入服务集合(Service Collection)与中间件管线(Middleware Pipeline)。与标准 IdentityServer4 用法相比,差异点已高亮标注:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        //...

        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
            .AddInMemoryApiScopes(IdentityServerConfig.GetApiScopes())
            .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
            .AddInMemoryClients(IdentityServerConfig.GetClients())
            .AddAbpPersistedGrants<IAbpPersistedGrantDbContext>()
            .AddAbpIdentityServer<User>();

        //...
    }

    public void Configure(IApplicationBuilder app)
    {
        //...

            app.UseJwtTokenMiddleware("IdentityBearer");
            app.UseIdentityServer();

        //...
    }
}

在启动模板项目中,代码插入位置如下:

  • services.AddIdentityServer() 放在 IdentityRegistrar.Register(services) 之后;
  • app.UseJwtTokenMiddleware("IdentityBearer") 放在 app.UseAuthentication() 之后。

AddAbpIdentityServer<User>() 是 ABP 提供的扩展,它会把 ABP 的用户体系(User 实体)与 IdentityServer4 的认证/令牌流程衔接起来——也就是说,密码模式登录时校验的账号正是 Module Zero 管理的用户。

IdentityServerConfig 类:定义身份资源、API 资源与客户端

IdentityServerConfig 用于集中定义身份资源(Identity Resources)、API 资源(API Resources)与客户端(Clients)。最简单的情况下,它可以是一个静态类:

public static class IdentityServerConfig
{
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Email(),
            new IdentityResources.Phone()
        };
    }

    public static IEnumerable<ApiScope> GetApiScopes()
    {
        return new List<ApiScope>
        {
            new ApiScope("default-api-scope")
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("default-api-resource", "Default (all) API")
            {
                Scopes = { "default-api-scope" }
            }
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "client",
                AllowedGrantTypes = GrantTypes.ClientCredentials.Union(GrantTypes.ResourceOwnerPassword).ToList(),
                AllowedScopes = {"default-api-scope"},
                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                }
            }
        };
    }
}

要点说明:

  • GetIdentityResources():声明 OpenID Connect 的 OpenId / Profile / Email / Phone 标准身份资源,客户端请求时可携带对应的 scope 以获取用户身份信息。
  • GetApiScopes() 与 GetApiResources():定义一个名为 default-api-resource 的 API 资源,并将其绑定到 default-api-scope 作用域上;受保护 API 校验令牌时需要声明允许该 scope。
  • GetClients():注册客户端 client(密钥为 secret 的 SHA256 哈希),同时启用 ClientCredentials(客户端凭证模式)与 ResourceOwnerPassword(密码模式)两种授权类型,允许访问 default-api-scope。
  • 上述资源/客户端均为内存配置(AddInMemory*),适用于原型与演示;生产环境可替换为 IdentityServer4 的持久化存储。

DbContext 变更:持久化授权许可

AddAbpPersistedGrants() 扩展方法用于把**用户同意响应(consent responses)**保存到持久化存储中。要使用它,YourDbContext 必须实现 IAbpPersistedGrantDbContext 接口:

public class YourDbContext : AbpZeroDbContext<Tenant, Role, User, YourDbContext>, IAbpPersistedGrantDbContext
{
    public DbSet<PersistedGrantEntity> PersistedGrants { get; set; }

    public YourDbContext(DbContextOptions<YourDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.ConfigurePersistedGrantEntity();
    }
}

其中:

  • IAbpPersistedGrantDbContext 接口定义了 PersistedGrants 这个 DbSet<PersistedGrantEntity>;
  • 必须在 OnModelCreating 中调用 modelBuilder.ConfigurePersistedGrantEntity() 扩展方法,为 PersistedGrantEntity 配置 EF Core 映射。

不要忘记迁移:这个 DbContext 变更会生成一个新的数据库迁移,请执行:

Add-Migration <YourMigrationName>
Update-Database

关于省略持久化的风险:即使不调用 AddAbpPersistedGrants<YourDbContext>() 扩展方法,IdentityServer4 依然能正常工作——但此时用户同意响应会存储在内存数据存储中,应用重启后即被清空!因此生产环境务必启用 EF Core 持久化。

JWT 认证中间件:保护同一个应用内的 API

如果我们希望客户端与同一个应用进行授权交互(即令牌颁发方与 API 宿主是同一应用),可以使用 IdentityServer 认证中间件(IdentityServer4.AccessTokenValidation)来校验令牌。

首先从 NuGet 安装认证包:

Install-Package IdentityServer4.AccessTokenValidation

然后在 Startup 类中添加认证方案(放在 services.AddIdentityServer() 这行之后):

services.AddAuthentication().AddIdentityServerAuthentication("IdentityBearer", options =>
{
    options.Authority = "https://localhost:44388/";
    options.RequireHttpsMetadata = false;
});

要点说明:

  • 方案名 "IdentityBearer" 必须与 app.UseJwtTokenMiddleware("IdentityBearer") 中传入的名称一致;
  • Authority 指向身份服务器地址(此处为本地开发地址 https://localhost:44388/,请替换为你的实际站点地址);
  • RequireHttpsMetadata = false 允许开发环境下通过 HTTP/自签名 HTTPS 获取元数据;生产环境应保持 true 并使用 HTTPS。

配置完成后,ABP 的 Web API 控制器与动态 Web API 即可通过该认证方案验证由 IdentityServer 签发的 JWT 令牌。

测试:用 Console 客户端实测完整认证流程

身份服务器就绪后,我们可以创建一个 Console 应用程序作为客户端,走一遍"获取令牌 → 调用受保护 API"的完整流程。

客户端项目准备

  • 在解决方案中新建一个 Console Application 项目;
  • 添加 IdentityModel NuGet 包:用于创建访问 OAuth 端点的客户端(发现文档、请求令牌等);
  • 添加对 Application 层的项目引用:这样客户端可以直接复用应用服务返回的 DTO 类型,实现类型安全的 API 调用;
  • 添加 Abp.Web.Common NuGet 包:以便使用 ASP.NET Boilerplate 定义的 AjaxResponse 类统一解析服务端响应,避免手工处理原始 JSON 字符串。

完整 Program.cs 代码

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Abp.Web.Models;
using IdentityModel.Client;
using Newtonsoft.Json;

namespace IdentityServerIntegrationDemo.ConsoleApiClient
{
    class Program
    {
        static void Main(string[] args)
        {
            RunDemoAsync().Wait();
            Console.ReadLine();
        }

        public static async Task RunDemoAsync()
        {
            var accessToken = await GetAccessTokenViaOwnerPasswordAsync();
            await GetUsersListAsync(accessToken);
        }

        private static async Task<string> GetAccessTokenViaOwnerPasswordAsync()
        {
            var disco = await new HttpClient().GetDiscoveryDocumentAsync("https://localhost:44388");

            var tokenClient = new HttpClient();
            tokenClient.DefaultRequestHeaders.Add("Abp.TenantId", "1");
            var tokenResponse = await tokenClient.RequestPasswordTokenAsync(new PasswordTokenRequest()
            {
                Address = disco.TokenEndpoint,
                ClientId = "client",
                ClientSecret = "secret",
                UserName = "admin",
                Password = "123qwe"
            });

            if (tokenResponse.IsError)
            {
                Console.WriteLine("Error: ");
                Console.WriteLine(tokenResponse.Error);
            }

            Console.WriteLine(tokenResponse.Json);

            return tokenResponse.AccessToken;
        }

        private static async Task GetUsersListAsync(string accessToken)
        {
            var client = new HttpClient();
            client.SetBearerToken(accessToken);

            var response = await client.GetAsync("https://localhost:44388/api/services/app/user/GetAll");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
                return;
            }

            var content = await response.Content.ReadAsStringAsync();
            var ajaxResponse = JsonConvert.DeserializeObject<AjaxResponse<PagedResultDto<UserListDto>>>(content);
            if (!ajaxResponse.Success)
            {
                throw new Exception(ajaxResponse.Error?.Message ?? "Remote service throws exception!");
            }

            Console.WriteLine();
            Console.WriteLine("Total user count: " + ajaxResponse.Result.TotalCount);
            Console.WriteLine();
            foreach (var user in ajaxResponse.Result.Items)
            {
                Console.WriteLine($"### UserId: {user.Id}, UserName: {user.UserName}");
                Console.WriteLine(JsonConvert.SerializeObject(user, Formatting.Indented));
            }
        }
    }

    internal class UserListDto
    {
        public int Id { get; set; }
        public string UserName { get; set; }
    }
}

运行前检查清单

  • Web 项目必须已启动并正在运行:Console 应用会向 Web 应用发起真实 HTTP 请求;
  • 端口必须一致:Console 应用中请求的端口(示例为 44388)必须与你的 Web 应用实际监听端口相同;
  • 账号必须存在:密码模式使用的 admin / 123qwe 是启动模板的默认管理员账号,请按你的实际初始化数据调整;
  • 多租户头:示例在令牌请求时附加了 Abp.TenantId: 1 请求头,这是 ABP 多租户体系(见 Multi-Tenancy)在令牌阶段的典型用法,单租户场景可去掉。

预期执行流程

  1. GetDiscoveryDocumentAsync 从身份服务器获取 TokenEndpoint 等端点信息(基于 OpenID Connect Discovery 协议);
  2. RequestPasswordTokenAsync 使用 client/secret 与 admin 账号密码换取 access token(走 Resource Owner Password 授权模式),并打印令牌的 JSON 详情;
  3. GetUsersListAsync 携带 Bearer 令牌调用 ABP 动态 Web API api/services/app/user/GetAll;
  4. 用 AjaxResponse<PagedResultDto<UserListDto>> 反序列化 ABP 标准响应,输出用户总数与每条用户记录的 Id、UserName 及完整 JSON。

成功运行后,你将看到控制台依次输出令牌响应 JSON、用户总数以及用户明细,证明"IdentityServer4 签发令牌 → ABP API 校验令牌 → 返回 DTO 数据"整条链路已打通。

许可证变更背景与 OpenIddict 迁移建议

为什么文档推荐新项目使用 OpenIddict

IdentityServer 官方改变了其许可证模型,因此 ASP.NET Boilerplate 文档在本文开头即明确:新项目建议使用 OpenIddict。OpenIddict 旨在为任意 ASP.NET Core 2.1+ 应用提供 OpenID Connect 客户端、服务端与令牌校验支持(ASP.NET 4.6.1+ 也通过 Owin 4.2 集成获得支持)。

仓库中已提供完整的 OpenIddict 集成方案,对应文档见 OpenIddict.md,涉及三个 NuGet 包:

包名安装项目
Abp.ZeroCore.OpenIddictCore 项目,模块依赖 AbpZeroCoreOpenIddictModule
Abp.AspNetCore.OpenIddictWeb 项目,模块依赖 AbpAspNetCoreOpenIddictModule
Abp.ZeroCore.OpenIddict.EntityFrameworkCoreEntityFrameworkCore 项目,模块依赖 AbpZeroCoreOpenIddictEntityFrameworkCoreModule

OpenIddict 方案的核心步骤包括:在 DbContext 实现 IOpenIddictDbContext 并在 OnModelCreating 中调用 modelBuilder.ConfigureOpenIddict();在 Web 项目中继承 ABP 提供的 AuthorizeController、TokenController、UserInfoController 泛型控制器;在 Startup.cs 中通过静态注册类完成 OpenIddict 配置。整体思路与本文的 IdentityServer4 集成非常相似:先装包、再加模块依赖、再改 DbContext、再配 Startup。

如何选择

  • 新项目:优先采用 OpenIddict 集成,这是 ASP.NET Boilerplate 官方推荐的现行方案;
  • 既有项目:若已基于 Abp.ZeroCore.IdentityServer4.vNext 集成 IdentityServer4,可继续沿用本文配置维护,同时规划向 OpenIddict 迁移;
  • 包版本区分:根据 Nuget-Packages.md 的说明,Abp.ZeroCore.IdentityServer4(无 vNext 后缀)面向早期 IdentityServer4 版本,Abp.ZeroCore.IdentityServer4.vNext 面向 IdentityServer4 4.x+,两个主包分别对应各自的 EntityFrameworkCore 存储包,选择时务必与所用 IdentityServer4 大版本匹配。

集成要点速查

步骤关键操作位置
1. 装包Install-Package Abp.ZeroCore.IdentityServer4.vNext.EntityFrameworkCoreEntityFrameworkCore 项目
2. 模块依赖[DependsOn(typeof(AbpZeroCoreIdentityServervNextEntityFrameworkCoreModule))]模块类
3. 服务注册services.AddIdentityServer().AddAbpPersistedGrants<...>().AddAbpIdentityServer<User>()ConfigureServices,紧跟 IdentityRegistrar.Register(services) 之后
4. 中间件app.UseJwtTokenMiddleware("IdentityBearer"); app.UseIdentityServer();Configure,紧跟 app.UseAuthentication() 之后
5. 资源/客户端静态类 IdentityServerConfig 提供 GetIdentityResources/GetApiScopes/GetApiResources/GetClients任意合适位置
6. DbContext实现 IAbpPersistedGrantDbContext,OnModelCreating 中调用 ConfigurePersistedGrantEntity(),然后 Add-Migration + Update-DatabaseEntityFrameworkCore 项目
7. JWT 校验(可选)Install-Package IdentityServer4.AccessTokenValidation,注册 IdentityBearer 认证方案Startup
8. 客户端测试Console 应用 + IdentityModel + Application 层引用 + Abp.Web.Common解决方案内

延伸阅读

  • 后端
  • Web框架
  • 依赖注入
  • 认证鉴权

【免费下载链接】aspnetboilerplate

ASP.NET Boilerplate - Web Application Framework

项目地址: https://gitcode.com/gh_mirrors/as/aspnetboilerplate
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值