目錄
前言:
- 資源所有者密碼憑證(例如用戶名和密碼)直接被用來請求 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的解決方案和Api、IdentityServer兩個項目。
打開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
表示的是用戶信息的端點
。