本篇將創建使用[ResourceOwnerPassword-資源所有者密碼憑證]授權模式的客戶端,來對受保護的API資源進行訪問。
接上一篇項目,在IdentityServer項目Config.cs中添加一個客戶端
/// 資源所有者密碼憑證(ResourceOwnerPassword) /// Resource Owner其實就是User,所以可以直譯爲用戶名密碼模式。 /// 密碼模式相較於客戶端憑證模式,多了一個參與者,就是User。 /// 通過User的用戶名和密碼向Identity Server申請訪問令牌。 new Client { ClientId = "client1", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" } }
再添加一個用戶的集合(測試數據來自IdentityServer官方)。
完整的Config.cs代碼
using System.Collections.Generic; using System.Security.Claims; using IdentityModel; using IdentityServer4.Models; using IdentityServer4.Test; namespace IdentityServer { /// <summary> /// IdentityServer資源和客戶端配置文件 /// </summary> public static class Config { /// <summary> /// API資源集合 /// 如果您將在生產環境中使用此功能,那麼給您的API取一個邏輯名稱就很重要。 /// 開發人員將使用它通過身份服務器連接到您的api。 /// 它應該以簡單的方式向開發人員和用戶描述您的api。 /// </summary> public static IEnumerable<ApiResource> Apis => new List<ApiResource> { new ApiResource("api1", "My API") }; /// <summary> /// 客戶端集合 /// </summary> public static IEnumerable<Client> Clients => new List<Client> { /// 客戶端模式(Client Credentials) /// 可以將ClientId和ClientSecret視爲應用程序本身的登錄名和密碼。 /// 它將您的應用程序標識到身份服務器,以便它知道哪個應用程序正在嘗試與其連接。 new Client { //客戶端標識 ClientId = "client", //沒有交互用戶,使用clientid/secret進行身份驗證,適用於和用戶無關,機器與機器之間直接交互訪問資源的場景。 AllowedGrantTypes = GrantTypes.ClientCredentials, //認證密鑰 ClientSecrets = { new Secret("secret".Sha256()) }, //客戶端有權訪問的作用域 AllowedScopes = { "api1" } }, /// 資源所有者密碼憑證(ResourceOwnerPassword) /// Resource Owner其實就是User,所以可以直譯爲用戶名密碼模式。 /// 密碼模式相較於客戶端憑證模式,多了一個參與者,就是User。 /// 通過User的用戶名和密碼向Identity Server申請訪問令牌。 new Client { ClientId = "client1", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" } } }; /// <summary> /// 用戶集合 /// </summary> public static List<TestUser> Users => new List<TestUser> { new TestUser{SubjectId = "818727", Username = "alice", Password = "alice", Claims = { new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "[email protected]"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) } }, new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob", Claims = { new Claim(JwtClaimTypes.Name, "Bob Smith"), new Claim(JwtClaimTypes.GivenName, "Bob"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "[email protected]"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://bob.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), new Claim("location", "somewhere") } } }; } }
我們使用Postman來獲取ResourceOwnerPassword這種模式的AcceccToken
與上一種 Client Credentials 模式不同的是 client_id 使用 client1,grant_type 由原來的 client_credentials 改爲 password,多了 username 和 password 兩個參數,使用用戶名密碼 alice / alice 來登錄
2、創建一個名爲 ResourceOwnerPasswordConsoleApp 的控制檯客戶端應用。
創建完成後的項目截圖
3、添加nuget包:IdentityModel
在Program.cs編寫代碼
using System; using System.Net.Http; using System.Threading.Tasks; using IdentityModel.Client; using Newtonsoft.Json.Linq; namespace ResourceOwnerPasswordConsoleApp { class Program { static async Task Main(string[] args) { bool verifySuccess = false; TokenResponse tokenResponse = null; while (!verifySuccess) { Console.WriteLine("請輸入用戶名:"); string userName = Console.ReadLine(); Console.WriteLine("請輸入密碼:"); string password = Console.ReadLine(); //discovery endpoint - 發現終結點 HttpClient client = new HttpClient(); DiscoveryDocumentResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000"); if (disco.IsError) { Console.WriteLine($"[DiscoveryDocumentResponse Error]: {disco.Error}"); return; } //request assess token - 請求訪問令牌 tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = disco.TokenEndpoint, ClientId = "client1", ClientSecret = "secret", Scope = "api1", UserName = userName, Password = password }); if (tokenResponse.IsError) { //ClientId 與 ClientSecret 錯誤,報錯:invalid_client //Scope 錯誤,報錯:invalid_scope //UserName 與 Password 錯誤,報錯:invalid_grant string errorDesc = tokenResponse.ErrorDescription; if (string.IsNullOrEmpty(errorDesc)) errorDesc = ""; if (errorDesc.Equals("invalid_username_or_password")) { Console.WriteLine("用戶名或密碼錯誤,請重新輸入!"); } else { Console.WriteLine($"[TokenResponse Error]: {tokenResponse.Error}, [TokenResponse Error Description]: {errorDesc}"); } Console.WriteLine(""); continue; } else { Console.WriteLine(""); Console.WriteLine($"Access Token: {tokenResponse.AccessToken}"); verifySuccess = true; } } //call API Resource - 訪問API資源 HttpClient apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse?.AccessToken); HttpResponseMessage response = await apiClient.GetAsync("http://localhost:6000/weatherforecast"); if (!response.IsSuccessStatusCode) { Console.WriteLine($"API Request Error, StatusCode is : {response.StatusCode}"); } else { string content = await response.Content.ReadAsStringAsync(); Console.WriteLine(""); Console.WriteLine($"Result: {JArray.Parse(content)}"); } Console.ReadKey(); } } }
用戶名密碼錯誤的話,會一直提示重新輸入
我們使用用戶名密碼 alice / alice 進行登錄
可以看到,成功獲取到AccessToken,並使用AccessToken訪問到受保護的API獲取到結果。