配套源码:https://gitee.com/jardeng/IdentitySolution
本篇将创建使用[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 { ////// IdentityServer资源和客户端配置文件 /// public static class Config { /// /// API资源集合 /// 如果您将在生产环境中使用此功能,那么给您的API取一个逻辑名称就很重要。 /// 开发人员将使用它通过身份服务器连接到您的api。 /// 它应该以简单的方式向开发人员和用户描述您的api。 /// public static IEnumerable Apis => new List { new ApiResource("api1", "My API") }; /// /// 客户端集合 /// public static IEnumerable Clients => new List { /// 客户端模式(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" } } }; /// /// 用户集合 /// public static List Users => new List { 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获取到结果。