ASP.NET Core3.1使用IdentityServer4中间件系列随笔(四):创建使用[ResourceOwnerPassword-资源所有者密码凭证]授权模式的客户端

配套源码: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" }
}
View Code

再添加一个用户的集合(测试数据来自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, "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 = "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, "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")
                    }
                }
            };
    }
}
View Code

我们使用Postman来获取ResourceOwnerPassword这种模式的AcceccToken

与上一种 Client Credentials 模式不同的是 client_id 使用 client1,grant_type 由原来的 client_credentials 改为 password,多了 usernamepassword 两个参数,使用用户名密码 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获取到结果。

原文地址:https://www.cnblogs.com/jardeng/p/12776317.html