IdentityServer4系列 | 授权码模式
一、前言
在上一篇關于簡化模式中,通過客戶端以瀏覽器的形式請求「IdentityServer」服務獲取訪問令牌,從而請求獲取受保護的資源,但由于token攜帶在url中,安全性方面不能保證。因此,我們可以考慮通過其他方式來解決這個問題。
我們通過Oauth2.0的「授權碼模式」了解,這種模式不同于簡化模式,「在于授權碼模式不直接返回token,而是先返回一個授權碼,然后再根據這個授權碼去請求token」。這顯得更為安全。
所以在這一篇中,我們將通過多種授權模式中的「授權碼」模式進行說明,主要針對介紹「IdentityServer」保護API的資源,「授權碼」訪問API資源。
二、初識
?「指的是第三方應用先申請一個授權碼,然后再用該碼獲取令牌,實現與資源服務器的通信。」
?看一個常見的QQ登陸第三方網站的流程如下圖所示:
2.1 適用范圍
授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。
?「授權碼模式適用于有后端的應用,因為客戶端根據授權碼去請求token時是需要把客戶端密碼轉進來的,為了避免客戶端密碼被暴露,所以請求token這個過程需要放在后臺。」
?2.2 ?授權流程:
?+----------+|?Resource?||???Owner??||??????????|+----------+^|(B)+----|-----+??????????Client?Identifier??????+---------------+|?????????-+----(A)--?&?Redirection?URI?---->|???????????????||??User-???|?????????????????????????????????|?Authorization?||??Agent??-+----(B)--?User?authenticates?--->|?????Server????||??????????|?????????????????????????????????|???????????????||?????????-+----(C)--?Authorization?Code?---<|???????????????|+-|----|---+?????????????????????????????????+---------------+|????|?????????????????????????????????????????^??????v(A)??(C)????????????????????????????????????????|??????||????|?????????????????????????????????????????|??????|^????v?????????????????????????????????????????|??????|+---------+??????????????????????????????????????|??????||?????????|>---(D)--?Authorization?Code?---------'??????||??Client?|??????????&?Redirection?URI??????????????????||?????????|?????????????????????????????????????????????||?????????|<---(E)-----?Access?Token?-------------------'+---------+???????(w/?Optional?Refresh?Token)「授權碼授權流程描述」
(A)「用戶訪問第三方應用,第三方應用將用戶導向認證服務器」;
(B)「用戶選擇是否給予第三方應用授權」;
(C)「假設用戶給予授權,認證服務器將用戶導向第三方應用事先指定的重定向URI,同時帶上一個授權碼」;
(D)「第三方應用收到授權碼,帶上上一步時的重定向URI,向認證服務器申請訪問令牌。這一步是在第三方應用的后臺的服務器上完成的,對用戶不可見」;
(E)「認證服務器核對了授權碼和重定向URI,確認無誤后,向第三方應用發(fā)送訪問令牌(Access Token)和更新令牌(Refresh token)」;
(F)「訪問令牌過期后,刷新訪問令牌」;
2.2.1 過程詳解
訪問令牌請求
「(1)用戶訪問第三方應用,第三方應用將用戶導向認證服務器」
(用戶的操作:用戶訪問https://client.example.com/cb跳轉到登錄地址,選擇授權服務器方式登錄)在授權開始之前,它首先生成state參數(隨機字符串)。client端將需要存儲這個(cookie,會話或其他方式),以便在下一步中使用。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 HTTP/1.1 Host: server.example.com生成的授權URL如上所述(如上),請求這個地址后重定向訪問授權服務器,其中 response_type參數為code,表示授權類型,返回code授權碼。
| response_type | 必需 | 表示授權類型,此處的值固定為"code" |
| client_id | 必需 | 客戶端ID |
| redirect_uri | 可選 | 表示重定向的URI |
| scope | 可選 | 表示授權范圍。 |
| state | 可選 | 表示隨機字符串,可指定任意值,認證服務器會返回這個值 |
「(2)假設用戶給予授權,認證服務器將用戶導向第三方應用事先指定的重定向URI,同時帶上一個授權碼」
HTTP/1.1 302 Found Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz| code | 表示授權碼,必選項。該碼的有效期應該很短,通常設為10分鐘,客戶端只能使用該碼一次,否則會被授權服務器拒絕。該碼與客戶端ID和重定向URI,是一一對應關系。 |
| state | 如果客戶端的請求中包含這個參數,認證服務器的回應也必須一模一樣包含這個參數。 |
「(3)第三方應用收到授權碼,帶上上一步時的重定向URI,向認證服務器申請訪問令牌。這一步是在第三方應用的后臺的服務器上完成的,對用戶不可見」。
POST /token HTTP/1.1 Host: server.example.com Authorization: Bearer czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb| grant_type | 表示使用的授權模式,必選項,此處的值固定為"authorization_code"。 |
| code | 表示上一步獲得的授權碼,必選項。 |
| redirect_uri | 表示重定向URI,必選項,且必須與步驟1中的該參數值保持一致。 |
| client_id | 表示客戶端ID,必選項。 |
「(4)認證服務器核對了授權碼和重定向URI,確認無誤后,向第三方應用發(fā)送訪問令牌(Access Token)和更新令牌(Refresh token)」
HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-storePragma: no-cache{"access_token":"2YotnFZFEjr1zCsicMWpAA","token_type":"Bearer","expires_in":3600,"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA","example_parameter":"example_value"}| access_token | 表示訪問令牌,必選項。 |
| token_type | 表示令牌類型,該值大小寫不敏感,必選項,可以是Bearer類型或mac類型。 |
| expires_in | 表示過期時間,單位為秒。如果省略該參數,必須其他方式設置過期時間。 |
| refresh_token | 表示更新令牌,用來獲取下一次的訪問令牌,可選項。 |
| scope | 表示權限范圍,如果與客戶端申請的范圍一致,此項可省略。 |
「(5) 訪問令牌過期后,刷新訪問令牌」
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA| granttype | 表示使用的授權模式,此處的值固定為"refreshtoken",必選項。 |
| refresh_token | 表示早前收到的更新令牌,必選項。 |
| scope | 表示申請的授權范圍,不可以超出上一次申請的范圍,如果省略該參數,則表示與上一次一致。 |
三、實踐
?在示例實踐中,我們將創(chuàng)建一個授權訪問服務,定義一個MVC客戶端,MVC客戶端通過「IdentityServer」上請求訪問令牌,并使用它來訪問API。
?3.1 搭建 Authorization Server 服務
?搭建認證授權服務
?3.1.1 安裝Nuget包
?IdentityServer4 程序包
?3.1.2 配置內容
建立配置內容文件Config.cs
public?static?class?Config {public?static?IEnumerable<IdentityResource>?IdentityResources?=>new?IdentityResource[]{new?IdentityResources.OpenId(),new?IdentityResources.Profile(),};public?static?IEnumerable<ApiScope>?ApiScopes?=>new?ApiScope[]{new?ApiScope("code_scope1")};public?static?IEnumerable<ApiResource>?ApiResources?=>new?ApiResource[]{new?ApiResource("api1","api1"){Scopes={?"code_scope1"?},UserClaims={JwtClaimTypes.Role},??//添加Cliam?角色類型ApiSecrets={new?Secret("apipwd".Sha256())}}};public?static?IEnumerable<Client>?Clients?=>new?Client[]{new?Client{ClientId?=?"code_client",ClientName?=?"code?Auth",AllowedGrantTypes?=?GrantTypes.Code,RedirectUris?={"http://localhost:5002/signin-oidc",?//跳轉登錄到的客戶端的地址},//?RedirectUris?=?{"http://localhost:5002/auth.html"?},?//跳轉登出到的客戶端的地址PostLogoutRedirectUris?={"http://localhost:5002/signout-callback-oidc",},ClientSecrets?=?{?new?Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256())?},AllowedScopes?=?{IdentityServerConstants.StandardScopes.OpenId,IdentityServerConstants.StandardScopes.Profile,"code_scope1"},//允許將token通過瀏覽器傳遞AllowAccessTokensViaBrowser=true,//?是否需要同意授權?(默認是false)RequireConsent=true}}; } ?RedirectUris : 登錄成功回調處理的客戶端地址,處理回調返回的數據,可以有多個。
PostLogoutRedirectUris :跳轉登出到的客戶端的地址。
這兩個都是配置的客戶端的地址,且是identityserver4組件里面封裝好的地址,作用分別是登錄,注銷的回調
?因為是「授權碼」授權的方式,所以我們通過代碼的方式來創(chuàng)建幾個測試用戶。
新建測試用戶文件TestUsers.cs
????public?class?TestUsers{public?static?List<TestUser>?Users{get{var?address?=?new{street_address?=?"One?Hacker?Way",locality?=?"Heidelberg",postal_code?=?69118,country?=?"Germany"};return?new?List<TestUser>{new?TestUser{SubjectId?=?"1",Username?=?"i3yuan",Password?=?"123456",Claims?={new?Claim(JwtClaimTypes.Name,?"i3yuan?Smith"),new?Claim(JwtClaimTypes.GivenName,?"i3yuan"),new?Claim(JwtClaimTypes.FamilyName,?"Smith"),new?Claim(JwtClaimTypes.Email,?"i3yuan@email.com"),new?Claim(JwtClaimTypes.EmailVerified,?"true",?ClaimValueTypes.Boolean),new?Claim(JwtClaimTypes.WebSite,?"http://i3yuan.top"),new?Claim(JwtClaimTypes.Address,?JsonSerializer.Serialize(address),?IdentityServerConstants.ClaimValueTypes.Json)}}};}}}返回一個TestUser的集合。
通過以上添加好配置和測試用戶后,我們需要將用戶注冊到IdentityServer4服務中,接下來繼續(xù)介紹。
3.1.3 注冊服務
在startup.cs中ConfigureServices方法添加如下代碼:
????????public?void?ConfigureServices(IServiceCollection?services){var?builder?=?services.AddIdentityServer().AddTestUsers(TestUsers.Users);?//添加測試用戶//?in-memory,?code?configbuilder.AddInMemoryIdentityResources(Config.IdentityResources);builder.AddInMemoryApiScopes(Config.ApiScopes);builder.AddInMemoryApiResources(Config.ApiResources);builder.AddInMemoryClients(Config.Clients);//?not?recommended?for?production?-?you?need?to?store?your?key?material?somewhere?securebuilder.AddDeveloperSigningCredential();services.ConfigureNonBreakingSameSiteCookies();}3.1.4 配置管道
在startup.cs中Configure方法添加如下代碼:
????????public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env){if?(env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseStaticFiles();app.UseRouting();app.UseCookiePolicy();app.UseAuthentication();app.UseAuthorization();app.UseIdentityServer();app.UseEndpoints(endpoints?=>{endpoints.MapDefaultControllerRoute();});?}以上內容是快速搭建簡易IdentityServer項目服務的方式。
「這搭建 Authorization Server 服務跟上一篇簡化模式有何不同之處呢?」
?在Config中配置客戶端(client)中定義了一個AllowedGrantTypes的屬性,這個屬性決定了Client可以被哪種模式被訪問,「GrantTypes.Code」為「授權碼模式」。所以在本文中我們需要添加一個Client用于支持授權碼模式(「Authorization Code」)。
3.2 搭建API資源
?實現對API資源進行保護
?3.2.1 快速搭建一個API項目
3.2.2 安裝Nuget包
?IdentityServer4.AccessTokenValidation 包
?3.2.3 注冊服務
在startup.cs中ConfigureServices方法添加如下代碼:
????public?void?ConfigureServices(IServiceCollection?services){services.AddControllersWithViews();services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options?=>{options.Authority?=?"http://localhost:5001";options.RequireHttpsMetadata?=?false;options.ApiName?=?"api1";options.ApiSecret?=?"apipwd";?//對應ApiResources中的密鑰});}AddAuthentication把Bearer配置成默認模式,將身份認證服務添加到DI中。
AddIdentityServerAuthentication把IdentityServer的access token添加到DI中,供身份認證服務使用。
3.2.4 配置管道
在startup.cs中Configure方法添加如下代碼:
????????public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env){if?(env.IsDevelopment()){app.UseDeveloperExceptionPage();}????app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.UseEndpoints(endpoints?=>{endpoints.MapDefaultControllerRoute();});}UseAuthentication將身份驗證中間件添加到管道中;
UseAuthorization 將啟動授權中間件添加到管道中,以便在每次調用主機時執(zhí)行身份驗證授權功能。
3.2.5 添加API資源接口
[Route("api/[Controller]")] [ApiController] public?class?IdentityController:ControllerBase {[HttpGet("getUserClaims")][Authorize]public?IActionResult?GetUserClaims(){return?new?JsonResult(from?c?in?User.Claims?select?new?{?c.Type,?c.Value?});} }在IdentityController 控制器中添加 [Authorize] , 在進行請求資源的時候,需進行認證授權通過后,才能進行訪問。
3.3 搭建MVC 客戶端
?實現對客戶端認證授權訪問資源
?3.3.1 快速搭建一個MVC項目
3.3.2 安裝Nuget包
?IdentityServer4.AccessTokenValidation 包
?3.3.3 注冊服務
要將對 OpenID Connect 身份認證的支持添加到MVC應用程序中。
在startup.cs中ConfigureServices方法添加如下代碼:
????public?void?ConfigureServices(IServiceCollection?services){services.AddControllersWithViews();services.AddAuthorization();services.AddAuthentication(options?=>{options.DefaultScheme?=?"Cookies";options.DefaultChallengeScheme?=?"oidc";}).AddCookie("Cookies")??//使用Cookie作為驗證用戶的首選方式.AddOpenIdConnect("oidc",?options?=>{options.Authority?=?"http://localhost:5001";??//授權服務器地址options.RequireHttpsMetadata?=?false;??//暫時不用httpsoptions.ClientId?=?"code_client";options.ClientSecret?=?"511536EF-F270-4058-80CA-1C89C192F69A";options.ResponseType?=?"code";?//代表Authorization?Codeoptions.Scope.Add("code_scope1");?//添加授權資源options.SaveTokens?=?true;?//表示把獲取的Token存到Cookie中options.GetClaimsFromUserInfoEndpoint?=?true;});services.ConfigureNonBreakingSameSiteCookies();} ?AddAuthentication注入添加認證授權,當需要用戶登錄時,使用 cookie 來本地登錄用戶(通過“Cookies”作為DefaultScheme),并將 DefaultChallengeScheme 設置為“oidc”,
使用 AddCookie 添加可以處理 cookie 的處理程序。
在AddOpenIdConnect用于配置執(zhí)行 OpenID Connect 協議的處理程序和相關參數。Authority表明之前搭建的 IdentityServer 授權服務地址。然后我們通過ClientId、ClientSecret,識別這個客戶端。SaveTokens用于保存從IdentityServer獲取的token至cookie,「ture」標識ASP.NETCore將會自動存儲身份認證session的access和refresh token。
3.3.4 配置管道
然后要確保認證服務執(zhí)行對每個請求的驗證,加入UseAuthentication和UseAuthorization到Configure中,在startup.cs中Configure方法添加如下代碼:
????????public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env){if?(env.IsDevelopment()){app.UseDeveloperExceptionPage();}else{app.UseExceptionHandler("/Home/Error");}app.UseStaticFiles();app.UseRouting();app.UseCookiePolicy();app.UseAuthentication();app.UseAuthorization();app.UseEndpoints(endpoints?=>{endpoints.MapControllerRoute(name:?"default",pattern:?"{controller=Home}/{action=Index}/{id?}");});} ?UseAuthentication將身份驗證中間件添加到管道中;
UseAuthorization 將啟動授權中間件添加到管道中,以便在每次調用主機時執(zhí)行身份驗證授權功能。
?3.3.5 添加授權
在HomeController控制器并添加[Authorize]特性到其中一個方法。在進行請求的時候,需進行認證授權通過后,才能進行訪問。
????????[Authorize]public?IActionResult?Privacy(){ViewData["Message"]?=?"Secure?page.";return?View();}還要修改主視圖以顯示用戶的Claim以及cookie屬性。
@using?Microsoft.AspNetCore.Authentication<h2>Claims</h2><dl>@foreach?(var?claim?in?User.Claims){<dt>@claim.Type</dt><dd>@claim.Value</dd>} </dl><h2>Properties</h2><dl>@foreach?(var?prop?in?(await?Context.AuthenticateAsync()).Properties.Items){<dt>@prop.Key</dt><dd>@prop.Value</dd>} </dl>訪問 Privacy 頁面,跳轉到認證服務地址,進行賬號密碼登錄,Logout 用于用戶的注銷操作。
3.3.6 添加資源訪問
在HomeController控制器添加對API資源訪問的接口方法。在進行請求的時候,訪問API受保護資源。
????????///?<summary>///?測試請求API資源(api1)///?</summary>///?<returns></returns>public?async?Task<IActionResult>?getApi(){var?client?=?new?HttpClient();var?accessToken?=?await?HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);if?(string.IsNullOrEmpty(accessToken)){return?Json(new?{?msg?=?"accesstoken?獲取失敗"?});}client.DefaultRequestHeaders.Authorization?=?new?AuthenticationHeaderValue("Bearer",?accessToken);var?httpResponse?=?await?client.GetAsync("http://localhost:5003/api/identity/GetUserClaims");?var?result?=?await?httpResponse.Content.ReadAsStringAsync();if?(!httpResponse.IsSuccessStatusCode){return?Json(new?{?msg?=?"請求 api1 失敗。",?error?=?result?});}return?Json(new{msg?=?"成功",data?=?JsonConvert.DeserializeObject(result)});} ?測試這里通過獲取accessToken之后,設置client請求頭的認證,訪問API資源受保護的地址,獲取資源。
?3.4 效果
3.4.1 動圖
3.4.2 過程
在用戶訪問MVC程序時候,將用戶導向認證服務器,
在客戶端向授權服務器Authorization Endpoint進行驗證的時候,我們可以發(fā)現,向授權服務器發(fā)送的請求附帶的那些參數就是我們之前說到的數據(clientid,redirect_url,type等)
繼續(xù)往下看,發(fā)現在用戶給予授權完成登錄之后,可以看到在登錄后,授權服務器向重定向URL地址同時帶上一個授權碼數據帶給MVC程序。
隨后MVC向授權客戶端的Token終結點發(fā)送請求,從下圖可以看到這次請求包含了client_id,client_secret,code,grant_type和redirect_uri,向授權服務器申請訪問令牌token, 并且在響應中可以看到授權服務器核對了授權碼和重定向地址URI,確認無誤后,向第三方應用發(fā)送訪問令牌(Access Token)和更新令牌(Refresh token)
完成獲取令牌后,訪問受保護資源的時候,帶上令牌請求訪問,可以成功響應獲取用戶信息資源。
四、總結
本篇主要闡述以「授權碼授權」,編寫一個MVC客戶端,并通過客戶端以瀏覽器的形式請求「IdentityServer」上請求獲取訪問令牌,從而訪問受保護的API資源。
「授權碼模式」解決了簡化模式由于token攜帶在url中,安全性方面不能保證問題,而通過授權碼的模式不直接返回token,而是先返回一個授權碼,然后再根據這個授權碼去請求token,這個請求token這個過程需要放在后臺,這種方式也更為安全。適用于有后端的應用。
在后續(xù)會對這方面進行介紹繼續(xù)說明,數據庫持久化問題,以及如何應用在API資源服務器中和配置在客戶端中,會進一步說明。
如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
項目地址https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/AuthorizationCode
五、附加
「OpenID Connect」資料
「Authorization Code資料」
「samesite問題解決」
總結
以上是生活随笔為你收集整理的IdentityServer4系列 | 授权码模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#开源项目:SiMay远程控制管理系统
- 下一篇: 聊一聊ABP vNext的模块化系统