從上一篇關於客戶端憑證模式中,咱們經過建立一個認證受權訪問服務,定義一個API和要訪問它的客戶端,客戶端經過IdentityServer上請求訪問令牌,並使用它來控制訪問API。其中,咱們也注意到了在4.x版本中於以前3.x版本之間存在的差別。javascript
因此在這一篇中,咱們將經過多種受權模式中的資源全部者密碼憑證受權模式進行說明,主要針對介紹IdentityServer保護API的資源,資源密碼憑證受權訪問API資源。html
若是你高度信任某個應用Client,也容許用戶把用戶名和密碼,直接告訴該應用Client。該應用Client就使用你的密碼,申請令牌,這種方式稱爲"密碼式"(password)。java
這種模式適用於鑑權服務器與資源服務器是高度相互信任的,例如兩個服務都是同個團隊或者同一公司開發的。git
資源全部者密碼憑證受權模式,適用於當資源全部者與客戶端具備良好信任關係的場景,好比客戶端是設備的操做系統或具有高權限的應用。受權服務器在開放此種受權模式時必須格外當心,而且只有在別的模式不可用時才容許這種模式。github
這種模式下,應用client可能存了用戶密碼這不安全性問題,因此才須要高可信的應用。數據庫
主要適用於用來作遺留項目升級爲oauth2的適配受權使用,固然若是client是自家的應用,也是能夠的,同時支持refresh token。c#
例如,A站點 須要添加了 OAuth 2.0 做爲對其現有基礎架構的一個受權機制。對於現有的客戶端轉變爲這種受權方案,資源全部者密碼憑據受權將是最方便的,由於他們只需使用現有的賬戶詳細信息(好比用戶名和密碼)來獲取訪問令牌。api
+----------+ | Resource | | Owner | | | +----------+ v | Resource Owner (A) Password Credentials | v +---------+ +---------------+ | |>--(B)---- Resource Owner ------->| | | | Password Credentials | Authorization | | Client | | Server | | |<--(C)---- Access Token ---------<| | | | (w/ Optional Refresh Token) | | +---------+ +---------------+
資源全部者密碼憑證受權流程描述安全
(A)資源全部者向客戶端提供其用戶名和密碼。服務器
(B)客戶端從受權中請求訪問令牌服務器的令牌端點,以獲取訪問令牌。當發起該請求時,受權服務器須要認證客戶端的身份。
(C) 受權服務器驗證客戶端身份,同時也驗證資源全部者的憑據,若是都經過,則簽發訪問令牌。
參數 | 是否必須 | 含義 |
---|---|---|
grant_type | 必需 | 受權類型,值固定爲「password」。 |
username | 必需 | 用戶名 |
password | 必需 | 密碼 |
scope | 可選 | 表示受權範圍。 |
同時將容許其餘請求參數client_id和client_secret,或在HTTP Basic auth標頭中接受客戶端ID和密鑰。
驗證用戶名密碼
示例:
客戶端身份驗證兩種方式
一、Authorization: Bearer base64(resourcesServer:123)
二、client_id(客戶端標識),client_secret(客戶端祕鑰),username(用戶名),password(密碼)。
(用戶的操做:輸入帳號和密碼)
A 網站要求用戶提供 B 網站的用戶名和密碼。拿到之後,A 就直接向 B 請求令牌。
POST /oauth/token HTTP/1.1 Host: authorization-server.com grant_type=password &username=user@example.com &password=1234luggage &client_id=xxxxxxxxxx &client_secret=xxxxxxxxxx
上面URL中,grant_type參數是受權方式,這裏的password是「密碼式」,username和password是B的用戶名和密碼。
第二步,B 網站驗證身份經過後,直接給出令牌。注意,這時不須要跳轉,而是把令牌放在 JSON 數據裏面,做爲 HTTP 迴應,A 所以拿到令牌。
響應給用戶令牌信息(access_token),以下所示
{ "access_token": "訪問令牌", "token_type": "Bearer", "expires_in": 4200, "scope": "server", "refresh_token": "刷新令牌" }
用戶使用這個令牌訪問資源服務器,當令牌失效時使用刷新令牌去換取新的令牌。
這種方式須要用戶給出本身的用戶名/密碼,顯然風險很大,所以只適用於其餘受權方式都沒法採用的狀況,並且必須是用戶高度信任的應用。
在示例實踐中,咱們將建立一個受權訪問服務,定義一個API和要訪問它的客戶端,客戶端經過IdentityServer上請求訪問令牌,並使用它來訪問API。
搭建認證受權服務
IdentityServer4
程序包
創建配置內容文件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("password_scope1") }; public static IEnumerable<ApiResource> ApiResources => new ApiResource[] { new ApiResource("api1","api1") { Scopes={ "password_scope1" }, ApiSecrets={new Secret("apipwd".Sha256())} //api密鑰 } }; public static IEnumerable<Client> Clients => new Client[] { new Client { ClientId = "password_client", ClientName = "Resource Owner Password", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) }, AllowedScopes = { "password_scope1" } }, }; }
由於是資源全部者密碼憑證受權的方式,因此咱們經過代碼的方式來建立幾個測試用戶。
新建測試用戶文件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服務中,接下來繼續介紹。
在startup.cs中ConfigureServices方法添加以下代碼:
public void ConfigureServices(IServiceCollection services) { var builder = services.AddIdentityServer() .AddTestUsers(TestUsers.Users); //添加測試用戶 // in-memory, code config builder.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 secure builder.AddDeveloperSigningCredential(); }
在startup.cs中Configure方法添加以下代碼:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseIdentityServer(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); }
以上內容是快速搭建簡易IdentityServer項目服務的方式。
這搭建 Authorization Server 服務跟上一篇客戶端憑證模式有何不一樣之處呢?
- 在Config中配置客戶端(client)中定義了一個
AllowedGrantTypes
的屬性,這個屬性決定了Client能夠被哪一種模式被訪問,GrantTypes.ClientCredentials爲客戶端憑證模式,GrantTypes.ResourceOwnerPassword爲資源全部者密碼憑證受權。因此在本文中咱們須要添加一個Client用於支持資源全部者密碼憑證模式(ResourceOwnerPassword)。- 由於資源全部者密碼憑證模式須要用到用戶名和密碼因此要添加用戶,而客戶端憑證模式不須要,這也是二者的不一樣之處。
實現對API資源進行保護
IdentityServer4.AccessTokenValidation 包
在startup.cs中ConfigureServices方法添加以下代碼:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddAuthorization(); 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中,供身份認證服務使用。
在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 將啓動受權中間件添加到管道中,以便在每次調用主機時執行身份驗證受權功能。
[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] , 在進行請求資源的時候,需進行認證受權經過後,才能進行訪問。
這搭建API資源跟上一篇客戶端憑證模式有何不一樣之處呢?
咱們能夠發現這跟上一篇基本類似,可是可能須要注意的地方應該是
ApiName
和ApiSecret
,要跟你配置的API資源名稱和API資源密鑰相同。
實現對API資源的訪問和獲取資源
IdentityModel 包
客戶端與受權服務器進行身份驗證並向令牌端點請求訪問令牌。受權服務器對客戶端進行身份驗證,若是有效,頒發訪問令牌。
IdentityModel 包括用於發現 IdentityServer 各個終結點(EndPoint)的客戶端庫。
咱們可使用從 IdentityServer 元數據獲取到的Token終結點請求令牌:
private void getToken_Click(object sender, EventArgs e) { var client = new HttpClient(); var disco = client.GetDiscoveryDocumentAsync(this.txtIdentityServer.Text).Result; if (disco.IsError) { this.tokenList.Text = disco.Error; return; } //請求token tokenResponse = client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = disco.TokenEndpoint, ClientId =this.txtClientId.Text, ClientSecret = this.txtClientSecret.Text, Scope = this.txtApiScopes.Text, UserName=this.txtUserName.Text, Password=this.txtPassword.Text }).Result; if (tokenResponse.IsError) { this.tokenList.Text = disco.Error; return; } this.tokenList.Text = JsonConvert.SerializeObject(tokenResponse.Json); this.txtToken.Text = tokenResponse.AccessToken; }
要將Token發送到API,一般使用HTTP Authorization標頭。 這是使用
SetBearerToken
擴展方法完成。
private void getApi_Click(object sender, EventArgs e) { //調用認證api if (string.IsNullOrEmpty(txtToken.Text)) { MessageBox.Show("token值不能爲空"); return; } var apiClient = new HttpClient(); //apiClient.SetBearerToken(tokenResponse.AccessToken); apiClient.SetBearerToken(this.txtToken.Text); var response = apiClient.GetAsync(this.txtApi.Text).Result; if (!response.IsSuccessStatusCode) { this.resourceList.Text = response.StatusCode.ToString(); } else { this.resourceList.Text = response.Content.ReadAsStringAsync().Result; } }
這搭建Client客戶端跟上一篇客戶端憑證模式有何不一樣之處呢?
- 客戶端請求token多了兩個參數,一個用戶名,一個密碼
- 請求Token中使用
IdentityModel
包的方法RequestPasswordTokenAsync
,實現用戶密碼方式獲取令牌。
以上展現的代碼有不明白的,能夠看本篇項目源碼,項目地址爲 :資源全部者密碼憑證模式
從上一篇的客戶端憑證模式到這一篇的資源全部者資源密碼憑證模式,咱們都已經初步掌握了大體的受權流程,以及項目搭建獲取訪問受保護的資源,可是咱們也可能發現了,若是是僅僅爲了訪問保護的API資源的話,加不加用戶和密碼好像也沒什麼區別呢。
可是若是仔細對比兩種模式在獲取token,以及訪問api返回的數據能夠發現,資源全部者密碼憑證模式返回的Claim的數量信息要多一些,可是客戶端模式返回的明顯少了一些,這是由於客戶端不涉及用戶信息。因此資源密碼憑證模式
能夠根據用戶信息作具體的資源權限判斷。
好比,在TestUser有一個Claims屬性,容許自已添加Claim,有一個ClaimTypes枚舉列出了能夠直接添加的Claim。因此咱們能夠爲用戶設置角色,來判斷角色的權限功能,作簡單的權限管理。
在以前建立的TestUsers.cs
文件的User
方法中,添加Cliam的角色熟悉,以下:
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), new Claim(JwtClaimTypes.Role,"admin") //添加角色 }, } }; } } }
由於要用到ApiResources
,ApiResources
的構造函數有一個重載支持傳進一個Claim集合,用於容許該Api資源能夠攜帶那些Claim, 因此在項目下的Config
類的ApiResources
作出以下修改:
public static IEnumerable<ApiResource> ApiResources => new ApiResource[] { new ApiResource("api1","api1") { Scopes={ "password_scope1" }, UserClaims={JwtClaimTypes.Role}, //添加Cliam 角色類型 ApiSecrets={new Secret("apipwd".Sha256())} } };
在API資源項目中,修改下被保護Api的,使其支持Role驗證。
[HttpGet("getUserClaims")] //[Authorize] [Authorize(Roles ="admin")] public IActionResult GetUserClaims() { return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); }
能夠看到,爲咱們添加了一個Role Claim,效果以下: