使用Identity Server 4建立Authorization Server (4)
預(yù)備知識(shí):?http://www.cnblogs.com/cgzl/p/7746496.html
第一部分:?http://www.cnblogs.com/cgzl/p/7780559.html
第二部分:?http://www.cnblogs.com/cgzl/p/7788636.html
第三部分:?http://www.cnblogs.com/cgzl/p/7793241.html
上一篇講了使用OpenId Connect進(jìn)行Authentication.
下面講
Hybrid Flow和Offline Access
目前我們解決方案里面有三個(gè)項(xiàng)目 Authorization Server, Web api和Mvc Client. 在現(xiàn)實(shí)世界中, 他們可能都在不同的地方.
現(xiàn)在讓我們從MvcClient使用從Authorization Server獲取的token來訪問web api. 并且確保這個(gè)token不過期.
現(xiàn)在我們的mvcClient使用的是implicit flow, 也就是說, token 被發(fā)送到client. 這種情況下 token的生命可能很短, 但是我們可以重定向到authorization server 重新獲取新的token.
例如, 在SPA(Single Page Application)中, implicit flow基本上就是除了resource owner password flow 以外唯一合適的flow, 但是我們的網(wǎng)站可能會(huì)在client(SPA client/或者指用戶)沒使用網(wǎng)站的時(shí)候訪問api, 為了這樣做, 不但要保證token不過期, 我們還需要使用別的flow. 我們要介紹一下authorization code flow. 它和implicit flow 很像, 不同的是, 在重定向回到網(wǎng)站的時(shí)候獲取的不是access token, 而是從authorization server獲取了一個(gè)code, 使用它網(wǎng)站可以交換一個(gè)secret, 使用這個(gè)secret可以獲取access token和refresh tokens.
Hybrid Flow, 是兩種的混合, 首先identity token通過瀏覽器傳過來了, 然后客戶端可以在進(jìn)行任何工作之前對(duì)其驗(yàn)證, 如果驗(yàn)證成功, 客戶端就會(huì)再打開一個(gè)通道向Authorization Server請(qǐng)求獲取access token.
首先在Authorization server的InMemoryConfiguration添加一個(gè)Client:
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的話不行, 因?yàn)槲覀兊木W(wǎng)站使用Authorization Server來進(jìn)行Authentication, 我們想獲取Access token以便被授權(quán)來訪問api. 所以這里用HybridFlow.
還需要添加一個(gè)新的Email scope, 因?yàn)槲蚁敫淖僡pi來允許我基于email來創(chuàng)建用戶的數(shù)據(jù), 因?yàn)閍uthorization server 和 web api是分開的, 所以用戶的數(shù)據(jù)庫(kù)也是分開的. Api使用用戶名(email)來查詢數(shù)據(jù)庫(kù)中的數(shù)據(jù).
AllowOfflineAccess. 我們還需要獲取Refresh Token, 這就要求我們的網(wǎng)站必須可以"離線"工作, 這里離線是指用戶和網(wǎng)站之間斷開了, 并不是指網(wǎng)站離線了.
這就是說網(wǎng)站可以使用token來和api進(jìn)行交互, 而不需要用戶登陸到網(wǎng)站上.?
修改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.SaveTokens = true;options.GetClaimsFromUserInfoEndpoint = true;});}首先改ClientId和Authorization server一致. 這樣用戶訪問的時(shí)候和implicit差不多, 只不過重定向回來的時(shí)候, 獲取了一個(gè)code, 使用這個(gè)code可以換取secret然后獲取access token.
所以需要在網(wǎng)站(MvcClient)上指定Client Secret. 這個(gè)不要泄露出去.
還需要改變r(jià)eponse type, 不需要再獲取access token了, 而是code, 這意味著使用的是Authorization Code flow.
還需要指定請(qǐng)求訪問的scopes: 包括 socialnetwork api和離線訪問
最后還可以告訴它從UserInfo節(jié)點(diǎn)獲取用戶的Claims.
運(yùn)行
點(diǎn)擊About, 重定向到Authorization Server:
同時(shí)在Authorization Server的控制臺(tái)可以看見如下信息:
這里可以看到請(qǐng)求訪問的scope, response_type. 還告訴我們r(jià)espose mode是from_post, 這就是說, 在這登陸后重定向回到網(wǎng)站是使用的form post方式.
然后登陸:
這里可以看到請(qǐng)求訪問的范圍, 包括個(gè)人信息和Application Access.
點(diǎn)擊Yes, Allow:
重定向回到了網(wǎng)站. 這里看起來好像和以前一樣. 但是如果看一下Authorization Server的控制臺(tái):
就會(huì)看到一個(gè)request. 中間件發(fā)起了一個(gè)請(qǐng)求使用Authorization Code和ClientId和secret來?yè)Q取了Access token.
當(dāng)Authorization驗(yàn)證上述信息后, 它就會(huì)創(chuàng)建一個(gè)token.
打印Refresh 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>刷新頁(yè)面:
看到了refresh token.
這些token包含了什么時(shí)候過期的信息.
如果access token過期了, 就無法訪問api了. 所以需要確保access token不過期. 這就需要使用refresh token了.
復(fù)制一下refresh token, 然后使用postman:
使用這個(gè)refresh token可以獲取到新的access token和refresh_token, 當(dāng)這個(gè)access_token過期的時(shí)候, 可以使用refresh_token再獲取一個(gè)access_token和refresh_token......
而如果使用同一個(gè)refresh token兩次, 就會(huì)得到下面的結(jié)果:
看看Authorization Server的控制臺(tái), 顯示是一個(gè)invalid refresh token:
所以說, refresh token是一次性的.
獲取自定義Claims
web api 要求request請(qǐng)求提供access token, 以證明請(qǐng)求的用戶是已經(jīng)授權(quán)的. 現(xiàn)在我們準(zhǔn)備從Access token里面提取一些自定義的Claims, 例如Email.
看看Authorization Server的Client配置:
Client的AllowedScopes已經(jīng)包括了Email. 但是還沒有配置Authorization Server允許這個(gè)Scope. 所以需要修改GetIdentityResources()(我自己的代碼可能改名成IdentityResources()了):
public static IEnumerable<IdentityResource> IdentityResources(){return new List<IdentityResource>{new IdentityResources.OpenId(),new IdentityResources.Profile(),new IdentityResources.Email()};}然后需要為TestUser添加一個(gè)自定義的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") }}};}然后需要對(duì)MvcClient進(jìn)行設(shè)置, 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就會(huì)也請(qǐng)求這個(gè)scope.
運(yùn)行:
這時(shí)在同意(consent)頁(yè)面就會(huì)出現(xiàn)email address一欄.
同意之后, 可以看到email已經(jīng)獲取到了.
使用Access Token調(diào)用Web Api
首先在web api項(xiàng)目建立一個(gè)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, 添加一個(gè)方法:
[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, 然后在請(qǐng)求的Authorization Header加上Bearer Token.
讓我們運(yùn)行一下, 并在MvcClient和Web Api里面都設(shè)好斷點(diǎn),
登錄后在瀏覽器輸入?http://localhost:5002/Home/GetIdentity 以執(zhí)行GetIdenttiy方法, 然后進(jìn)入Web Api看看斷點(diǎn)調(diào)試情況:
由于我們已經(jīng)授權(quán)了, 所以可以看到User的一些claims, 而其中沒有email這個(gè)claim. 再運(yùn)行就報(bào)錯(cuò)了.
這是怎么回事? 我們回到About頁(yè)面, 復(fù)制一下access_token, 去jwt.io分析一下:
確實(shí)沒有email的值, 所以提取不出來.
所以我們需要把email添加到access token的數(shù)據(jù)里面, 這就需要告訴Authorization Server的Api Resource里面要包括User的Scope, 因?yàn)檫@是Identity Scope, 我們想要把它添加到access token里:
修改Authorization Server的InMemoryConfiguration的ApiResources():
public static IEnumerable<ApiResource> ApiResources(){return new[]{new ApiResource("socialnetwork", "社交網(wǎng)絡(luò)"){UserClaims = new [] { "email" }}};}這對(duì)這個(gè)Api Resouce設(shè)置它的屬性UserClaims, 里面寫上email.
然后再運(yùn)行一下程序, 這里需要重新登陸, 首先分析一下token:
有email了.?
然后執(zhí)行GetIdentity(), 在web api斷點(diǎn)調(diào)試, 可以看到UserClaims已經(jīng)包含了email:
上面這些如果您不會(huì)的話, 需要整理總結(jié)一下.
用戶使用Authorization Server去登錄網(wǎng)站(MvcClient), 也就是說用戶從網(wǎng)站跳轉(zhuǎn)到第三方的系統(tǒng)完成了身份的驗(yàn)證, 然后被授權(quán)可以訪問web api了(這里講的是用戶通過mvcClient訪問api). 當(dāng)訪問web api的時(shí)候, 首先和authorization server溝通確認(rèn)access token的正確性, 然后就可以成功的訪問api了.
刷新Access Token
根據(jù)配置不同, token的有效期可能差別很大, 如果token過期了, 那么發(fā)送請(qǐng)求之后就會(huì)返回401 UnAuthorized.
當(dāng)然如果token過期了, 你可以讓用戶重定向到Authorization Server重新登陸,再回來操作, 不過這樣太不友好, 太繁瑣了.
既然我們有refresh token了, 那不如向authorization server請(qǐng)求一個(gè)新的access token和refresh token. 然后再把這些更新到cookie里面. 所以下次再調(diào)用api的時(shí)候使用的是新的token.
在MvcClient的HomeController添加RefreshTokens()方法:
首先需要安裝IdentityModel, 它是OpenIdConnect, OAuth2.0的客戶端庫(kù):
[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);}首先使用一個(gè)叫做discovery client的東西來獲取Authorization Server的信息. Authorization Server里面有一個(gè)discovery節(jié)點(diǎn)(endpoint), 可以通過這個(gè)地址查看:?/.well-known/openid-configuration. 從這里可以獲得很多信息, 例如: authorization節(jié)點(diǎn), token節(jié)點(diǎn), 發(fā)布者, key, scopes等等.
然后使用TokenClient, 參數(shù)有token節(jié)點(diǎn), clientId和secret. 然后可以使用這個(gè)client和refreshtoken來請(qǐng)求新的access token等.?
找到refresh token后, 使用client獲取新的tokens, 返回結(jié)果是tokenresponse. 你可以設(shè)斷點(diǎn)查看一下token reponse里面都有什么東西, 這里就不弄了, 里面包括identitytoken, accesstoken, refreshtoken等等.
然后需要找到原來的identity token, 因?yàn)樗喈?dāng)于是cookie中存儲(chǔ)的主鍵...
然后設(shè)置一下過期時(shí)間.
然后將老的identity token和新獲取到的其它tokens以及過期時(shí)間, 組成一個(gè)集合.
然后使用這些tokens來重新登陸用戶. 不過首先要獲取當(dāng)前用戶的authentication信息, 使用HttpContext.AuthenticateAsync("Cookies"), 參數(shù)是AuthenticationScheme.?然后修改屬性, 存儲(chǔ)新的tokens.
最后就是重登錄, 把當(dāng)前用戶信息的Principal和Properties傳進(jìn)去. 這就會(huì)更新客戶端的Cookies, 用戶也就保持登陸并且刷新了tokens.
先簡(jiǎn)單調(diào)用一下這個(gè)方法:
[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 });}}正式生產(chǎn)環(huán)境中可不要這么做, 正式環(huán)境中應(yīng)該在401之后, 調(diào)用這個(gè)方法, 如果再失敗, 再返回錯(cuò)誤.
運(yùn)行一下:
發(fā)現(xiàn)獲取的access token是空的, 一定是哪出現(xiàn)了問題, 看一下 authorization server的控制臺(tái):
說refresh token不正確(應(yīng)該是內(nèi)存數(shù)據(jù)和cookie數(shù)據(jù)不匹配). 那就重新登陸.
看斷點(diǎn), 有token了:
并且和About頁(yè)面顯示的不一樣, 說明刷新token了.
也可以看一下authorization server的控制臺(tái):
說明成功請(qǐng)求了token.
今天先到這里.
轉(zhuǎn)載于:https://www.cnblogs.com/cgzl/p/7795121.html
總結(jié)
以上是生活随笔為你收集整理的使用Identity Server 4建立Authorization Server (4)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue的父子组建之间的通信(-),基于p
- 下一篇: 获取所有栈的信息,只有最上面的和最下面的