IdentityServer4: 配置项持久化

IdentityServer4 配置项持久化

​ 对于 IdentityServer4 认证和授权框架,是支持数据库持久化操作的,也就是在 IdentityServer4 服务器上需要配置的一些数据存储到数据库中永久存储.
在 IdentityServer4 服务器上,配置的数据有:客户端、API 作用域等,之前我们都是存储在内存中进行操作,这里,我们将这些数据存储到数据库中。IdentityServer4 支持的持久存储体有 Redis、SQLServer、
Oracle、MySql 等。

注意:这里永久存储的数据不包括用户信息,用户信息需要使用ASP.NET Core Identity 认证框架来实现。

创建 IdentityServer4 项目

打开 VS IDE 开发工具,新建一个 ASP.NET Core 6 空白项目,名称为:Dotnet.WebApi.Ids4.AuthService。

添加依赖包

添加 IdentityServer 配置项持久化所需的依赖包:

 <PackageReference Include="IdentityServer4" Version="4.1.2" />
 <PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
 <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />
 <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">

然后在其中添加如下程序包:
(1). IdentityServer4 最新稳定程序包,主程序包。
(2). IdentityServer4.EntityFramework:用于 IdentityServer4 的 EF Core 程序包,安装最新稳定版本。
(3). Microsoft.EntityFrameworkCore.SqlServer:使用 EF Core 工具操作微软的 SQL Server 数据库程序包,安装最新稳定版本。
(4). Microsoft.EntityFrameworkCore.Tools:使用 Nuget 包管理器控制台进行配置迁移操作的程序包,安装最新稳定版本。

添加 Quickstart UI

下载 Quickstart UI:https://github.com/IdentityServer/IdentityServer4.Quickstart.UI,
然后把 Quickstart、Views、wwwroot 三个文件夹复制到 Dotnet.WebApi.Ids4.AuthService 项目根目录下。

数据库迁移

ConfigurationDbContext

使用 ConfigurationDbContext 生成迁移客户端、资源数据的代码,命令如下:

Add-Migration init -Context ConfigurationDbContext -OutputDir Data/Migrations/Ids4/ConfigurationDb

使用如下命令生成 ConfigurationDbContext 相关表结构:

Update-Database -Context ConfigurationDbContext

PersistedGrantDbContext

使用 PersistedGrantDbContext 生成迁移同意授权的临时数据、Token 代码,命令如下:

Add-Migration init -Context PersistedGrantDbContext -OutputDir Data/Migrations/Ids4/PersistedGrantDb

使用如下命令生成 PersistedGrantDbContext 相关的表结构:

Update-Database -Context PersistedGrantDbContext

查看迁移结果

迁移成功后,数据库表会新增相关的数据表,如下图所示:

其中,除了 PersistedGrantsDeviceCodes 这两张表来自 ``PersistedGrantDbContext外,其它表都来自ConfigurationDbContext`

生成初始化数据

在 Program 类中添加如下代码,初始化 Client 数据:

//同步数据
SyncData.InitializeDatabase(app); 

SyncData的代码如下:

using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using Microsoft.EntityFrameworkCore;

namespace Dotnet.WebApi.Ids4.AuthService
{
    public class SyncData
    {
        public static void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
                context.Database.Migrate();
                if (!context.Clients.Any())
                {
                    foreach (var client in IdentityConfig.GetClients())
                    {
                        context.Clients.Add(client.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.IdentityResources.Any())
                {
                    foreach (var resource in IdentityConfig.GetIdentityResources())
                    {
                        context.IdentityResources.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }

                if (!context.ApiScopes.Any())
                {
                    foreach (var api in IdentityConfig.GetApiScopes())
                    {
                        context.ApiScopes.Add(api.ToEntity());
                    }
                    context.SaveChanges();
                }
            }
        }
    }
}

严重 BUG

如果使用 .net7, 在调用方法context.Clients.Add(client.ToEntity());,会出现一个BUG:

System.TypeInitializationException:“The type initializer for 'IdentityServer4.EntityFramework.Mappers.ClientMappers' threw an exception.”

The type initializer for 'IdentityServer4.EntityFramework.Mappers.ClientMappers' threw an exception.
InnerException	{"GenericArguments[0], 'System.Char', on 'T MaxFloat[T](System.Collections.Generic.IEnumerable`1[T])'
violates the constraint of type 'T'."}	System.Exception {System.ArgumentException}

