配套源码: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
{
/// <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 = "", 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, "AliceSmith@email.com"),
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 = "", 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, "BobSmith@email.com"),
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获取到结果。