IdentityServer4: 配置項持久化

IdentityServer 配置項持久化

​ 對於 IdentityServer4 認證和授權框架,是支持數據庫持久化操作的,也就是在 IdentityServer4 服務器上需要配置的一些數據存儲到數據庫中永久存儲.
在 IdentityServer4 服務器上,配置的數據有:客戶端、API 作用域等,之前我們都是存儲在內存中進行操作,這裏,我們將這些數據存儲到數據庫中。IdentityServer4 支持的持久存儲體有 Redis、SQLServer、
Oracle、MySql 等。

注意:這裏永久存儲的數據不包括用戶信息,用戶信息需要使用ASP.NET Core Identity 認證框架來實現。

創建 IdentityServer 項目

打開 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: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

生成初始化數據

在 Program 類中找到如下代碼,

//同步數據
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, 修改方法爲編輯項目文件
裏面的 net7.0
改爲 net6.0

集成代碼

集成 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。"),
                new ApiScope("ERPAPI","ERP平臺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, "[email protected]"),
                            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();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章