eShopOnContainers 看微服務③:Identity Service OAuth 2.0 簡介 OpenID Connect 簡介 Identity Server 4 eShopOnContainers 知多少[3]:Identity microservice
引言
通常,服務所公開的資源和 API 必須僅限受信任的特定使用者和客戶端訪問。那進行 API 級別信任決策的第一步就是身份認證——確定使用者身份是否可靠。
在微服務場景中,身份認證通常統一處理。一般有兩種實現形式:
-
基於API 閘道器中心化認證:要求客戶端必須都通過閘道器訪問微服務。(這就要求提供一種安全機制來認證請求是來自於閘道器。)
-
基於安全令牌服務(STS)認證:所有的客戶端先從STS獲取令牌,然後請求時攜帶令牌完成認證。
Identity Service就是使用第二種身份認證方式。
服務簡介
Identity microservice 主要用於統一的身份認證和授權,為其他服務提供支撐。
提到認證,大家最熟悉不過的當屬Cookie認證了,它也是目前使用最多的認證方式。但Cookie認證也有其侷限性:不支援跨域、移動端不友好等。而從當前的架構來看,需要支援移動端、Web端、微服務間的交叉認證授權,所以傳統的基於Cookie的本地認證方案就行不通了。我們就需要使用遠端認證的方式來提供統一的認證授權機制。
而遠端認證方式當屬:OAuth2.0和OpenID Connect了。藉助OAuth2.0和OpenID Connect即可實現類似下圖的認證體系:
而如何實現呢,藉助:
- ASP.NET Core Identity
- IdentityServer4
基於Cookie的認證和基於Token的認證的差別如下所示:
架構模式
從目錄結構可以看出它是一套MVC單層架構的網站。我們可以單獨進行執行和除錯,也可以把它放進自己的專案中。
主要依賴:
1、HealthCheck 健康檢查
2、WebHost
3、Entity Framework
4、Autofac
6、其中IdentityServer4.AspNetIdentity又用到了ASP.NET Core Identity
啟動流程
Program.cs
Main函式:
1 public static void Main(string[] args) 2 { 3 BuildWebHost(args) 4 .MigrateDbContext<PersistedGrantDbContext>((_, __) => { }) 5 .MigrateDbContext<ApplicationDbContext>((context, services) => 6 { 7 var env = services.GetService<IHostingEnvironment>(); 8 var logger = services.GetService<ILogger<ApplicationDbContextSeed>>(); 9 var settings = services.GetService<IOptions<AppSettings>>(); 10 11 new ApplicationDbContextSeed() 12 .SeedAsync(context, env, logger, settings)//初始化預設登入使用者種子資料 13 .Wait(); 14 }) 15 .MigrateDbContext<ConfigurationDbContext>((context, services) => 16 { 17 var configuration = services.GetService<IConfiguration>(); 18 19 new ConfigurationDbContextSeed() 20 .SeedAsync(context, configuration)//初始化identity server種子資料 21 .Wait(); 22 }).Run(); 23 }View Code
BuildWebHost函式:
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseKestrel()//使用Kestrel作為的web伺服器 .UseHealthChecks("/hc")//健康檢查 .UseContentRoot(Directory.GetCurrentDirectory())//將當前專案的根目錄作為ContentRoot目錄 .UseIISIntegration()//使用IIS .UseStartup<Startup>()//使用startup類 .ConfigureAppConfiguration((builderContext, config) => { var builtConfig = config.Build(); var configurationBuilder = new ConfigurationBuilder(); if (Convert.ToBoolean(builtConfig["UseVault"])) { configurationBuilder.AddAzureKeyVault( $"https://{builtConfig["Vault:Name"]}.vault.azure.net/", builtConfig["Vault:ClientId"], builtConfig["Vault:ClientSecret"]); } configurationBuilder.AddEnvironmentVariables(); config.AddConfiguration(configurationBuilder.Build()); }) .ConfigureLogging((hostingContext, builder) => { builder.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); builder.AddConsole(); builder.AddDebug(); }) .UseApplicationInsights() .Build();
其中有一個UseHealthChecks,這是一個對專案健康的檢查。
健康檢查,其實這個名稱已經很明確了,它是檢查你的應用程式是否健康執行的一種方式。隨著當前各類專案越來越多的應用程式正在轉向微
服務式架構,健康檢查就變得尤為關鍵。雖然微服務體系結構具有許多好處,但其中一個缺點就是為了確保所有這些服務都正常執行的操作開銷
更高。你不在是監視一個龐大的整體專案的健康狀況,而是需要監控許多不同服務的狀態,甚至這些服務通常只負責一件事情。健康檢查(Heatlh
Checks)通常與一些服務發現工具結合使用,如Consul ,來監控您的微伺服器,來觀測您的服務是否健康執行。
健康檢查有很多種不同的方法,但最常見的方法是將HTTP端點暴露給專門用於健康檢查的應用程式。一般來說,如果一切情況都很好,你的服
務將返回200的狀態碼,然而任何非200的程式碼則意味著出現問題。例如,如果發生錯誤,你可能會返回500以及一些出錯的JSON資訊。
Startup.cs
1 public class Startup 2 { 3 public Startup(IConfiguration configuration) 4 { 5 Configuration = configuration; 6 } 7 8 public IConfiguration Configuration { get; } 9 10 /// <summary> 11 /// 來配置我們應用程式中的各種服務, 12 /// 它通過引數獲取一個IServiceCollection 例項 。 13 /// </summary> 14 /// <param name="services"></param> 15 /// <returns>IServiceProvider</returns> 16 public IServiceProvider ConfigureServices(IServiceCollection services) 17 { 18 RegisterAppInsights(services); 19 20 // Add framework services. 21 //註冊EF使用的DbContext 22 services.AddDbContext<ApplicationDbContext>(options => 23 //使用mysql 24 options.UseSqlServer(Configuration["ConnectionString"],//資料庫連線字串 25 sqlServerOptionsAction: sqlOptions => 26 { 27 sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); 28 //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 29 sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); 30 })); 31 //使用Microsoft asp.net identity系統 32 services.AddIdentity<ApplicationUser, IdentityRole>() 33 .AddEntityFrameworkStores<ApplicationDbContext>()//使用EF 34 .AddDefaultTokenProviders(); 35 36 services.Configure<AppSettings>(Configuration); 37 38 services.AddMvc();//使用MVC 39 40 //對叢集對配置 41 if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString) 42 { 43 services.AddDataProtection(opts => 44 { 45 //在叢集環境中,如果不被具體的硬體機器環境所限制,就要排除執行機器的一些差異, 46 //就需要抽象出來一些特定的標識,來標識應用程式本身並且使用該標識來區分不同的應用程式。 47 //這個時候,我們可以指定ApplicationDiscriminator。 48 opts.ApplicationDiscriminator = "eshop.identity"; 49 //叢集環境下同一應用程式他們需要設定為相同的值(ApplicationName or ApplicationDiscriminator)。 50 }) 51 .PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys"); 52 } 53 54 //註冊健康檢查 55 services.AddHealthChecks(checks => 56 { 57 var minutes = 1; 58 if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) 59 { 60 minutes = minutesParsed; 61 } 62 //資料庫健康檢查 63 checks.AddSqlCheck("Identity_Db", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); 64 }); 65 66 //註冊登陸註冊的應用服務(ApplicationService) 67 services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>(); 68 services.AddTransient<IRedirectService, RedirectService>(); 69 70 var connectionString = Configuration["ConnectionString"]; 71 var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; 72 73 // 註冊 IdentityServer 74 services.AddIdentityServer(x => 75 { 76 x.IssuerUri = "null"; 77 x.Authentication.CookieLifetime = TimeSpan.FromHours(2);//cookie有效期兩小時 78 }) 79 .AddSigningCredential(Certificate.Get())//設定加密證書 80 //配置IdentityServer。IUserClaimsPrincipalFactory、IResourceOwnerPasswordValidator和IProfileService的網路標識實現。 81 .AddAspNetIdentity<ApplicationUser>() 82 .AddConfigurationStore(options => //使用IdentityServer配置IClientStore、IResourceStore和ICorsPolicyService的EF實現。 83 { 84 options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, 85 sqlServerOptionsAction: sqlOptions => 86 { 87 sqlOptions.MigrationsAssembly(migrationsAssembly); 88 //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 89 sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); 90 }); 91 }) 92 //註冊IPersistedGrantStore的實現,用於儲存AuthorizationCode和RefreshToken等等,預設實現是儲存在記憶體中, 93 //如果服務重啟那麼這些資料就會被清空了,因此實現IPersistedGrantStore將這些資料寫入到資料庫中 94 .AddOperationalStore(options => 95 { 96 options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, 97 sqlServerOptionsAction: sqlOptions => 98 { 99 sqlOptions.MigrationsAssembly(migrationsAssembly); 100 //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency 101 sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); 102 }); 103 }) 104 //註冊IProfileService,該介面允許IdentityServer連線到使用者。 105 .Services.AddTransient<IProfileService, ProfileService>(); 106 107 //使用autofac 108 var container = new ContainerBuilder(); 109 container.Populate(services); 110 111 return new AutofacServiceProvider(container.Build()); 112 } 113 114 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 115 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 116 { 117 //配置日誌 118 loggerFactory.AddConsole(Configuration.GetSection("Logging")); 119 loggerFactory.AddDebug(); 120 loggerFactory.AddAzureWebAppDiagnostics(); 121 loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); 122 123 if (env.IsDevelopment()) 124 { 125 app.UseDeveloperExceptionPage(); 126 app.UseDatabaseErrorPage(); 127 } 128 else 129 { 130 app.UseExceptionHandler("/Home/Error"); 131 } 132 133 var pathBase = Configuration["PATH_BASE"]; 134 if (!string.IsNullOrEmpty(pathBase)) 135 { 136 loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); 137 app.UsePathBase(pathBase); 138 } 139 140 141 #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 142 app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); 143 #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously 144 145 //使用StaticFiles,等於啟動了靜態檔案伺服器功能。wwwroot 就是靠這個中介軟體讀取的。 146 //也可以不使用wwwroot,並且制定自己對目錄。傳入引數就可以了。 147 app.UseStaticFiles(); 148 149 150 // Make work identity server redirections in Edge and lastest versions of browers. WARN: Not valid in a production environment. 151 app.Use(async (context, next) => 152 { 153 context.Response.Headers.Add("Content-Security-Policy", "script-src 'unsafe-inline'"); 154 await next(); 155 }); 156 157 //處理代理伺服器和負載均衡對解決方案, 158 //詳情 https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.1 159 app.UseForwardedHeaders(); 160 //使用IdentityServer4 161 app.UseIdentityServer(); 162 163 //配置MVC 164 app.UseMvc(routes => 165 { 166 routes.MapRoute( 167 name: "default", 168 template: "{controller=Home}/{action=Index}/{id?}"); 169 }); 170 } 171 172 /// <summary> 173 /// 應用監控 174 /// </summary> 175 /// <param name="services"></param> 176 private void RegisterAppInsights(IServiceCollection services) 177 { 178 services.AddApplicationInsightsTelemetry(Configuration); 179 var orchestratorType = Configuration.GetValue<string>("OrchestratorType"); 180 181 if (orchestratorType?.ToUpper() == "K8S") 182 { 183 // Enable K8s telemetry initializer 184 services.EnableKubernetes(); 185 } 186 if (orchestratorType?.ToUpper() == "SF") 187 { 188 // Enable SF telemetry initializer 189 services.AddSingleton<ITelemetryInitializer>((serviceProvider) => 190 new FabricTelemetryInitializer()); 191 } 192 } 193 }View Code
ASP.NET Core Identity && IdentityServer4簡介
ASP.NET Core Identity用於構建ASP.NET Core Web應用程式的成員資格系統,包括成員資格,登入和使用者資料(包括登入資訊、角色和宣告)。
ASP.NET Core Identity封裝了User、Role、Claim等身份資訊,便於我們快速完成登入功能的實現,並且支援第三方登入(Google、Facebook、QQ、Weixin等,支援開箱即用[第三方身份提供商列表]),以及雙重驗證,同時內建支援Bearer 認證(令牌認證)。
雖然ASP.NET Core Identity已經完成了絕大多數的功能,且支援第三方登入(第三方為其使用者頒發令牌),但若要為本地使用者頒發令牌,則需要自己實現令牌的頒發和驗證邏輯。換句話說,我們需要自行實現OpenId Connect協議。
OpenID Connect 1.0 是基於OAuth 2.0協議之上的簡單身份層,它允許客戶端根據授權伺服器的認證結果最終確認終端使用者的身份,以及獲取基本的使用者資訊。
而IdentityServer4就是為ASP.NET Core量身定製的實現了OpenId Connect和OAuth2.0協議的認證授權中介軟體。IdentityServer4在ASP.NET Core Identity的基礎上,提供令牌的頒發驗證等。
相關知識:
認證流程
在ASP.NET Core中使用的是基於申明(Claim)的認證,而什麼是申明(Cliam)呢?
Claim 是關於一個人或組織的某個主題的陳述,比如:一個人的名稱,角色,個人喜好,種族,特權,社團,能力等等。它本質上就是一個鍵值對,是一種非常通用的儲存使用者資訊的方式,可以很容易的將認證和授權分離開來,前者用來表示使用者是/不是什麼,後者用來表示使用者能/不能做什麼。在認證階段我們通過使用者資訊獲取到使用者的Claims,而授權便是對這些的Claims的驗證,如:是否擁有Admin的角色,姓名是否叫XXX等等。
認證主要與以下幾個核心物件打交道:
- Claim(身份資訊)
- ClaimsIdentity(身份證)
- ClaimsPrincipal (身份證持有者)
- AuthorizationToken (授權令牌)
- IAuthenticationScheme(認證方案)
- IAuthenticationHandler(與認證方案對應的認證處理器)
- IAuthenticationService (向外提供統一的認證服務介面)
那其認證流程是怎樣的呢?
1、使用者開啟登入介面,輸入使用者名稱密碼先行登入,服務端先行校驗使用者名稱密碼是否有效,有效則返回使用者例項(User)。
2、這時進入認證準備階段,根據使用者例項攜帶的身份資訊(Claim),建立身份證(ClaimsIdentity),然後將身份證交給身份證持有者(ClaimsPrincipal)持有。
3、接下來進入真正的認證階段,根據配置的認證方案(IAuthenticationScheme),使用相對應的認證處理器(IAuthenticationHandler)進行認證 。認證成功後發放授權令牌(AuthorizationToken)。該授權令牌包含後續授權階段需要的全部資訊。
授權流程
授權就是對於使用者身份資訊(Claims)的驗證,,授權又分以下幾種種:
- 基於Role的授權
- 基於Scheme的授權
- 基於Policy的授權
授權主要與以下幾個核心物件打交道:
- IAuthorizationRequirement(授權條件)
- IAuthorizationService(授權服務)
- AuthorizationPolicy(授權策略)
- IAuthorizationHandler (授權處理器)
- AuthorizationResult(授權結果)
那授權流程是怎樣的呢?
當收到授權請求後,由授權服務(IAuthorizationService)根據資源上指定的授權策略(AuthorizationPolicy)中包含的授權條件(IAuthorizationRequirement),找到相對應的授權處理器(IAuthorizationHandler )來判斷授權令牌中包含的身份資訊是否滿足授權條件,並返回授權結果。
中介軟體整合
回過頭來我們再來刷一遍startup程式碼中是怎麼整合進Identity service的。
1. 首先是對映自定義擴充套件的User和Role
// 對映自定義的User,Role services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>()//配置使用EF持久化儲存 .AddDefaultTokenProviders();//配置預設的TokenProvider用於變更密碼和修改email時生成Token
2. 配置IdentityServer服務
services.AddIdentityServer(x => {
... }) .AddSigningCredential(Certificate.Get()) .AddAspNetIdentity<ApplicationUser>() .AddConfigurationStore(options => { ... }) .AddOperationalStore(options => { ... }) .Services.AddTransient<IProfileService, ProfileService>();
使用AddConfigurationStore
和AddOperationalStore
擴充套件方法就是用來來指定配置資料和操作資料基於EF進行持久化。
3. 新增IdentityServer中介軟體
app.UseIdentityServer();
4. 預置種子資料
需要預置Client和Resource寫在Config.cs檔案中,他們又是中main函式中被MigrateDbContext使用的。
- GetClients
public static IEnumerable<Client> GetClients(Dictionary<string,string> clientsUrl) { return new List<Client> {
//通過不同對ClientId設定不同客戶端引數 new Client ... ... new Client }; }
- IdentityResources
身份資源是使用者ID、姓名或電子郵件地址等資料
public static IEnumerable<IdentityResource> GetResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile() }; }
- ApiResources
public static IEnumerable<ApiResource> GetApis() { return new List<ApiResource> { new ApiResource("orders", "Orders Service"), new ApiResource("basket", "Basket Service"), new ApiResource("marketing", "Marketing Service"), new ApiResource("locations", "Locations Service"), new ApiResource("mobileshoppingagg", "Mobile Shopping Aggregator"), new ApiResource("webshoppingagg", "Web Shopping Aggregator"), new ApiResource("orders.signalrhub", "Ordering Signalr Hub") }; }
5. 遷移資料庫上下文
IdentityServer為配置資料和操作資料分別定義了DBContext
用於持久化,配置資料對應ConfigurationDbContext
,操作資料對應PersistedGrantDbContext
。詳細看main函式。
這篇文章使用了園子裡『___知多少』文章對不少內容,表示感謝,原文連結eShopOnContainers 知多少[3]:Identity microservice。