Tip: 此篇已加入.NET Core微服務基礎系列文章索引
上一篇我們基於IdentityServer4建立了一個AuthorizationServer,並且繼承了QuickStartUI,能夠成功獲取Token了。這一篇我們了解下如何集成API Service和MVC Web Application。
一、集成API Service
1.1 添加ASP.NET Core WebAPI項目
新建兩個WebAPI程序,假設這里取名為ApiService01(占用端口5010)和ApiService02(占用端口5020)。
為了方便快速啟動,可以繼續在launchSettings.json中刪掉關於IISExpress的部分,由於是WebAPI,所以也不需要啟動瀏覽器(將其設為false):
{ "profiles": { "Manulife.DNC.MSAD.IdentityServer4Test.ApiService01": { "commandName": "Project", "launchBrowser": false, "launchUrl": "api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:5010/" } } }
1.2 安裝IdentityServer4.AccessTokenValidation
NuGet>Install-Package IdentityServer4.AccessTokenValidation
安裝完成之后,需要做一些適配,所以我們來注冊一下關於其的配置:這里設置的默認模式是Bearer,其中AddIdentityServerAuthentication方法是將我們上一篇創建的授權服務器注冊為token的處理人,即在本API程序中涉及到token的處理,都會移交給指定服務器(這里即上一篇中創建的AuthorizationServer)進行處理。
public void ConfigureServices(IServiceCollection services) { ...... // IdentityServer services.AddMvcCore().AddAuthorization().AddJsonFormatters(); services.AddAuthentication(Configuration["Identity:Scheme"]) .AddIdentityServerAuthentication(options => { options.RequireHttpsMetadata = false; // for dev env options.Authority = $"http://{Configuration["Identity:IP"]}:{Configuration["Identity:Port"]}"; options.ApiName = Configuration["Service:Name"]; // match with configuration in IdentityServer }); // Swagger ...... }
配置文件中的內容如下所示:這里需要注意的是Name需要和我們在上一篇中的InMemoryConfiguration中自行hard-code的一樣。

{ "Service": { "Name": "clientservice", "Port": "5010", "DocName": "clientservice", "Version": "v1", "Title": "CAS Client Service API", "Description": "CAS Client Service API provide some API to help you get client information from CAS", "Contact": { "Name": "CAS 2.0 Team", "Email": "EdisonZhou@manulife.com" }, "XmlFile": "Manulife.DNC.MSAD.IdentityServer4Test.ApiService01.xml" }, "Identity": { "IP": "localhost", "Port": "5000", "Scheme": "Bearer" } }
此外,還需要在Configure方法中配置Authentication中間件:這里需要注意的就是需要放在UseMvc()之前調用。
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // authentication app.UseAuthentication(); app.UseMvc(); // swagger ....... }
以上是ApiService01的配置,ApiService02的配置類似,只是配置文件中的信息從clientservice改為了productservice。
1.3 為要進行驗證授權的方法添加[Authorize]特性
由於我們創建WebAPI時,默認有一個ValuesController,保留它,我們直接為這個Controller添加一個[Authorize]特性。
[Authorize] [Route("api/[controller]")] public class ValuesController : Controller { ...... }
這樣的話,剛剛注冊的中間件就會在請求的過程中基於傳遞過來的token進行Authorization,如果沒有token或者token是非法的,它就會告訴api的消費者這個請求時未授權的(HTTP StatusCode 401)
1.4 簡單測試一下
測試之前首先確保AuthorizationServer和兩個ApiService都一起啟動,可以在解決方案屬性的啟動選項中進行設置。
(1)不帶token的情況
(2)帶正確token的情況
首先請求獲取一下token:這里我們使用的grant_type是client_credentials,也可以使用password(需要輸入用戶名和密碼)。
帶上這個token再去調用api service
(3)帶不正確的token的情況(這里簡單改一下token的值)
(4)用剛剛授予(clientservice)的token訪問未經授權的productservice
我們也可以通過在ValuesController中加上如下的一句,來獲取Claims => 它是從已驗證過的token中抽取的信息。如果我們將上面獲取的token的grant_type設置為password的話,那我們會在Claims中獲取到用戶ID的信息。
二、集成MVC Web Application
2.1 添加ASP.NET Core MVC項目
新建一個ASP.NET Core MVC項目,這里假設取名為MvcClient(占用端口5100)
仍然修改launchSettings.json,刪掉IISExpress部分,但仍然選擇啟動瀏覽器:
{ "profiles": { "Manulife.DNC.MSAD.IdentityServer4Test.MvcClient": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:5100/" } } }
2.2 為指定方法添加[Authorize]特性
這里我們在HomeController的About方法上面添加[Authorize]特性:
[Authorize] public IActionResult About() { ViewData["Message"] = "Your application description page."; return View(); }
這時如果我們直接訪問About,會拋異常,告知我們No authenticationScheme was specified......
而我們想要的效果是當User第一次點擊About,頁面重定向到AuthorizationServer (localhost:5000),當用戶登錄驗證授權之后,再重定向到該網站。此后一定時間范圍內的第二次,第三次點擊,都不再需要再重定向到AuthorizationServer,而是直接讀取保存的token。
2.3 添加OpenID Connect Authentication
這一部分主要集中於做Authentication(身份驗證)而非Authorization(授權)。
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // ensure not change any return Claims from Authorization Server services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; // oidc => open ID connect }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; options.Authority = $"http://{Configuration["Identity:IP"]}:{Configuration["Identity:Port"]}"; options.RequireHttpsMetadata = false; // please use https in production env options.ClientId = "cas.mvc.client.implicit"; options.ResponseType = "id_token token"; // allow to return access token options.SaveTokens = true; }); }
這里我們使用的是implicit這個flow(詳細內容可以閱讀ddrsql的IdentityServer4之Implicit(隱式許可)),它主要用於客戶端應用程序(主要指基於javascript的應用),它允許客戶端程序重定向到AuthorizationServer,然后帶着token重定向回來。值得一提的是這里的ResponseType為"id_token token",表示既獲取id_token也獲取access_token。而SaveTokens設為true則表示要將從AuthorizationServer返回的token持久化在cookie之中,這樣就不用每次都去請求token了。
當然,也還得在Configure方法中,配置Authentication中間件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseBrowserLink(); app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseAuthentication(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
*.位置仍然需要在UseMvc之前。
2.4 在AuthorizationServer添加這個MvcClient
在InMemoryConfiguration類中修改GetClients方法:
public static IEnumerable<Client> GetClients() { return new[] { new Client { ClientId = "client.api.service", ClientSecrets = new [] { new Secret("clientsecret".Sha256()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, AllowedScopes = new [] { "clientservice" } }, ......, new Client { ClientId = "cas.mvc.client.implicit", ClientName = "CAS MVC Web App Client", AllowedGrantTypes = GrantTypes.Implicit, RedirectUris = { $"http://{Configuration["Clients:MvcClient:IP"]}:{Configuration["Clients:MvcClient:Port"]}/signin-oidc" }, PostLogoutRedirectUris = { $"http://{Configuration["Clients:MvcClient:IP"]}:{Configuration["Clients:MvcClient:Port"]}/signout-callback-oidc" }, AllowedScopes = new [] { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "agentservice", "clientservice", "productservice" }, AllowAccessTokensViaBrowser = true // can return access_token to this client } }; }
這里的ClientId要和MvcClient中設置的一致。RedirectUris是指登錄成功之后需要重定向的地址(這里這個位置在MvcClient中),而PostLogoutRedirectUris是指登出之后需要重定向的地址。和API Service Client的設置不同的就是在AllowedScopes中給它增加了OpenId和Profile,因為我們為MvcClient設定的是oidc而不是bearer的模式。最后為了使用這些OpenID Connect Scopes,需要設置這些Identity Resources:
public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; } public void ConfigureServices(IServiceCollection services) { ....... services.AddIdentityServer() //.AddDeveloperSigningCredential() .AddSigningCredential(new X509Certificate2(Path.Combine(basePath, Configuration["Certificates:CerPath"]), Configuration["Certificates:Password"])) .AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources()) .AddTestUsers(InMemoryConfiguration.GetUsers().ToList()) .AddInMemoryClients(InMemoryConfiguration.GetClients()) .AddInMemoryApiResources(InMemoryConfiguration.GetApiResources()); ...... }
同時,為了演示方便,我們在MvcClient的About視圖中添加幾句:
@{ ViewData["Title"] = "About"; } <h2>@ViewData["Title"]</h2> <h3>@ViewData["Message"]</h3> @using Microsoft.AspNetCore.Authentication <div> <strong>id_token</strong> <span>@await ViewContext.HttpContext.GetTokenAsync("id_token")</span> </div> <div> <strong>access_token</strong> <span>@await ViewContext.HttpContext.GetTokenAsync("access_token")</span> </div> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl>
這里我們將id_token, access_token(只有拿到access_token,才可以在MvcClient中發起請求調用API Service,而具體可以訪問哪些API Service是在InMemoryConfiguration中配置的AllowScopes里面的那些)都顯示出來,還會將Claims數據也顯示出來(這里的Claims數據就是從AuthorizationServer返回的token里面payload部分的數據,關於payload請搜索JWT)
2.5 簡單測試一下
(1)未登陸的情況
拿到access_token,可以去JWT.io上進行解析看看:比如關注payload部分,可以得到很多信息,比如過期時間為1小時(默認)
(2)已登錄的情況
這里為了方便演示,新增一個Logout方法:這里需要確保同時登出MvcClient的Cookies和OpenId Connect(即到Identity Server去清除單點登錄的Session)。
public class HomeController: Controller { ...... public async Task Logout() { await HttpContext.SignOutAsync("Cookies"); await HttpContext.SignOutAsync("oidc"); } ...... }
最后,關於access token的生命周期,可以閱讀一下園友曉晨Master(李志強)的《IdentityServer4實戰 - AccessToken 生命周期分析》,里面提到一個時間偏移的概念,需要了解一下。另外,如果是前后端分離的結構,也可以瀏覽一下ddrsql的《IdentityServer4之Implicit(隱式許可) —— oidc-client-js前后端分離》,里面介紹了oidc-client這個JS庫的使用,以及如何支持跨域。
三、小結
本篇基於上一篇搭建好的AuthorizationServer,通過集成API Service與MVC Web Application來演示他們如何與Authorization Server的交互,了解了兩種不同的Scheme(Bearer和Implicit),最后補充了一些材料供擴展閱讀。但是,IdentityServer的內容很多,我只是學習了一些我要掌握以做POC的部分,其他還有很多功能和知識點我沒有學習,大家可以通過搜索園子里各種大神(eg.曉晨Master, solenovex等等等等)的文章進行學習。后面我會將IdentityServer與Ocelot進行集成,嘗試在API網關處做統一驗證與授權。最后,感謝參考資料的作者們,本篇主要基於參考資料的學習而成的筆記。另外,一些朋友十分想要一些基於生產環境的采坑經驗以及更加實際的應用案例,對此我只能說聲抱歉,我目前仍然處於學習與准備POC階段,目的是為了在公司里推廣以及給老板安利(雖然很難),還沒有實際項目遷移到.NET Core微服務架構上去,但是如果不努力讓公司遷移和應用,那像我司一樣的傳統行業的金融保險公司是不會主動升級的,還請各位諒解。
示例代碼
Click => https://github.com/EdisonChou/EDC.IdentityServer4.Demo
參考資料
《identityserver4官方文檔》=> 重點關注那些流程圖與術語
ddrsql,《IdentityServer4之Implicit(隱式許可)》
solenovex,《學習Identity Server4的預備知識》
solenovex,《使用Identity Server 4建立Authorization Server (1)》
solenovex,《使用Identity Server 4建立Authorization Server (2)》
solenovex,《使用Identity Server 4建立Authorization Server (3)》
曉晨Master,《IdentityServer4實戰 - AccessToken 生命周期分析》