IdentityServer4與ocelot實現認證與客戶端統一入口
關於IdentityServer4與ocelot部落格園裡已經有很多介紹我這裡就不再重複了。
ocelot與IdentityServer4組合認證部落格園裡也有很多,但大多使用ocelot內建的認證,而且大多都是用來認證API的,查找了很多資料也沒看到如何認證oidc,所以這裡的ocelot實際只是作為統一入口而不參與認證,認證的完成依然在客戶端。程式碼是使用IdentityServer4的Quickstart5_HybridAndApi 示例修改的。專案結構如下
一 ocelot閘道器
我們先在示例新增一個閘道器。
修改launchSettings.json中的埠為54660
"NanoFabricApplication": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:54660", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }
配置檔案如下
{View Code"ReRoutes": [ { // MvcClient "DownstreamPathTemplate": "/MvcClient/{route}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50891 } ], "UpstreamPathTemplate": "/MvcClient/{route}","UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } }, { // signin-oidc "DownstreamPathTemplate": "/signin-oidc", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50891 } ], "UpstreamPathTemplate": "/signin-oidc", "UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } }, { // signout-callback-oidc "DownstreamPathTemplate": "/signout-callback-oidc", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50891 } ], "UpstreamPathTemplate": "/signout-callback-oidc", "UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } }, { // MyApi "DownstreamPathTemplate": "/MyApi/{route}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50890 } ], "UpstreamPathTemplate": "/MyApi/{route}", "UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } }, { // IdentityServer "DownstreamPathTemplate": "/{route}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50875 } ], "UpstreamPathTemplate": "/IdentityServer/{route}", "UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } }, { // IdentityServer "DownstreamPathTemplate": "/{route}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 50875 } ], "UpstreamPathTemplate": "/{route}", "UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } } ] }
這裡我們定義3個下游服務,MvcClient,MyApi,IdentityServer,並使用路由特性把signin-oidc,signout-callback-oidc導航到MvcClient,由MvcClient負責生成最後的Cooike。並將預設路由指定到IdentityServer服務。
在ConfigureServices中新增Ocelot服務。
services.AddOcelot() .AddCacheManager(x => { x.WithDictionaryHandle(); }) .AddPolly()
在Configure中使用Ocelot中介軟體
app.UseOcelot().Wait();
Ocelot閘道器就部署完成了。
二 修改QuickstartIdentityServer配置
首先依然是修改launchSettings.json中的埠為50875
在ConfigureServices中修改AddIdentityServer配置中的PublicOrigin和IssuerUri的Url為http://localhost:54660/IdentityServer/
services.AddIdentityServer(Option => { Option.PublicOrigin = "http://localhost:54660/IdentityServer/"; Option.IssuerUri = "http://localhost:54660/IdentityServer/"; }) .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers());
這樣一來發現文件中的IdentityServer地址就變為閘道器的地址了,進一步實現IdentityServer的負載均衡也是沒有問題的。
修改Config.cs中mvc客戶端配置如下
ClientId = "mvc", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, // AccessTokenType = AccessTokenType.Reference, RequireConsent = true, RedirectUris = { "http://localhost:54660/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:54660/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "api1" }, AllowOfflineAccess = true, //直接返回客戶端需要的Claims AlwaysIncludeUserClaimsInIdToken = true,
主要修改RedirectUris和PostLogoutRedirectUris為閘道器地址,在閘道器也設定了signin-oidc和signout-callback-oidc轉發請求到Mvc客戶端。
三 修改MvcClient
修改MvcClient的launchSettings.json埠為50891。
修改MvcClient的Authority地址為http://localhost:54660/IdentityServer和預設路由地址MvcClient/{controller=Home}/{action=index}/{id?}
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; //options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie("Cookies",options=> { options.ExpireTimeSpan = TimeSpan.FromMinutes(30); options.SlidingExpiration = true; }) .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "http://localhost:54660/IdentityServer"; options.RequireHttpsMetadata = false; options.ClientId = "mvc"; options.ClientSecret = "secret"; options.ResponseType = "code id_token"; options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Add("api1"); options.Scope.Add("offline_access"); });View Code
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "MvcClient/{controller=Home}/{action=index}/{id?}"); });View Code
修改HomeController,將相關地址修改為閘道器地址
public async Task<IActionResult> CallApiUsingClientCredentials() { var tokenClient = new TokenClient("http://localhost:54660/IdentityServer/connect/token", "mvc", "secret"); var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1"); var client = new HttpClient(); client.SetBearerToken(tokenResponse.AccessToken); var content = await client.GetStringAsync("http://localhost:54660/MyApi/identity"); ViewBag.Json = JArray.Parse(content).ToString(); return View("json"); } public async Task<IActionResult> CallApiUsingUserAccessToken() { var accessToken = await HttpContext.GetTokenAsync("access_token"); //OpenIdConnectParameterNames var client = new HttpClient(); client.SetBearerToken(accessToken); var content = await client.GetStringAsync("http://localhost:54660/MyApi/identity"); ViewBag.Json = JArray.Parse(content).ToString(); return View("json"); }View Code
四 修改Api專案
Api專案修改多一點。
將MvcClient的HomeController和相關檢視複製過來,模擬MVC與API同時存在的專案。
修改Api的launchSettings.json埠為50890。
修改Startup
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddDataProtection(options => options.ApplicationDiscriminator = "00000").SetApplicationName("00000"); services.AddMvc(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }).AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = "http://localhost:54660/IdentityServer"; options.RequireHttpsMetadata = false; options.ClientId = "mvc"; options.ClientSecret = "secret"; options.ResponseType = "code id_token"; options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Add("api1"); options.Scope.Add("offline_access"); }) .AddIdentityServerAuthentication("Bearer", options => { options.Authority = "http://localhost:54660/IdentityServer"; options.RequireHttpsMetadata = false; options.ApiSecret = "secret123"; options.ApiName = "api1"; options.SupportedTokens= SupportedTokens.Both; }); services.AddAuthorization(option => { //預設 只寫 [Authorize],表示使用oidc進行認證 option.DefaultPolicy = new AuthorizationPolicyBuilder("oidc").RequireAuthenticatedUser().Build(); //ApiController使用這個 [Authorize(Policy = "ApiPolicy")],使用jwt認證方案 option.AddPolicy("ApiPolicy", policy => { policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); }); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { //var options = new ForwardedHeadersOptions //{ // ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, // ForwardLimit = 1 //}; //options.KnownNetworks.Clear(); //options.KnownProxies.Clear(); //app.UseForwardedHeaders(options); //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); //} //else //{ // app.UseExceptionHandler("/Home/Error"); //} app.UseAuthentication(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "MyApi/{controller=MAccount}/{action=Login}/{id?}"); }); } }View Code
主要添加了oidc認證配置和配置驗證策略來同時支援oidc認證和Bearer認證。
修改IdentityController中的[Authorize]特性為[Authorize(Policy = "ApiPolicy")]
依次使用除錯-開始執行(不除錯)並選擇專案名稱啟動QuickstartIdentityServer,Gateway,MvcClient,Api,啟動方式如圖
應該可以看到Gateway啟動後直接顯示了IdentityServer的預設首頁
在瀏覽器輸入http://localhost:54660/MVCClient/Home/index進入MVCClient
點選Secure進入需要授權的頁面,這時候會跳轉到登陸頁面(才怪
實際上我們會遇到一個錯誤,這是因為ocelot做閘道器時下游服務獲取到的Host實際為localhost:50891,而在IdentityServer中設定的RedirectUris為閘道器的54660,我們可以通過ocelot轉發X-Forwarded-Host頭,並在客戶端通過UseForwardedHeaders中介軟體來獲取頭。但是UseForwardedHeaders中介軟體為了防止IP欺騙攻擊需要設定KnownNetworks和KnownProxies以實現嚴格匹配。當然也可以通過清空KnownNetworks和KnownProxies的預設值來不執行嚴格匹配,這樣一來就有可能受到攻擊。所以這裡我直接使用硬編碼的方式設定Host,實際使用時應從配置檔案獲取,同時修改MvcClient和Api相關程式碼
app.Use(async (context, next) => { context.Request.Host = HostString.FromUriComponent(new Uri("http://localhost:54660/")); await next.Invoke(); }); //var options = new ForwardedHeadersOptions //{ // ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, // ForwardLimit = 1 //}; //options.KnownNetworks.Clear(); //options.KnownProxies.Clear(); //app.UseForwardedHeaders(options);
在反向代理情況下通過轉發X-Forwarded-Host頭來獲取Host地址應該時常見設定不知道還有沒有其他更好的解決辦法。
再次啟動MVCClient並輸入http://localhost:54660/MvcClient/Home/Secure。
使用bob,password登陸一下
點選Yes, Allow返回http://localhost:54660/MvcClient/Home/Secure,此時可以檢視到登陸後的資訊
分別點選Call API using user token和Call API using application identity來驗證一下通過access_token和ClientCredent模式請求來請求API
成功獲取到返回值。
輸入http://localhost:54660/myapi/Home/index來檢視API情況
請求成功。
點選Secure從API專案檢視使用者資訊,此時展示資訊應該和MvcClient一致
嗯,並沒有看到使用者資訊而是又到了授權頁.....,這是因為.netCore使用DataProtection來保護資料(點選檢視詳細資訊),Api專案不能解析由MvcClient生成的Cookie,而被重定向到了IdentityServer服務中。
在MvcClient和Api的ConfigureServices下新增如下程式碼來同步金鑰環。
services.AddDataProtection(options => options.ApplicationDiscriminator = "00000").SetApplicationName("00000");
再次啟動MvcClient和Api專案並在瀏覽器中輸入http://localhost:54660/MvcClient/home/Secure,此時被要求重新授權,點選Yes, Allow後看到使用者資訊
再輸入http://localhost:54660/myapi/Home/Secure從API專案檢視使用者資訊
分別點選Call API using user token和Call API using application identity來驗證一下通過access_token和ClientCredent模式請求來請求API
請求成功。
如此我們便實現了通過ocelot實現統一入口,通過IdentityServer4來實現認證的需求
原始碼 https://github.com/saber-wang/Quickstart5_HybridAndApi
參考
https://www.cnblogs.com/stulzq/category/1060023.html
https://www.cnblogs.com/xiaoti/p/10118930.html
https://www.cnblogs.com/jackcao/tag/identityserver4/