使用 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 表示的是用戶信息的端點


參考文檔

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