預備知識: http://www.cnblogs.com/cgzl/p/7746496.htmlhtml
第一部分: http://www.cnblogs.com/cgzl/p/7780559.htmlweb
第二部分: http://www.cnblogs.com/cgzl/p/7788636.html數據庫
第三部分: http://www.cnblogs.com/cgzl/p/7793241.htmljson
上一篇講了使用OpenId Connect進行Authentication.api
下面講瀏覽器
目前咱們解決方案裏面有三個項目 Authorization Server, Web api和Mvc Client. 在現實世界中, 他們可能都在不一樣的地方.cookie
如今讓咱們從MvcClient使用從Authorization Server獲取的token來訪問web api. 而且確保這個token不過時.網絡
如今咱們的mvcClient使用的是implicit flow, 也就是說, token 被髮送到client. 這種狀況下 token的生命可能很短, 可是咱們能夠重定向到authorization server 從新獲取新的token.mvc
例如, 在SPA(Single Page Application)中, implicit flow基本上就是除了resource owner password flow 之外惟一合適的flow, 可是咱們的網站可能會在client(SPA client/或者指用戶)沒使用網站的時候訪問api, 爲了這樣作, 不但要保證token不過時, 咱們還須要使用別的flow. 咱們要介紹一下authorization code flow. 它和implicit flow 很像, 不一樣的是, 在重定向回到網站的時候獲取的不是access token, 而是從authorization server獲取了一個code, 使用它網站能夠交換一個secret, 使用這個secret能夠獲取access token和refresh tokens.async
Hybrid Flow, 是兩種的混合, 首先identity token經過瀏覽器傳過來了, 而後客戶端能夠在進行任何工做以前對其驗證, 若是驗證成功, 客戶端就會再打開一個通道向Authorization Server請求獲取access token.
new Client { ClientId = "mvc_code", ClientName = "MVC Code Client", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, RedirectUris = { "http://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, "socialnetwork" }, AllowOfflineAccess = true, AllowAccessTokensViaBrowser = true }
首先確定要修改一下ClientId.
GrantType要改爲Hybrid或者HybrdAndClientCredentials, 若是隻使用Code Flow的話不行, 由於咱們的網站使用Authorization Server來進行Authentication, 咱們想獲取Access token以便被受權來訪問api. 因此這裏用HybridFlow.
還須要添加一個新的Email scope, 由於我想改變api來容許我基於email來建立用戶的數據, 由於authorization server 和 web api是分開的, 因此用戶的數據庫也是分開的. Api使用用戶名(email)來查詢數據庫中的數據.
AllowOfflineAccess. 咱們還須要獲取Refresh Token, 這就要求咱們的網站必須能夠"離線"工做, 這裏離線是指用戶和網站之間斷開了, 並非指網站離線了.
這就是說網站可使用token來和api進行交互, 而不須要用戶登錄到網站上.
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_code"; options.ClientSecret = "secret"; options.ResponseType = "id_token code"; options.Scope.Add("socialnetwork"); options.Scope.Add("offline_access"); options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; }); }
首先改ClientId和Authorization server一致. 這樣用戶訪問的時候和implicit差很少, 只不太重定向回來的時候, 獲取了一個code, 使用這個code能夠換取secret而後獲取access token.
因此須要在網站(MvcClient)上指定Client Secret. 這個不要泄露出去.
還須要改變reponse type, 不須要再獲取access token了, 而是code, 這意味着使用的是Authorization Code flow.
還須要指定請求訪問的scopes: 包括 socialnetwork api和離線訪問
最後還能夠告訴它從UserInfo節點獲取用戶的Claims.
點擊About, 重定向到Authorization Server:
同時在Authorization Server的控制檯能夠看見以下信息:
這裏能夠看到請求訪問的scope, response_type. 還告訴咱們respose mode是from_post, 這就是說, 在這登錄後重定向回到網站是使用的form post方式.
而後登錄:
這裏能夠看到請求訪問的範圍, 包括我的信息和Application Access.
點擊Yes, Allow:
重定向回到了網站. 這裏看起來好像和之前同樣. 可是若是看一下Authorization Server的控制檯:
就會看到一個request. 中間件發起了一個請求使用Authorization Code和ClientId和secret來換取了Access token.
當Authorization驗證上述信息後, 它就會建立一個token.
修改MvcClient的About.cshtml:
@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> <div> <strong>refresh_token</strong> <span>@await ViewContext.HttpContext.GetTokenAsync("refresh_token")</span> </div> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl>
刷新頁面:
看到了refresh token.
這些token包含了何時過時的信息.
若是access token過時了, 就沒法訪問api了. 因此須要確保access token不過時. 這就須要使用refresh token了.
複製一下refresh token, 而後使用postman:
使用這個refresh token能夠獲取到新的access token和refresh_token, 當這個access_token過時的時候, 可使用refresh_token再獲取一個access_token和refresh_token......
而若是使用同一個refresh token兩次, 就會獲得下面的結果:
看看Authorization Server的控制檯, 顯示是一個invalid refresh token:
因此說, refresh token是一次性的.
web api 要求request請求提供access token, 以證實請求的用戶是已經受權的. 如今咱們準備從Access token裏面提取一些自定義的Claims, 例如Email.
看看Authorization Server的Client配置:
Client的AllowedScopes已經包括了Email. 可是尚未配置Authorization Server容許這個Scope. 因此須要修改GetIdentityResources()(我本身的代碼可能更名成IdentityResources()了):
public static IEnumerable<IdentityResource> IdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() }; }
而後須要爲TestUser添加一個自定義的Claims;
public static IEnumerable<TestUser> Users() { return new[] { new TestUser { SubjectId = "1", Username = "mail@qq.com", Password = "password", Claims = new [] { new Claim("email", "mail@qq.com") } } }; }
而後須要對MvcClient進行設置, 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_code"; options.ClientSecret = "secret"; options.ResponseType = "id_token code"; options.Scope.Add("socialnetwork"); options.Scope.Add("offline_access"); options.Scope.Add("email"); options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; }); }
添加email scope. 因此MvcClient就會也請求這個scope.
運行:
這時在贊成(consent)頁面就會出現email address一欄.
贊成以後, 能夠看到email已經獲取到了.
首先在web api項目創建一個IdentityController:
namespace WebApi.Controllers { [Route("api/[controller]")] public class IdentityController: Controller { [Authorize] [HttpGet] public IActionResult Get() { var username = User.Claims.First(x => x.Type == "email").Value; return Ok(username); //return new JsonResult(from c in User.Claims select new { c.Type, c.Value}); } } }
咱們想要經過自定義的claim: email的值.
而後回到mvcClient的HomeController, 添加一個方法:
[Authorize] public async Task<IActionResult> GetIdentity() { var token = await HttpContext.GetTokenAsync("access_token"); using (var client = new HttpClient()) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var content = await client.GetStringAsync("http://localhost:5001/api/identity"); // var json = JArray.Parse(content).ToString(); return Ok(new { value = content }); } }
這裏首先經過HttpContext得到access token, 而後在請求的Authorization Header加上Bearer Token.
讓咱們運行一下, 並在MvcClient和Web Api裏面都設好斷點,
登陸後在瀏覽器輸入 http://localhost:5002/Home/GetIdentity 以執行GetIdenttiy方法, 而後進入Web Api看看斷點調試狀況:
因爲咱們已經受權了, 因此能夠看到User的一些claims, 而其中沒有email這個claim. 再運行就報錯了.
這是怎麼回事? 咱們回到About頁面, 複製一下access_token, 去jwt.io分析一下:
確實沒有email的值, 因此提取不出來.
因此咱們須要把email添加到access token的數據裏面, 這就須要告訴Authorization Server的Api Resource裏面要包括User的Scope, 由於這是Identity Scope, 咱們想要把它添加到access token裏:
修改Authorization Server的InMemoryConfiguration的ApiResources():
public static IEnumerable<ApiResource> ApiResources() { return new[] { new ApiResource("socialnetwork", "社交網絡") { UserClaims = new [] { "email" } } }; }
這對這個Api Resouce設置它的屬性UserClaims, 裏面寫上email.
而後再運行一下程序, 這裏須要從新登錄, 首先分析一下token:
有email了.
而後執行GetIdentity(), 在web api斷點調試, 能夠看到UserClaims已經包含了email:
用戶使用Authorization Server去登陸網站(MvcClient), 也就是說用戶從網站跳轉到第三方的系統完成了身份的驗證, 而後被受權能夠訪問web api了(這裏講的是用戶經過mvcClient訪問api). 當訪問web api的時候, 首先和authorization server溝通確認access token的正確性, 而後就能夠成功的訪問api了.
根據配置不一樣, token的有效期可能差異很大, 若是token過時了, 那麼發送請求以後就會返回401 UnAuthorized.
固然若是token過時了, 你可讓用戶重定向到Authorization Server從新登錄,再回來操做, 不過這樣太不友好, 太繁瑣了.
既然咱們有refresh token了, 那不如向authorization server請求一個新的access token和refresh token. 而後再把這些更新到cookie裏面. 因此下次再調用api的時候使用的是新的token.
在MvcClient的HomeController添加RefreshTokens()方法:
首先須要安裝IdentityModel, 它是OpenIdConnect, OAuth2.0的客戶端庫:
[Authorize] public async Task RefreshTokensAsync() { var authorizationServerInfo = await DiscoveryClient.GetAsync("http://localhost:5000/"); var client = new TokenClient(authorizationServerInfo.TokenEndpoint, "mvc_code", "secret"); var refreshToken = await HttpContext.GetTokenAsync("refresh_token"); var response = await client.RequestRefreshTokenAsync(refreshToken); var identityToken = await HttpContext.GetTokenAsync("identity_token"); var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn); var tokens = new[] { new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = identityToken }, new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = response.AccessToken }, new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = response.RefreshToken }, new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) } }; var authenticationInfo = await HttpContext.AuthenticateAsync("Cookies"); authenticationInfo.Properties.StoreTokens(tokens); await HttpContext.SignInAsync("Cookies", authenticationInfo.Principal, authenticationInfo.Properties); }
首先使用一個叫作discovery client的東西來獲取Authorization Server的信息. Authorization Server裏面有一個discovery節點(endpoint), 能夠經過這個地址查看: /.well-known/openid-configuration. 從這裏能夠得到不少信息, 例如: authorization節點, token節點, 發佈者, key, scopes等等.
而後使用TokenClient, 參數有token節點, clientId和secret. 而後可使用這個client和refreshtoken來請求新的access token等.
找到refresh token後, 使用client獲取新的tokens, 返回結果是tokenresponse. 你能夠設斷點查看一下token reponse裏面都有什麼東西, 這裏就不弄了, 裏面包括identitytoken, accesstoken, refreshtoken等等.
而後須要找到原來的identity token, 由於它至關因而cookie中存儲的主鍵...
而後設置一下過時時間.
而後將老的identity token和新獲取到的其它tokens以及過時時間, 組成一個集合.
而後使用這些tokens來從新登錄用戶. 不過首先要獲取當前用戶的authentication信息, 使用HttpContext.AuthenticateAsync("Cookies"), 參數是AuthenticationScheme. 而後修改屬性, 存儲新的tokens.
最後就是重登陸, 把當前用戶信息的Principal和Properties傳進去. 這就會更新客戶端的Cookies, 用戶也就保持登錄而且刷新了tokens.
先簡單調用一下這個方法:
[Authorize] public async Task<IActionResult> GetIdentity() { await RefreshTokensAsync(); var token = await HttpContext.GetTokenAsync("access_token"); using (var client = new HttpClient()) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var content = await client.GetStringAsync("http://localhost:5001/api/identity"); //var json = JArray.Parse(content).ToString(); return Ok(new { value = content }); } }
正式生產環境中可不要這麼作, 正式環境中應該在401以後, 調用這個方法, 若是再失敗, 再返回錯誤.
運行一下:
發現獲取的access token是空的, 必定是哪出現了問題, 看一下 authorization server的控制檯:
說refresh token不正確(應該是內存數據和cookie數據不匹配). 那就從新登錄.
看斷點, 有token了:
而且和About頁面顯示的不同, 說明刷新token了.
也能夠看一下authorization server的控制檯:
說明成功請求了token.
今天先到這裏.