而开源项目 IdentityServer4 已经停止维护,
请将 Dotnet.WebApi.Ids4.AuthService 项目改为 .net6, 修改方法为编辑项目文件里面的: <TargetFramework>net7.0</TargetFramework> 改为: <TargetFramework>net6.0</TargetFramework>

集成 IdentityServer4

集成 IdentityServer4 代码:

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Ids4Connection": "Server=localhost;Database=Ids4_Db;Uid=sa;Pwd=123456;Encrypt=True;TrustServerCertificate=True;"
  }
}

Program.cs

using Microsoft.EntityFrameworkCore;
using System.Reflection;

namespace Dotnet.WebApi.Ids4.AuthService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.Title = "IdentityServer4服务器";

            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddControllersWithViews();
            //获取数据库连接字符串,从appsettings.json中读取。
            var connetionString = builder.Configuration.GetConnectionString("Ids4Connection");
            //获取迁移使用的程序集,这里是在Program类中实现迁移操作的。
            var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name;

            //注册IdentityServer,并使用EFCore存储客户端和API作用域。
            var ids4Builder = builder.Services.AddIdentityServer()
                //配置存储客户端、资源等到数据库中。
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = dbBuilder =>
                        dbBuilder.UseSqlServer(connetionString, t_builder => 
                           t_builder.MigrationsAssembly(migrationsAssembly));
                })
                //配置用户授权的同意授权的数据、Token等存储到数据库中。
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = dbBuilder =>
                        dbBuilder.UseSqlServer(connetionString, t_builder =>
                           t_builder.MigrationsAssembly(migrationsAssembly));
                })
                //使用临时的用户,后续使用ASP.NET Identity认证存储用户。
                .AddTestUsers(IdentityConfig.GetTestUsers());

            //RSA证书加密,使用开发环境下的临时证书,后续使用固定证书。
            ids4Builder.AddDeveloperSigningCredential();


            var app = builder.Build();
            //同步数据
            SyncData.InitializeDatabase(app); 

            //发布后的端口号
            app.Urls.Add("https://*:6001");
            //启用静态文件。
            app.UseStaticFiles();
            //路由
            app.UseRouting();
            //启用IdentityServer4。
            app.UseIdentityServer();
            //身份验证
            app.UseAuthentication();
            //授权
            app.UseAuthorization();
            //终结点
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });

            app.Run();
        }
    }
}

IdentityConfig.cs

using IdentityModel;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Security.Claims;

namespace Dotnet.WebApi.Ids4.AuthService
{
    public class IdentityConfig
    {
        /// <summary>
        /// 配置IdentityResource。
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource> {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
            };
        }

        /// <summary>
        /// 配置可访问的API范围。
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new List<ApiScope>
            {
                new ApiScope("OAAPI","OA办公平台API。")
            };
        }

        /// <summary>
        /// 配置可从IDS4认证中心获取授权码和令牌的客户端。
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    //客户端ID。
                    ClientId="MvcApp",
                    //客户端名称。
                    ClientName="MvcApplication",
                    //客户端密钥。
                    ClientSecrets =
                    {
                        new Secret("MvcAppOA00000001".Sha256())
                    },
                    //Code表示授权码认证模式。
                    AllowedGrantTypes=GrantTypes.Code,
                    //是否支持授权操作页面,true表示显示授权界面,否则不显示。
                    RequireConsent=true,
                    //认证成功之后重定向的客户端地址,默认就是signin-oidc。
                    RedirectUris={ "https://localhost:6003/signin-oidc"},
                    //登出时重定向的地址,默认是siginout-oidc。
                    PostLogoutRedirectUris={"https://localhost:6003/signout-callback-oidc"},
                    //是否允许返回刷新Token。
                    AllowOfflineAccess=true,
                    //指定客户端获取的AccessToken能访问到的API作用域。
                    AllowedScopes={
                        //API作用域。
                        "OAAPI",
                        //OpenId身份信息权限。
                        IdentityServerConstants.StandardScopes.OpenId,
                        //Profile身份信息权限。
                        IdentityServerConstants.StandardScopes.Profile
                    }
                }
            };
        }

        /// <summary>
        /// 配置账户,用于登录。
        /// </summary>
        /// <returns></returns>
        public static List<TestUser> GetTestUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                     SubjectId="00001",
                     Username="kevin",
                     Password="123456",
                      Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "kevin"),
                            new Claim(JwtClaimTypes.GivenName, "kevin"),
                            new Claim(JwtClaimTypes.FamilyName, "Li"),
                            new Claim(JwtClaimTypes.Email, "kevin@dotnet.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
                    }
                }
            };
        }

    }
}

