使用 IdentityServer4 资源所有者密码凭证(ResourceOwnerPassword)访问受保护资源

前言:

  • 资源所有者密码凭证(例如用户名和密码)直接被用来请求 Access Token
  • 通常用于遗留的应用
  • 资源所有者和客户端应用之前必须高度信任
  • 其他授权方式不可用的时候才使用,尽量不用


一、创建项目

创建项目时用的命令:

$ mkdir Tutorial-Plus
$ cd Tutorial-Plus
$ mkdir src
$ cd src
$ dotnet new api -n Api
$ dotnet new is4inmem -n IdentityServer
$ cd ..
$ dotnet new sln -n Tutorial-Plus
$ dotnet sln add ./src/Api/Api.csproj
$ dotnet sln add ./src/IdentityServer/IdentityServer.csproj

此时创建好了名为Tutorial-Plus的解决方案和ApiIdentityServer两个项目。
打开Tutorial-Plus解决方案,并创建名为WpfClient的WPF项目。

二、Api 项目

修改 Api 项目启动端口为 5001

1) 配置 Startup.cs

将 Api 项目的 Startup.cs 修改为如下。

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvcCore().AddAuthorization().AddJsonFormatters();
            services.AddAuthentication("Bearer")
                .AddJwtBearer("Bearer", options =>
                {
                    options.Authority = "http://localhost:5000"; // IdentityServer的地址
                    options.RequireHttpsMetadata = false; // 不需要Https

                    options.Audience = "api1"; // 和资源名称相对应
                });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
            app.UseMvc();
        }
    }

2) IdentityController.cs 文件

将 Controllers 文件夹中的 ValuesController.cs 改名为 IdentityController.cs
并将其中代码修改了如下:

    [Route("[controller]")]
    [ApiController]
    [Authorize]
    public class IdentityController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
        }
    }

三、IdentityServer 项目

修改 IdentityServer 项目启动端口为 5000

1) 将 json config 修改为 code config

在 IdentityServer 项目的 Startup.cs 文件的 ConfigureServices 方法中,
找到以下代码:

    // in-memory, code config
    //builder.AddInMemoryIdentityResources(Config.GetIdentityResources());
    //builder.AddInMemoryApiResources(Config.GetApis());
    //builder.AddInMemoryClients(Config.GetClients());
    
    // in-memory, json config
    builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
    builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
    builder.AddInMemoryClients(Configuration.GetSection("clients"));

将其修改为

    // in-memory, code config
    builder.AddInMemoryIdentityResources(Config.GetIdentityResources());
    builder.AddInMemoryApiResources(Config.GetApis());
    builder.AddInMemoryClients(Config.GetClients());
    
    // in-memory, json config
    //builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
    //builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
    //builder.AddInMemoryClients(Configuration.GetSection("clients"));

以上修改的内容为将原来写在配置文件中的配置,改为代码配置。

2) 修改 Config.cs 文件

将 Config.cs 文件的 GetIdentityResources() 方法修改为如下:

    // 被保护的 IdentityResource
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new IdentityResource[]
        {
            // 如果要请求 OIDC 预设的 scope 就必须要加上 OpenId(),
            // 加上他表示这个是一个 OIDC 协议的请求
            // Profile Address Phone Email 全部是属于 OIDC 预设的 scope
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResources.Address(),
            new IdentityResources.Phone(),
            new IdentityResources.Email()
        };
    }

将 Config.cs 文件的 GetClients() 方法修改为如下:

    public static IEnumerable<Client> GetClients()
    {
        return new[] 
        {
            new Client
            {
                ClientId = "wpf client",
                ClientName = "Client Credentials Client",
    
                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
    
                AllowedScopes = {
                    "api1",
                    IdentityServerConstants.StandardScopes.OpenId,
                    //IdentityServerConstants.StandardScopes.Profile,
                    //IdentityServerConstants.StandardScopes.Address,
                    //IdentityServerConstants.StandardScopes.Phone,
                    //IdentityServerConstants.StandardScopes.Email,
                }
            },
        };
    }

在上面的代码中,我们将AllowedScopes属性只配置上一个OpenId,
那么用户的 OIDC 预设的 scope 信息,只能得到 Id,
如果加上其他的,客户端中也需要加,然后客户端获取数据时,会相应增加数据。
具体信息就查看 Requesting Claims using Scope Values

四、WpfClient 项目

添加 NuGet 包 IdentityModel。

1) 修改 MainWindow.xaml 文件

将 MainWindow.xaml 文件修改为能够输入账号密码,能够用输入的账号密码去请求 Access Token,
能够用 Access Token 去请求 ApiResource,能够用 Access Token 去请求IdentityResource
具体代码如下:

