目录
前言:
- 资源所有者密码凭证(例如用户名和密码)直接被用来请求 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
表示的是用户信息的端点
。