預備知識: http://www.cnblogs.com/cgzl/p/7746496.html
第一部分: http://www.cnblogs.com/cgzl/p/7780559.html
第二部分: http://www.cnblogs.com/cgzl/p/7788636.html
上一部分簡單的弄了個web api 並通過Client_Credentials和ResourceOwnerPassword兩種方式獲取token然后進行api請求.
這次講一下Authentication 身份驗證 (而Authorization是授權, 注意區分), 使用的是OpenIdConnect.
這次我們使用的是一個MVC客戶端.
建立MVC客戶端項目
在同一個解決方案建立一個名字叫MvcClient的asp.net core mvc 項目:
不要配置Authentication(身份驗證), 應該是沒有驗證.
修改運行方式為控制台, 端口改為5002, 也就是修改launchSettings.json, 把IISExpress相關的去掉:
{ "profiles": { "MvcClient": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:5002/" } } }
在HomeController的About方法上面添加Authorize屬性:
[Authorize] public IActionResult About() { ViewData["Message"] = "Your application description page."; return View(); }
然后設置解決方案的啟動項目為MvcClient和Authorization Server, 解決方案右鍵屬性:
然后運行項目, 在http://localhost:5002 里點擊About菜單:
就會出現以下異常 (500):
我們現在要做的就是, 用戶點擊About之后, 頁面重定向到Authorization Server, 用戶填寫完信息之后登陸到Authorization Server之后再重定向回到該網站(MvcClient).
這也意味着用戶是在Authorization Server使用用戶名和密碼, 而MvcClient不保存用戶的用戶名和密碼.
下面就開始配置
添加OpenId Connect Authentication
在Startup的ConfigureServices里面添加:
public void ConfigureServices(IServiceCollection services) { 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:5000"; options.RequireHttpsMetadata = false; options.ClientId = "mvc_implicit"; options.SaveTokens = true; }); }
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 這句話是指, 我們關閉了JWT的Claim 類型映射, 以便允許well-known claims.
這樣做, 就保證它不會修改任何從Authorization Server返回的Claims.
AddAuthentication()方法是像DI注冊了該服務.
這里我們使用Cookie作為驗證用戶的首選方式: DefaultScheme = "Cookies".
而把DefaultChanllangeScheme設為"oidc"是因為, 當用戶需要登陸的時候, 將使用的是OpenId Connect Scheme.
然后的AddCookie, 是表示添加了可以處理Cookie的處理器(handler).
最后AddOpenIdConnect是讓上面的handler來執行OpenId Connect 協議.
其中的Authority是指信任的Identity Server ( Authorization Server).
ClientId是Client的識別標志. 目前Authorization Server還沒有配置這個Client, 一會我們再弄.
Client名字也暗示了我們要使用的是implicit flow, 這個flow主要應用於客戶端應用程序, 這里的客戶端應用程序主要是指javascript應用程序. implicit flow是很簡單的重定向flow, 它允許我們重定向到authorization server, 然后帶着id token重定向回來, 這個 id token就是openid connect 用來識別用戶是否已經登陸了. 同時也可以獲得access token. 很明顯, 我們不希望access token出現在那個重定向中. 這個一會再說.
一旦OpenId Connect協議完成, SignInScheme使用Cookie Handler來發布Cookie (中間件告訴我們已經重定向回到MvcClient了, 這時候有token了, 使用Cookie handler來處理).
SaveTokens為true表示要把從Authorization Server的Reponse中返回的token們持久化在cookie中.
注意正式生產環境要使用https, 這里就不用了.
接下來在Startup的Configure方法配置中間件, 以確保每次請求都執行authentication:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseAuthentication(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
注意在管道配置的位置一定要在useMVC之前.
在Authorization Server添加Client
在Authorization Server的InMemoryConfiguration里面添加Client:
public static IEnumerable<Client> Clients() { return new[] { new Client { ClientId = "socialnetwork", ClientSecrets = new [] { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, AllowedScopes = new [] { "socialnetwork" } }, new Client { ClientId = "mvc_implicit", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.Implicit, RedirectUris = { "http://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile,
"socialnetwork" } } }; }
ClientId要和MvcClient里面指定的名稱一致.
OAuth是使用Scopes來划分Api的, 而OpenId Connect則使用Scopes來限制信息, 例如使用offline access時的Profile信息, 還有用戶的其他細節信息.
這里GrantType要改為Implicit. 使用Implicit flow時, 首先會重定向到Authorization Server, 然后登陸, 然后Identity Server需要知道是否可以重定向回到網站, 如果不指定重定向返回的地址的話, 我們的Session有可能就會被劫持.
RedirectUris就是登陸成功之后重定向的網址, 這個網址(http://localhost:5002/signin-oidc)在MvcClient里, openid connect中間件使用這個地址就會知道如何處理從authorization server返回的response. 這個地址將會在openid connect 中間件設置合適的cookies, 以確保配置的正確性.
而PostLogoutRedirectUris是登出之后重定向的網址. 有可能發生的情況是, 你登出網站的時候, 會重定向到Authorization Server, 並允許從Authorization Server也進行登出動作.
最后還需要指定OpenId Connect使用的Scopes, 之前我們指定的socialnetwork是一個ApiResource. 而這里我們需要添加的是讓我們能使用OpenId Connect的SCopes, 這里就要使用Identity Resources. Identity Server帶了幾個常量可以用來指定OpenId Connect預包裝的Scopes. 上面的AllowedScopes設定的就是我們要用的scopes, 他們包括 openid Connect和用戶的profile, 同時也包括我們之前寫的api resource: "socialnetwork". 要注意區分, 這里有Api resources, 還有openId connect scopes(用來限定client可以訪問哪些信息), 而為了使用這些openid connect scopes, 我們需要設置這些identity resoruces, 這和設置ApiResources差不多:
public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; }
然后我們需要配置Authorization Server來允許使用這些Identity Resources, Statup的:
public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() //.AddDeveloperSigningCredential() .AddSigningCredential(new X509Certificate2(@"D:\Projects\test\socialnetwork.pfx", "Bx@steel")) .AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources()) .AddTestUsers(InMemoryConfiguration.Users().ToList()) .AddInMemoryClients(InMemoryConfiguration.Clients()) .AddInMemoryApiResources(InMemoryConfiguration.ApiResources()); services.AddMvc(); }
設置完了, 運行一下, 點擊About菜單就會重定向到Authorization Server:
注意看URL, 我們確實是在Authorization Server.
然后輸入用戶名密碼(TestUser的), 會看見一個請求允許的畫面:
可以看到網站請求了Profile信息和User Identity.
這個時候看上面菜單處, 可以發現用戶已經成功登陸了Authorization Server:
所以這就允許我們做SSO(Single Sign-On) 單點登錄了. 這時候其他使用這個Authorization Server的Client應用, 由於用戶已經登陸到Authorization Server了, 只需要請求用戶的許可來訪問用戶的數據就行了.
然后點擊同意 Yes Allow, 就會重定向返回MvcClient網站的About頁面:
在View中顯示Claims
打開MvcClient的About.cshtml:
<dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl>
顯示所有用戶的Claims. Claims就是從Authorization Server返回的Payload里面的數據.
運行進入About頁面:
嗯當前用戶有這些信息....
想要從MvcClient調用WebApi
我們現在想從MvcClient調用WebApi的api/Values節點, 這就需要使用從Authorization Server返回的token. 但是由於我們使用的是implicit flow, 而使用implicit flow, 一切數據都是被發送到Client的. 這就是說, 為了讓MvcClient知道用戶已經成功登陸了, Authorization Server將會告訴Client(Chrome瀏覽器)重定向回到MvcClient網站, 並附帶着數據. 這意味着token和其他安全信息將會在瀏覽器里面被傳遞. 也就是說從Authorization Server發送access token的時候, 如果有人監聽的話就會看見這些數據, 使用ssl能有效阻止監聽到數據. 當然肯定有辦法解決這個問題, 例如使用其他flow. 但是有時候還是必須要使用implicit flow 獲取到access token. 我們需要做的就是告訴Authorization Server可以使用implicit flow來獲取token.
首先我們把token顯示出來:
@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是openid connect指定的, 你需要從authorization server獲得它, 用來驗證你的身份, 知道你已經登陸了. id_token不是你用來訪問api的.
access_token是用來訪問api的.
運行一下:
可以看到id_token有了, 而access_token沒有, 這是因為我們還沒有告訴Authorization Server在使用implicit flow時可以允許返回Access token.
修改Authorization Server的Client來允許返回Access Token
new Client { ClientId = "mvc_implicit", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.Implicit, RedirectUris = { "http://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "socialnetwork" }, AllowAccessTokensViaBrowser = true }
在某種情況下還是不建議這么做.
然后在運行一下:
還是沒有access token. 這是因為我們需要重新登陸來獲取access token. 我們首先需要登出.
實現Logout 登出
在MvcClient的homeController添加方法:
public async Task Logout() { await HttpContext.SignOutAsync("Cookies"); await HttpContext.SignOutAsync("oidc"); }
這里需要確保同時登出本地應用(MvcClient)的Cookies和OpenId Connect(去Identity Server清除單點登錄的Session).
運行, 在瀏覽器輸入地址: http://localhost:5002/Home/Logout
然后就會跳轉到Identity Server的Logout了的頁面:
這寫到, 點擊here可以返回到mvcclient.
點擊here, 回到mvcclient, 然后點擊About, 重新登陸. 同意, 重定向回來:
還是沒有access token.....
看看authorization server的控制台:
有個地方寫到返回類型是id_token. 這表示我們要進行的是Authentication.
而我們想要的是既做Authentication又做Authorization. 也就是說我們既要id_token還要token本身.
這么做, 在MvcClient的CongifureServices:
public void ConfigureServices(IServiceCollection services) { 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:5000"; options.RequireHttpsMetadata = false; options.ClientId = "mvc_implicit"; options.ResponseType = "id_token token"; options.SaveTokens = true; }); }
然后重新運行, 退出, 重登錄:
這次終於看到access_token了....
現在就可以使用access_token訪問api了.
先寫到這. 明后天繼續.