<Window x:Class="WpfClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfClient"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="70" />
            <RowDefinition Height="40" />
            <RowDefinition />
            <RowDefinition Height="40" />
            <RowDefinition />
            <RowDefinition Height="40" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Margin="20" Orientation="Horizontal">
            <Label>用户名:</Label>
            <TextBox x:Name="UserNameInput" Margin="20 0" Width="150" Height="20" Text="alice" />
            <Label>密码:</Label>
            <PasswordBox x:Name="PasswordInput" Margin="20 0" Width="150"  Height="20" Password="alice"/>
        </StackPanel>
        <Button Grid.Row="1" Click="RequestAccessToken_ButtonClick">1. 请求 Access Token</Button>
        <TextBox Grid.Row="2" 
                 x:Name="AccessTokenTextBlock" 
                 IsReadOnly="True"
                 AcceptsReturn="True" 
                 AcceptsTab="True" />
        <Button Grid.Row="3" Click="RequesApi1Resource_ButtonClick">2. 请求API1资源</Button>
        <TextBox Grid.Row="4" 
                 x:Name="Api1ResponseTextBlock" 
                 VerticalAlignment="Stretch"
                 HorizontalAlignment="Stretch"
                 IsReadOnly="True"
                 AcceptsReturn="True" 
                 AcceptsTab="True" />
        <Button Grid.Row="5" Click="RequestIdentityResource_ButtonClick">3. 请求Identity资源</Button>
        <TextBox Grid.Row="6" 
                 x:Name="IdentityResponseTextBlock" 
                 VerticalAlignment="Stretch"
                 HorizontalAlignment="Stretch"
                 IsReadOnly="True"
                 AcceptsReturn="True" 
                 AcceptsTab="True" />
    </Grid>
</Window>

2) 修改 MainWindow.xaml.cs 文件

WPF的界面写完以后,MainWindow.xaml.cs 的代码应该是如下所示:

    public partial class MainWindow : Window
    {
        private DiscoveryResponse _disco;
        private string _accessToken;

        public MainWindow() { InitializeComponent(); }

        private async void RequestAccessToken_ButtonClick(object sender, RoutedEventArgs e) { }
        private async void RequesApi1Resource_ButtonClick(object sender, RoutedEventArgs e) { }
        private async void RequestIdentityResource_ButtonClick(object sender, RoutedEventArgs e) { }
    }

然后对 请求Access Token 的按钮的点击事件,
也就是 RequestAccessToken_ButtonClick 方法进行代码写入:

    var userName = UserNameInput.Text;
    var passWord = PasswordInput.Password;
    
    var client = new HttpClient();
    _disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
    
    if (_disco.IsError)
    {
        Console.WriteLine(_disco.Error);
        return;
    }
    
    // request access token
    var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
    {
        Address = _disco.TokenEndpoint,
        ClientId = "wpf client",
        ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A",
        Scope = "api1 openid", // profile address phone email
    
        UserName = userName,
        Password = passWord
    });
    
    if (tokenResponse.IsError)
    {
        MessageBox.Show(tokenResponse.Error);
        return;
    }
    _accessToken = tokenResponse.AccessToken;
    AccessTokenTextBlock.Text = tokenResponse.Json.ToString();

以上代码展示了如何用账号密码去请求 Access Token。

然后对 请求Api1资源 的按钮的点击事件,
也就是 RequesApi1Resource_ButtonClick 方法进行代码写入:

    // call API1 Resource
    var apiClient = new HttpClient();
    apiClient.SetBearerToken(_accessToken);
    var response = await apiClient.GetAsync("http://localhost:5001/identity");
    if (!response.IsSuccessStatusCode)
    {
        MessageBox.Show(response.StatusCode.ToString());
    }
    else
    {
        var content = await response.Content.ReadAsStringAsync();
        Api1ResponseTextBlock.Text = content;
    }

以上代码展示拥有 Access Token 的用户,如何用 Access Token 去获取 ApiResource。

后面对 请求Identity资源 的按钮的点击事件,
也就是 RequestIdentityResource_ButtonClick 方法进行代码写入:

    // call Identity Resource from Identity Server
    var apiClient = new HttpClient();
    apiClient.SetBearerToken(_accessToken);
    var response = await apiClient.GetAsync(_disco.UserInfoEndpoint);
    
    if (!response.IsSuccessStatusCode)
    {
        MessageBox.Show(response.StatusCode.ToString());
    }
    else
    {
        var content = await response.Content.ReadAsStringAsync();
        IdentityResponseTextBlock.Text = content;
    }

以上代码展示拥有 Access Token 的用户,如何用 Access Token 去获取 IdentityResource。
上面代码中的 _disco.UserInfoEndpoint 表示的是用户信息的端点


参考文档

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章