2. 快速开始
2.1 概览
快速开始会一步一步的带你实现各种 IdentityServer 的使用场景. 整个过程从零基础开始逐步变得复杂. 建议按照顺序进行.
- 将 IdentityServer 添加到 ASP.NET Core 应用程序
- 配置 IdentityServer
- 为各类客户端颁发 token
- 加密 Web 应用程序以及 API
- 添加 EF 的支持
- 添加 Identity 的支持
每一个快速开始都有解决方案, 可以在案例文件夹中找到.
2.1.1 准备
首先是要安装模板
dotnet new -i IdentityServer4.Templates
我们会使用它来表示各个案例的起点.
如果你使用私有的 Nuget 仓库, 别忘记添加 -nuget-source 参数: -ngget-source https://api.nuget.org/v3/index.json
下面准备开始.
快速开始使用 IdentityServer4 和 ASP.NET Core 3.1. 这同样适用于 ASP.NET Core 2 和 1.
2.2 使用客户端验证来保护API
Protecting an API using Client Credentials
建议按照步骤一步步实现.
第一个案例是使用 IdentityServer 保护 API 最为基础的情况. 本案例中你会定义一个客户端, 和一个 API. 这两个端均会访问 IdentityServer. 客户端会使用 client id 以及秘钥来访问 IdentityServer, 从而获取 access token. 然后使用 access token 来访问 API.
2.2.1 源代码
可以在 IdentityServer4 的仓库中找到源代码. 该项目为 Quickstart #1: Securing an API using Client Credentials.
2.2.2 准备
安装模板包
dotnet new -i IdentityServer4.Templates
这是所有 demo 的起点.
2.2.3 创建 ASP.NET Core 应用程序
首先为应用程序创建一个目录. 然后使用模板创建 ASP.NET Core 应用程序, 其中会包含 IdentityServer 的基本设置
md quickstart
cd quickstart
md src
cd src
dotnet new is4empty -n IdentityServer
它将创建下面文件:
IdentityServer.csproj
这是项目文件, 以及Properties/launchSettings.json
文件.Program.cs
和Startup.cs
文件, 这是程序的入口文件.Config.cs
文件, 这是 IdentityServer 资源的配置文件.
现在你可以使用你喜欢的文本编辑器来打开它. 如实想要使用 VS, 还需要创建解决方案文件.
cd ..
dotnet new sln -o QuickStart
然后将 IdentityServer 添加到解决方案中 (记住这条命令, 后面会反复用到).
dotnet sln add .\src\IdentityServer\IdentityServer.csproj
注意:
模板使用的协议是 https, 在 Kestrel 上端口是 5001, 在 IISExpress 上使用随机端口. 你可以修改 launchSettings.json 来修改这个数据, 请记住, 在生产环境中一定使用 https.
简要描述生成的文件
Program.cs
文件中主要配置了日志方法.
Startup.cs
文件中添加了 IdentityServer 的服务, 并启用 IdentityServer 中间件.
Config.cs
是配置文件, 注入 IdentityServer 服务时用到了. 其中定义了 资源 (IdentityResources
), ApiScope
, Clients
.
2.3.4 定义 API Scope
API 是你需要保护的资源. 加载资源的方式有很多. 模板中使用 代码即配置 的方法来实现.
jk: 实际上就是硬编码.
Config.cs
文件已经创建好, 打开编辑即可:
public static class Config {
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope> {
new ApiScope("api1", "My API")
};
}
完整代码可以参考这里
建议将名字设置得更加符合逻辑, 开发者会使用该名字, 通过你的 IdentityServer 来访问 API.
2.3.5 定义客户端
接下来定义客户端, 该客户端用于访问你的 API.
在这种情况下, 客户端不需要用户交互, 并使用客户端秘钥在 IdentityServer 中进行身份验证 (鉴权).
添加客户端定义:
public static IEnumerable<Client> Clients => new List<Client> {
new Client {
ClientId = "client",
// 没有用户交互, 则使用 客户端ID/秘钥 来鉴权
AllowedGrantTypes = GrantTypes.ClientCredentials,
// 鉴权用的秘钥
ClientSecrets = {
new Secret("secret".Sha256())
},
// 客户端可以访问的 scope
AllowedScopes = { "api1" }
}
};
可以将 客户端 ID 和秘钥理解为用户名和密码. 客户端使用它在 IdentityServer 中登记, 让服务器知道是谁在进行操作.
2.3.6 配置 IdentityServer
在 Startup.cs
文件中加载资源和客户端的定义. 该代码看起来:
public void ConfigureService(IServiceCollection services) {
var builder = services.AddIdentityServer()
.AddDeveloperSigningCredential() // 在没有证书的时候, 适用于开发模式.
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.CLients);
// ...
}
至此程序已经配置完成, 运行后访问: https://localhost:5001/.well-known/openid-configuration. 你会看到被称为发现文档的内容. 这个发现文档是 IdentityServer 中的标准端点. 该发现文档用于客户端与 API 来下载需要的配置数据.
第一次运行时, IdentityServer 会生成签名用的 key, 文件名为 tempkey.jwk
. 不要将该文件上传到你的源代码管理工具中. 该文件若不存在, 会重新创建.
2.3.7 添加 API
下面将 API 添加到解决方案中.
在 src 目录下, 使用 ASP.NET Core 的 webapi 模板来创建.
dotnet new webapi -n Api
将 Api 加入解决方案
cd ..
dotnet sln add ./src/Api/Api.csproj
配置 Api 运行在 https://localhost:6001
. 通过修改 Properties/launchSettings.json
来实现.
{
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:6001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
2.3.7.1 控制器
添加 IdentityController
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase {
[HttpGet]
public IActionResult Get() {
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
马上会使用到该控制器. 并使用它进行用户信息校验 (鉴权).
可以参考代码中会用户信息的获取.
2.3.7.2 添加 Nuget 包的依赖
为了进行配置, 在 Api 目录下添加依赖
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
2.3.7.3 配置
最后一步是将鉴权服务添加到 DI 中. 并将鉴权中间件添加到管道. 如此:
- 验证传入的 token, 来确保来源是受信发布者.
- 校验 token 是否可以访问 API (即所谓的受众).
更新 Startup 类
public class Startup {
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddAuthentication("Bearer") // 添加鉴权方案, 并使用 Bearer 作为默认方案
.AddJwtBearer("Bearer", options => {
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters {
ValidateAudience = false
};
});
}
public void Configure(IApplicationBuilder app) {
// ...
app.UseAuthentication(); // 确保每一个请求都会被鉴权处理
app.UseAuthorization(); // 确保每一个请求都需要授权处理, 即不允许匿名用户访问 [Authorize] 表示的端点
// ...
}
}
然后启动项目, 并访问 identity 端口, 会返回 401 状态码, 即无权访问. 这表示端口被 IdentityServer 保护起来了.
如果想要了解为什么不校验 Audience, 可以[参考](file:///C:/Users/jk/Desktop/Ids4-dosc-notes/identityserver4-latest/index.html#refresources).
2.3.8 创建客户端
最后一步是创建客户端, 该客户端需要请求 access token, 并使用该 token 来访问 Api.
在 src 目录下创建一个控制台项目.
dotnet new console -n Client
cd ..
dotnet sln add ./src/Client/Client.csproj
IdentityServer 的 token 端点实现了 OAuth2.0 协议. 并可以使用原始 HTTP 进行访问. 然而可以使用 IdentityModel 库, 它封装了协议, 提供了友好的 API.
在控制台项目中添加 IdnetityModel Nuget 包.
dotnet add package IdentityModel
IdentityModel 包含使用发现文档的客户端, 只需要 IdentityServer 的基地址. 实际上端点地址可以动元数据中读取.
// discover endpoints from metadata
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001"); // 方法在 IdentityMOdel.Client 中
if (disco.IsError) {
Console.WriteLine(disco.Error);
return;
}
如果出现连接错误, 可能是 https 未被信任. 可以执行
dotnet dev-cert https --trust
.
然后使用发现文档, 从 IdentityServer 中请求来访问 api1 的 token
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest {
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
if (tokenResponse.IsError) {
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
可以将 token 拷贝出来, 放在 https://jwt.ms 来解析查看.
2.3.9 调用 API
要访问 API, 通常将 token 放在 Authorization
请求头中. 这个操作可以使用 SetBearerToken
扩展方法来实现.
// call api
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("https://localhost:6001/identity");
if (!response.IsSuccessStatusCode) {
Console.WriteLine(response.StatusCode);
} else {
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
默认情况下, access token 会包含 有关 scope 的 claim, 生命周期 (nbf 和 exp), client_id, 以及 issuer name (iss).
2.3.10 在 API 中授权
现在, API 会让来自 IdentityServer 的所有 token 通过. 在 Startup 的 ConfigureServices 方法中添加 基于策略的访问控制逻辑.
services.AddAuthorization(options => {
options.AddPolicy("ApiScope", policy => {
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
然后, 你可以将此策略应用于不同级别, 例如:
- 全局
- 针对所有 API 端点
- 针对某个 控制器/动作方法
一般可以在路由系统中, 设置所有 API 端点启用策略:
app.UseEndpoints(endpoints => {
endpoints.MapControllers()
.RequireAuthorization("ApiScope");
});
2.3.11 进一步体验
本案例已经完成的:
- 客户端可以请求 token
- 客户端可以使用 token 访问 API
下面可以通过一些错误来学习系统的行为. 例如:
- 尝试不运行 IdentityServer 来运行系统.
- 尝试使用非法的 client_id 与 secret.
- 尝试在获取 token 时, 使用不正确的 scope.
- 尝试不运行 API 时请求 API.
- 在访问 API 是不提供 token.
- 配置 API 使用不同 scope
2.3 使用 ASP.NET Core 实现交互式应用
本案例中, 基于以提供的 IdentityServer 中, 我们要通过交互式的方式添加一个支持 OpenID Connect 鉴权的用户.
最后, 我们需要一个 MVC 应用, 使用 IdentityServer 来进行鉴权.
2.3.1 添加 UI
所有 OpenID Connect 所需要的协议均集成到 IdentityServer 中了. 你只需要提供需要的 UI, 即 login, logout, 以及相关协议页面.
官方提供了 MVC 模板, 以简化这个过程. UI 仓库在 Quickstart UI repo. 你可以下载或克隆代码, 将其中需要的内容拖入你的项目中使用.
你也可以直接使用 CLI 来创建. 在 src/IdentityServer
文件夹中
dotnet new is4ui
引入 ui 部分后, 需要在 Startup 文件中启用 MVC 相关的服务与中间件. 解开注释即可:
有一个模板 is4inmem
, 它集成了 IdentityServer 以及 标准 UI.
花一些时间了解这个模板中的代码有一定好处, 代码文件的命名基本与功能命名一致. 然后可以将项目运行起来
2.3.2 创建 MVC 客户端
然后创建 MVC 的项目.
dotnet new mvc -n MvcClient
cd ..
dotnet sln add ./src/MvcClient/MvcClient.csproj
我们建议使用 self-host 模式, 而不是使用 IISExpress. 将项目固定在 5002 端口上.
在 Mvc 项目中集成 OpenID Connect 鉴权, 需要安装包
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
然后配置 Startup
文件中的 ConfigureServices
方法
using System.IdentityModel.Tokens.Jwt;
// ...
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => {
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
});
AddAuthentication
方法将鉴权服务添加到 DI 中.
- 本地使用 cookie 来实现登录 (sign-in) (将 Cookie 设置为 DefaultScheme).
- 然后将 DefaultChallengeScheme 设置为 oidc. 这是因为在需要用户登录 (login) 的时候, 我们会使用 OpenID Connect 协议.
然后使用 AddCookie
方法来添加处理 Cookie 的程序.
最后使用 AddOpenIdConnect
方法来配置处理 OpenID Connect 协议的程序.
Authority
表示本地受信任的 token 服务器地址.- 我们通过
ClientId
和ClientSecret
来标识客户端. SaveToken
表示将来自Token
服务器的 token 保存到 cookie 中 (后面会使用).
我们使用的被称为 authentication code 流的方式, 携带 PKCE, 连接到 OpenID Connect provider.
有关协议流的信息可以参考[这里](file:///C:/Users/jk/Desktop/Ids4-dosc-notes/identityserver4-latest/index.html#refgranttypes)
然后, 为了确保每一个请求可以实现鉴权, 在 Startup
的 Configure
中, 添加 UseAuthentication
.
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapDefaultControllerRoute()
.RequireAuthorization(); // 该方法确保不允许匿名访问
});
同样, 也可以选择使用 [Authorize]
来标记方法, 或控制器.
修改 home 视图, 来显示用户的 claim, 以及 cookie 数据:
@using Microsoft.AspNetCore.Authentication
<h2>Claims</h2>
<dl>
@foreach (var claim in User.Claims) {
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>属性</h2>
<dl>
@foreach (var prop in (await Context.AuthenticateAsync()).Properties!.Items) {
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
然后运行程序, 使用浏览器访问, 会进行重定向 (确保 IdentityServer 处于运行状态), 然后会报错, 因为该客户端未在 IdentityServer 中注册.
2.3.3 添加支持 OpenID Connect 标识作用域
Adding support for OpenID Connect Identity Scope
类似于 OAuth2.0, OpenID Connect 也使用 Scope 这个概念. 同样, Scope 用于表示你需要保护的内容, 以及客户端需要访问的内容.
不同于 OAuth, OIDC 中的 Scope 不表示 API, 而用于标识数据, 例如 id, name, email 等.
修改 Config.cs
文件中的 IdnetityResources
属性, 来添加支持标准的 openid
(subject id) 和 profile
(first name, last name 等) Scope.
public static IEnumerable<IdentityResource> IdentityResources => new IdentityResource[] {
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
然后配置启用资源
var builder = services.AddIdentityServer(options => {
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddInMemoryIdentityResources(Config.IdentityResources); // 这里添加
所有的标准 scope 以及其相关的 claim 可以在 OpenID Connect 规范中找到.
2.3.4 添加 Test User
UI 模板使用基于内存的用户数据库, 可以利用 AddTestUsers
来添加:
var builder = services.Add...()
.AddTestUsers(TestUsers.Users);
jk: TestUsers 是写在代码中的 Mock 数据, 提供两个用户 alice 和 bob, 密码与名字相同, 以供测试.
2.3.5 将 MVC 客户端添加到 IdentityServer 配置中
基于 OIDC 的客户端配置与 OAuth2.0 的配置类似, 的不同的是交互逻辑, 这里需要提供一些重定向的页面.
客户端配置代码如下:
new Client {
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string> {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
2.3.5 测试客户端
再次运行 IdentityServer 和 MvcClient.
访问 MvcClient 的时候就会重定向到 IdentityServer 中, 并在其中完成登录校验与用户信息的展示.
2.3.6 添加 sign-out
最后在 MVC 项目中添加登出的逻辑. 基于 IdentityServer, 仅仅完成本地的 cookie 清除是不够的, 还需要清除中心服务器中的数据.
jk: 考虑单点的情况, 可能在其他站点中依旧可以访问.
具体的实现步骤 IdentityServer 内部已经处理, 只需要在某控制器中使用下面方法即可
public IActionResult Logout() {
return SignOut("Cookies", "oidc");
}
该方法会清除本地 Cookie, 然后重定向到 IdentityServer, 清除中心服务器上的 Cookie, 然后给你一个链接返回到 MVC 应用.
jk: 这里有一些比较迷惑的地方, 其中就是重定向的一些流程.
另外 IdentityServer 是将 OpenID Connect 与 OAuth2.0 合并的协议, 其中协议的一些细节有待整理.
2.3.7 从 UserInfo 端点获取 claims
你可能已经发现, 即使我们在 IS4 中配置了客户端, 并允许获得 profile
identity scope, 但是与 scope 相关的属性并未出现在返回的 token 中, 例如 name, family_name, website 等.
我们需要告诉客户端从 UserInfo 端点获取剩余的 claim 数据, 通过指定客户端需要访问的 scope 以及设置 GetClaimsFromUserInfioEndpoint
来实现.
下面的代码中演示了 profile
scope, 实际上可以是任意的 scope. 记得客户端是需要被验证的.
.AddOpenIdConnect("oidc", options => {
// ...
options.Scope.Add("profile");
options.GetClaimsFromUserInfoEndpoint = true;
// ...
});
然后重启 MvcClient, 退出, 再次登录.
2.3.8 进一步实验
随意添加 claim 数据到测试用户中 - 以及更多的 identity resource.
定义 identity resource 的过程如下:
- 在列表中添加一个新的 identity resource - 并指定一个名字, 以及请求该资源时应返回的 claim.
- 在客户端配置中, 利用
AllowScopes
属性来指定客户端可以访问该资源. - 在客户端中将需要的 Scope 集合通过 OpenID Connect Handler 进行配置, 然后请求资源.
- (可选)若 identity resource 与非标准资源关联, 例如 mycliam1, 在客户端添加 ClaimAction 映射, 将 JSON (UserInfo 端点返回的数据) 中出现的 claim 与 用户 Claim 中出现的名字对应起来.
using Microsoft.AspNetCore.Authentication;
// ...
.AddOpenIdConnect("oidc", options => {
// ...
options.ClaimActions.MapUniqueJsonKey("myclaim1", "myclaim1");
});
值得注意的是, 从 token 中检索 claim 是可扩展的 - IProfileService. 在使用 AddTestUsers 时, 默认会使用 TestUserProfileService.
可以参考源码来看看其如何工作.
2.3.9 添加扩展鉴权 (略)
2.3.10 添加 google 的支持 (略)
2.3.11 进一步实验 (略)
2.4 ASP.NET Core 与 API 的访问
在前面的案例中, 我们探讨了 API 的访问, 以及用户的鉴权. 下面我们将其合并起来.
OpenID Connect 与 OAuth2.0 的结合带来了, 在令牌服务器上, 可以使用单个令牌, 以及单次交换.
现在, 我们只是在请求 token 的时候来获取 identity resource. 一旦我们接入 API 后, IdentityServer 会返回两个 token
- identity token 中包含鉴权与 session 信息.
- access token 代表已登录的用户可以访问 API.
2.4.1 修改客户端配置
在 IdentityServer 中更新客户端很容易, 这里我们只需要在 AllowedScopes
中添加一个 "api1"
即可.
作为扩展, 我们可以设置 AllowOfflineAccess
属性来支持令牌刷新.
new Client {
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowOfflineAccess = true, // 令牌刷新
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1" // 让客户端可以访问 api1
}
}
2.4.2 更新 MVC 客户端
现在剩下要做的, 在客户端中, 就是请求额外的资源, 通过 scope 参数. 在 OIDC Handler 中完成这个配置:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
此时 SaveTokens
已被激活. ASP.NET Core 会自动存储, 并在鉴权会话中刷新. 你可以在页面中展示数据, 打印出前面创建的会话内容.
2.4.3 使用 sccess token
可以使用 ASP.NET Core 扩展方法在会话中读取访问令牌, 该方法在 Microsoft.AspNetCore.Authentication
命名空间中.
var accessToken = await HttpContext.GetTokenAsync("access_token");
使用访问令牌来访问 API, 你需要做的便是读取 token, 并设置你的 HttpClient:
public async Task<IActionResult> CallApi() {
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var content = await client.GetStringAsync("https://localhost:6001/identity");
ViewBag.Json = JArray.Parse(content).ToString();
return View("json");
}
创建 json.cshtml 视图文件:
<pre>@ViewBag.Json</pre>
确保 API 已经运行, 运行 MvcClient, 校验用户后, 访问 /home/CallApi
.
2.4.4 管理 access token
到目前为止, 典型客户机最复杂的任务是管理访问令牌. 通常你会:
- 请求 access token, 并在一段时间后刷新 token
- 缓存 token
- 使用 token 访问 API, 直到其超过生命周期
- 使用刷新 token 来更新 access token
- 重新开始
ASP.NET Core 有很多内置模块, 这些模块可以帮助你完成这些任务 (例如缓存等). 但是还有很多事项可以处理, 参考这个代码, 里面可以自动的完成很多样板任务.
2.5 添加 JavaScript 客户端
本案例要创建一个基于浏览器的 JavaScript 客户端应用程序, 通常是一个 SPA (单页面应用程序).
用户会通过 IdentityServer 登录. 然后通过 IdentityServer 获得的 access token 来访问 API. 同时通过 IdentityServer 来退出.
所有的这些都在浏览器中的 JavaScript 中运行.
2.5.1 JavaScript 客户端新项目
这里与文档中描述的不同.
文档中是创建一个 web 应用, 然后启用 静态文件服务, 最后通过 npm 安装需要的包, 将包移到 wwwdir 目录中, 使用 js 来进行开发
有一点需要注意, 项目运行在 5003 端口.
安装 oidc-client
这里使用 vue 来开发
oidc-client 的仓库在 GitHub.
使用步骤:
准备配置文件, 配置文件中包含
- 鉴权基地址
authority
- 客户端id
client_id
- 响应类型
response_type
, 设置为'code'
scope
, 取值为:openid profile api1
- 登录后重定向地址:
redirect_url: 'https://localhost:5003/callback'
- 退出后重定向:
post_logout_redirect_url: 'https://localhost:5003/index'
- 鉴权基地址
使用配置实例化
UserManager
const mgr = new Oidc.UserManager(config)
UserManager
提供了一个方法getUser
, 可用于判断用户是否登录了 JavaScript 应用. 该方法返回一个Promise
.- 如果用户登录了, 会返回用户信息, 其中含有 profiles 属性, 存储用户的 claims
- 若用户未登录, 则不会返回.
然后, 我们实现
login
,api
, 和logout
函数.UserManager
提供了signinRedirect
方法来实现登录.UserManager
提供了signoutRedirect
方法来实现登出.- 在返回的 user 数据中还有
access_token
属性, 用于存储访问令牌, 将其放在请求头中, 发出 API 的请求.
jk: 这里没有录入前端代码的详细步骤
callback
这个页面是在 IdentityServer 完成之后重定向的位置. 在重定向到该页面中, url 中会携带参数.
jk: 这里还需要研究协议与文档, 猜测是将 url 中数据取出来, 然后发回 IdentityServer 验证, 再将数据存储本地存储中. 最后由代码回调指定页面.
// 参考代码
new Oidc.UserManager({response_mode:"query"}).signinRedirectCallback().then(function() {
window.location = "index.html";
}).catch(function(e) {
console.error(e);
});
2.5.2 在 IdentityServer 中注册 JavaScript 客户端
// JavaScript Client
new Client {
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RedirectUris = { "https://localhost:5003/callback" },
PostLogoutRedirectUris = { "https://localhost:5003/index" },
AllowedCorsOrigins = { "https://localhost:5003" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
2.5.3 在 API 中允许 CORS 跨域
services.AddCors(options => {
// this defines a CORS policy called "default"
options.AddPolicy("default", policy => {
policy.WithOrigins("https://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
app.UseCors("default");
最后运行测试即可.
2.6 使用 EFCore 来配置与操作数据
在前面的代码中, 我们客户端, scope 等数据写在代码中. 启动时, IdentityServer 将加载的配置数据存储与内存中. 如果需要对配置数据修改, 就需要重启 IdentityServer.
IdentityServer 还会生成临时数据, 如授权码, 同意选项, 刷新令牌等, 也是存储与内存中.
将这些数据存储与数据库中, 这表示可以不用重启应用, 也可以跨多个 IdentityServer 实例.
这里我们将使用 IdentityServer4 EntityFramework 库.
模板中提供了 is4ef 模板, 可以直接创建项目.
2.6.1 IdentityServer4.EntityFramework
IdentityServer4.EntityFramework
实现了必要的存储与服务, 使用下面的 DbContext
:
ConfigurationDbContext
- 用于存储配置数据, 例如 客户端, 资源, 以及作用域等.PersistedGrantDbCOntext
- 用于临时操作数据. 例如授权码, 刷新令牌.
这些上下文适用于任何 EFCore 兼容的关系型数据库.
你可以在 IdentityServer4.EntityFramework.Storage
nuget 包中找到这些上下文, 实体, 以及 IdentityServer4 存储.
你可以在 IdentityServer4.EntityFramework
nuget 包中找到需要在 IdentityServer 注册的扩展方法.
下面我们首先要:
dotnet add package IdentityServer4.EntityFramework
2.6.2 使用 SQLServer
案例中会使用到 LocalDB 作为数据库.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
2.6.3 数据库架构的修改以及数据库迁移
在 IdentityServer4.EntityFramework.Storage
中包含映射到 IdentityServer 模型的实体. 当需要修改 IdentityServer 模型时, 也随之需要修改 IdnetityServer4.EntityFramework.Storage
中的实体. 随着实体的更新, 表示你的数据库也需要伴随着实体更新. 一个更新的办法是使用 EF 的数据迁移. 这里也会使用.
如果你不喜欢使用数据迁移, 也可以用自己习惯的方式.
最终的 SQL 文件可以查看这里.
2.6.4 配置存储
首先需要将:
AddInMemoryClients()
AddInMemoryIdentityResources()
AddInMemoryApiScopes()
AddInMemoryApiResources()
AddInMemoryPersisedGrants()
替换为:
AddConfigurationStore()
AddOperationalStore()
这里每一个方法都需要使用 DbContextOptionsBuilder
. 因此修改后的代码看起来如下:
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=IdentityServer4.Quickstart.EntityFramework-4.0.0;trusted_connection=yes;";
services.AddIdentityServer()
.AddTestUsers(TestUsers.Users)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
你可能需要引入的命名空间:
using Microsoft.EntityFrameworkCore;
using System.Reflection;
由于使用 EF 迁移, 调用 MigrationsAssembly
是告知 EF 主机项目会包含迁移代码. 在主机项目与包含数据库上下文在不同程序集时, 这是很有必要的.
jk: 这个用法有点不会了. 没用过这个函数, 有待验证与测试
2.6.5 添加迁移
完成 EFCore 配置就, 首先需要做的是创建迁移. 创建迁移需要安装 EFCore CLI 以及 Design Nuget 包:
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
指向下面脚本创建数据库迁移:
dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration `
-c PersistedGrantDbContext `
-o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration `
-c ConfigurationDbContext `
-o Data/Migrations/IdentityServer/ConfigurationDb
然后即可看到 Data/Migrations/IdentityServer
目录下的数据库迁移文件了.
2.6.6 初始化数据库
现在有了数据库迁移, 下面可以编写代码来利用迁移创建数据库.
还可以基于前面写入内存中的数据来初始化数据库中的数据 (就不要内存中的数据了).
这个方法是的快速案例启动更为容易, 你应该为自己的架构设计合理的处理流程.
在 Startup.cs
文件中添加初始化数据库的代码:
private void InitializeDatabase(IApplicationBuilder app) {
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) {
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any()) {
foreach (var client in Config.Clients) {
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any()) {
foreach (var resource in Config.IdentityResources) {
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiScopes.Any()) {
foreach (var resource in Config.ApiScopes) {
context.ApiScopes.Add(resource.ToEntity());
}
context.SaveChanges();
}
}
}
jk: 这里也有需要深入的点
这里需要引入三个命名空间:
using System.Linq;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
然后在 Configure
方法中调用初始化方法:
public void Configure(IApplicationBuilder app) {
// this will do the initial DB population
InitializeDatabase(app);
// the rest of the code that was already here
// ...
}
然后运行代码, 可以用数据库管理工具查看数据库表.
注意种子数据的填入一般在第一次运行时写入. 再次运行考虑将其删除.
2.6.7 运行客户端程序 (略)
2.7 使用 ASP.NET Core Identity
Identity 为灵活而设计, 其中一点是可以使用你想要的任何数据库, 来存储你的用户数据 (包括密码).
本指南介绍 Identity 如何与 IdentityServer 结合.
首先创建一个新项 IdentityServer 项目, 将使用 Identity:
cd quickstart/src
dotnet new is4aspid -n IdentityServerAspNetIdentity
如果提示 seed
, 选择 Y
/yes
. 该模板创建的数据库中的用户名为 alice
和 bob
, 密码为 Pass123$
.
模板采用 sqlite 作为数据库, 模板中已经包含了 EF 迁移, 如果想要使用自定义的数据库, 需要修改代码, 自己创建迁移.
2.7.1 查看项目
打开项目, 然后看看代码.
考虑该代码是 netcoreapp3.1 的代码, 检查一下本地是否安装 3.1 的 SDK, 使用
dotnet --list-sdks
然后在项目所在目录下, 创建 globaljson
文件, 并使用 3.1 的版本
dotnet new globaljson --sdk-version 3.1.426
IdentityServerAspNetIdentity.csproj
注意 IdentityServer4.AspNetIdentity
的引用, 它包含了集成 ASP.NET Core Identity 的 IdentityServer.
Startup.cs
注意到 ConfigureServices
方法, 通过调用 AddDbContext<ApplicationDbContext>()
和 AddIdentity<ApplicationUser, IdentityRole>()
方法来配置 ASP.NET Core Identity.
同时注意在前面已经创建过的案例中, 与 IdentityServer 类似的配置. 当前模板, 加载 Config.cs 的数据, 在内存中存储 客户端, 资源数据.
最后, 注意几个新的函数调用: AddAspNetIdentity<ApplicationUser>()
. AddAspNetIdentity()
集成了一个访问层, 来允许 IdentityServer 访问存储与 Identity 用户数据库中的用户数据. 当 IdentityServer 需要将用户的 claim 写入 token 时, 就会使用它.
还需注意, AddIdentity<ApplicationUser, IdentityRole>()
必须在 AddIdentityServer()
之前被调用.
Config.cs
该文件包含写入内存中客户端与资源定义的硬编码. 如果要保留前面代码的客户端与 API 信息, 就需要将前面的信息拷贝过来.
拷贝之后代码结构应该如下:
using IdentityServer4;
using IdentityServer4.Models;
using System.Collections.Generic;
namespace IdentityServerAspNetIdentity {
public static class Config {
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource> {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope> {
new ApiScope("api1", "My API")
};
public static IEnumerable<Client> Clients => new List<Client> {
// machine to machine client
new Client {
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
// scopes that client has access to
AllowedScopes = { "api1" }
},
// interactive ASP.NET Core MVC client
new Client {
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string> {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
};
}
}
至此不再需要之前的 IdentityServer 项目了.
Program.cs 与 SeedData.cs
Program.cs 文件与之前有一点不同, 里面添加了 /seed
命令来初始化数据.
可以参考 SeedData.cs
文件来查看如何进行数据的初始化.
AccountController
模板中最后需要注意的代码是 AccountController
. 它与之前的登录与登出的代码略有不同. 注意 SignInManager<ApplicationUser>
和 UserManager<ApplicationUser>
的使用. 它们是 ASP.NET Core Identity 中用于校验证书和管理鉴权会话的.
其他的代码与之前的案例代码是一样的.
jk: 很有必要将 IdentityServer 的源码, 以及 Identity 的源码研究研究. 路漫漫其修远兮.
2.7.2 使用 MVC 客户端登录
至此应该运行所有的客户端, 在资源所有者客户端中会抛出异常, 需要将密码从 password
修改为 Pass123$
.
启用 MVC 客户端应用, 使用 "Secure" 链接来登录.
这部分需要修改一些内容, 与文档的案例有些不同
- 修改 scope, 通过查看控制台信息
- Mvc 中也没有提供对应的请求接口链接
按照文档的说, 这里需要自己探究. 可以参考官网的案例.
这里整理一下需要的改动:
- 提供控制器中的动作方法 CallApi
- 提供 json.cshtml 视图
- 在 index.cshtml 页面中使用链接