创建资源Api项目

创建资源Api项目,打开 VS,新建 AspNetCore WebApi 项目,名为: Dotnet.WebApi.Ids4.Api

添加依赖包

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />

添加Api

添加 Controllers/WeatherForecastController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Dotnet.WebApi.Ids4.Api.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [Authorize("OAScope")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = new DateTime(2060, 9, 23).AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

添加认证方案

修改 Program.cs 类为如下代码:

using Microsoft.IdentityModel.Tokens;

namespace Dotnet.WebApi.Ids4.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddControllers();

            //注册认证组件并配置Bearer
            builder.Services.AddAuthentication("Bearer")
                .AddJwtBearer("Bearer", options =>
                {
                    //认证服务器地址
                    options.Authority = "https://localhost:6001";
                    //在验证token时,不验证Audience
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = false
                    };
                });

            //配置策略授权
            builder.Services.AddAuthorization(options =>
            {
                options.AddPolicy("OAScope", policy =>
                {
                    policy.RequireAuthenticatedUser();
                    policy.RequireClaim("scope", "OAAPI");
                });
            });

            var app = builder.Build();
            //设置端口号
            app.Urls.Add("https://*:6002");
            app.UseHttpsRedirection();

            //认证中间件
            app.UseAuthentication();
            //授权中间件
            app.UseAuthorization();

            app.MapControllers();
            app.Run();
        }
    }
}

客户端项目

创建资源Api项目,打开 VS,新建 AspNetCore MVC 项目,名为: Dotnet.WebApi.Ids4.MvcApp。

添加依赖包

添加依赖包

    <PackageReference Include="IdentityModel" Version="6.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.3" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

Program.cs

修改 Program.cs 代码为如下代码:

using Microsoft.AspNetCore.Authentication.Cookies;
using System.IdentityModel.Tokens.Jwt;

namespace Dotnet.WebApi.Ids4.MvcApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.Title = "MVC客户端";

            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddControllersWithViews();

            //去除映射,保留Jwt原有的Claim名称
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            builder.Services.AddAuthentication(options => {
                    //使用Cookies 
                    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    //使用OpenID Connect 
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddOpenIdConnect("oidc", options =>
                {
                    options.SignInScheme = "Cookies";
                    //客户端ID
                    options.ClientId = "MvcApp";
                    //客户端密钥
                    options.ClientSecret = "MvcAppOA00000001";
                    //IdentityServer4服务器地址
                    options.Authority = "https://localhost:6001";
                    //响应授权码
                    options.ResponseType = "code";
                    //允许Token保存的Cookies中
                    options.SaveTokens = true;
                    //权限范围
                    options.Scope.Clear();
                    options.Scope.Add("openid");
                    options.Scope.Add("profile");
                    //设置允许获取刷新Token
                    options.Scope.Add("offline_access");
                    //设置访问的API范围
                    options.Scope.Add("OAAPI");
                    //获取用户的Claims信息
                    options.GetClaimsFromUserInfoEndpoint = true;
                });


            var app = builder.Build();

            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            //发布后的端口号
            app.Urls.Add("https://*:6003");

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            //Cookie策略
            app.UseCookiePolicy();
            //身份验证
            app.UseAuthentication();
            //授权。
            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            app.Run();
        }
    }
}

调用Api

在 Controllers/HomeController.cs 中添加如下代码:

using Dotnet.WebApi.Ids4.MvcApp.Models;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Diagnostics;

namespace Dotnet.WebApi.Ids4.MvcApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        ......
        /// <summary>
        /// 获取API资源。
        /// </summary>
        /// <returns></returns>
        public async Task<IActionResult> ApiData()
        {
            //获取accessToken
            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            //请求API资源
            var httpClient = new HttpClient();
            //将获取到的AccessToken以Bearer的方案设置在请求头中
            httpClient.SetBearerToken(accessToken);
            //向API资源服务器请求受保护的API
            var data = await httpClient.GetAsync("https://localhost:6002/api/WeatherForecast");
            if (data.IsSuccessStatusCode)
            {
                var r = await data.Content.ReadAsStringAsync();
                ViewBag.ApiData = r;
            }
            else
            {
                ViewBag.ApiData = $"获取API数据失败。状态码:{data.StatusCode}";
            }
            return View();
        }
    }
}

添加视图: Views/Home/ApiData.cshtml

<p>
    @ViewBag.ApiData
</p>
posted @ 2023-03-09 19:31  easy5  阅读(733)  评论(0编辑  收藏  举报