【One by One系列】IdentityServer4(二)使用客户端凭证(Client Credentials)保护API资源
书接上回,我们将会正式开始介绍IdentityServer4。
IdentityServer4是实现了OAuth2.0+OpenId Connect两种协议的优秀第三方库,属于.net生态中的优秀成员。可以很容易集成至ASP.NET Core,颁发token。
使用Id4
基本步骤如下:
- **1.**在
Startup.Configure
方法中调用
app.UseIdentityServer();
添加中间件,把Id4添加至http请求处理管道,这使得Id4可以为OpenID Connect和OAuth2协议描述的端点(如/connect/token
)请求提供服务。
- **2.**在
Startup.ConfigureServices
中注册IdentityServer4
services.AddIdentityServer(options=>
{
...
});
- **3.**配置Identity Server
- 用于签名的凭据(credentials)
- 用户可能会请求访问的Identity资源和API资源
- Identity资源表示提供给客户端进行用户识别的信息(声明)。 声明可能包括用户名称、电子邮件地址等。
- API资源表示用户可通过访问令牌访问的受保护数据或功能。 API 资源的一个示例是要求授权的 Web API(或 API集合)。
- 会请求获取token的客户端
- 用户信息的存储机制,如ASP.NET Core Identity或者其他机制
当你指明Id4使用的客户端和资源,可以将IEnumerable<T>
传递给接受内存中的客户端或资源存储的方法,如果在更复杂的场景,可以通过依赖注入的方式提供客户端和资源提供程序类型。
IdentityServer4 使用自定义 IClientStore 类型提供的内存中资源和客户端的示例配置:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.AddSingleton<IClientStore, CustomClientStore>();
services.AddIdentityServer()
.AddSigningCredential("CN=sts")
.AddInMemoryApiResources(MyApiResourceProvider.GetAllResources())
.AddAspNetIdentity<ApplicationUser>();
//...
}
经过上面的概述,我们先易后难,将从最简单的客户端凭证开始实战搭建IdentityServer4项目并以此保护api资源,首先客户端凭证属于OAuth2.0的一种授权方式。
- 主要是向IdentityServer发送post请求
token?grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
,获取access-token,以此来访问api资源。 - 在IdentityServer4中,增加了
Scope
参数,表明了客户端的访问权限
1.安装Id4模板
dotnet new -i IdentityServer4.Templates
AdminUI
:测试,生产环境需要交钱,商业软件ASP.NET Core Identity
:结合ASP.NET Core IndentityEmpty
:空模板Entity Frame Store
:使用ef数据持久化身份认证信息In-Memory Stores and Test Users
:添加内存中的用户认证信息,和测试用户Quickstart UI (UI assets only)
:UI
2.创建ASP.NET Core应用,搭载Id4
2.1 创建项目
- 使用IdentityServer4的空模板创建应用
md quickstart
cd quickstart
md src
cd src
#空模板 项目
dotnet new is4empty -n IdentityServer
The template "IdentityServer4 Empty" was created successfully.
- 项目添加解决方案
cd ..
dotnet new sln -n QuickStart
dotnet sln add .\IdentityServer\IdentityServer.csproj
2.2 修改launchSettings.json
- 测试环境,使用http,删掉IIS相关的配置
{
"profiles": {
"SelfHost": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": ".well-known/openid-configuration",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5001/"
}
}
}
2.3 定义一个api scope
上篇与前文都介绍过,scope
代表资源所有者在被保护资源那里的一些权限,可以把被保护资源分为不同的scope
,具体的粒度由开发自定义。
- 模板中ApiScope为空,在Config.cs增加
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
//新增
new ApiScope("api1", "My API")
};
- 第二个参数,是
displayname
2.4 定义一个客户端
要让我们的IdentityServer给客户端颁发token,就要让客户端在IdentityServer注册。
客户端,模板中的客户端与scope一样为空,在Config.cs增加客户端,代码如下:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "client app",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret-123456".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
我们之后会增加这个定义的客户端,这个客户端将会访问
AllowedScopes
指定的api scope。
注意:在此场景下,客户端跟用户是没有交互的,身份认证是通过IdentityServer的客户密钥。
官方描述:你可以把ClientId和ClientSecret看作应用程序本身的登录名和密码。它向身份服务器表明您的应用程序的身份(我是xx应用程序,想访问服务器)。
2.5 注册IdentityServer
注释模板代码Startup.ConfigureServices()
所有代码,增加代码:加载定义的资源和客户端,代码如下:
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer()
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
// omitted for brevity
}
配置完成。运行并浏览器访问http://localhost:5001/.well-known/openid-configuration
,就能看到discovery document
.
- 它是IdentityServer中的标准端点
- 客户端和APIs会使用它下载必要的配置数据,容后再表
在第一次启动时,IdentityServer将创建一个开发者签名密钥,它是一个名为tempkey.rsa的文件。您不必将该文件签入源代码版本控制,如果不存在该文件,它将被重新创建。
3.创建webapi
限制开始创建我们需要保护的api资源
3.1 新建项目
dotnet new webapi -n webapi
cd ..
dotnet sln add .\webapi\webapi.csproj
3.2 修改launchSettings.json
{
"profiles": {
"Api": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:6001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
3.3 新增api
IdentityController.cs
[Route("api/[controller]")]
[ApiController]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
3.4 引入nuget包
Microsoft.AspNetCore.Authentication.JwtBearer
这个包是当收到请求时,对授权头中JWT的具体身份认证
dotnet add .\webapi\webapi.csproj package Microsoft.AspNetCore.Authentication.JwtBearer
3.5 注册服务和添加中间件
最后一步是将身份认证服务添加到依赖注入中,并将身份认证中间件添加到管道中。以便:
- 验证传入的token,确保token来自可信的颁布者(服务器)
- 验证这个token在这个api中使用是有效的(也就是受众)
看代码:
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
//token颁发者
options.Authority = "http://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
options.RequireHttpsMetadata = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
//scope
policy.RequireClaim("scope", "api1");
});
});
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireAuthorization("ApiScope");
});
}
}
-
AddAuthentication
:增加认证服务到依赖注入,注册Bearer
作为默认scheme
-
AddAuthorization
:增加授权服务到依赖注入,验证token
中是否存在scope
,这里使用的是ASP.NET Core授权策略系统这里实质是验证jwt中的payload的scope
-
RequireHttpsMetadata
用于测试目的;将此参数设置为 false,可在你没有证书的环境中进行测试。 在实际部署中,JWT 持有者令牌应始终只能通过 HTTPS 传递。 -
UseAuthentication
:添加认证中间件,以便对host的每次调用自动执行身份认证,此中间件准备就绪后,会自动从授权标头中提取 JWT 令牌。 然后对其进行反序列化、验证,,并将其存储为用户信息,稍后供 MVC 操作或授权筛选器引用。JWT 持有者身份验证中间件还可以支持更高级的方案,例如颁发机构authority 不可用时使用本地证书验证令牌。 对于此情景,可以在
JwtBearerOptions
对象中指定TokenValidationParameters
对象。 -
UseAuthorization
:添加授权中间件,以确保我们的api不会被匿名客户端访问 -
RequireAuthorization("ApiScope")
:全局执行授权策略除了全局以外,还可以针对多有的api端点,或者特定的controller,action,根据实际的业务场景灵活变化吧
访问:http://localhost:6001/identity
,返回状态码401,这是api要求凭证,所以现在api是被IdentityServer保护着
4.创建客户端
最后一步,创建一个由IdentityServer管理的客户端,并通过客户端请求access-token,然后访问api
4.1 新建项目
dotnet new console -n Client
dotnet sln add .\Client\Client.csproj
4.2 引入nuget包
需要引入IdentityModel
包,一个客户端类,以请求disconvery endpoint
cd .\Client\
dotnet add package IdentityModel
4.3 编码-请求Idisconvery endpoint
只需要知道IdentityServer的基础地址,实际的各类端点地址就可以从元数据中读取:
class Program
{
static async Task Main(string[] args)
{
// discover endpoints from metadata
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
}
}
4.4 编码-请求access token
这一步,使用从discovery document
中获取的信息,向IdentityServer
请求一个访问api1的token:
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
//在IdentityServer注册的id与secret
ClientId = "client app",
ClientSecret = "secret-123456",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
注意看,这里的ClientId,ClientSecret
必须与2.4中定义客户端保持一致,Scope
也要与AllowedScopes
保持一致。
4.5 编码-调用api
在这一步,使用扩展方法SetBearerToken
,这个方法主要组装http请求:授权头+access token,并以此请求访问api资源:
// call api
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:6001/api/identity");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
5.测试
- 启动IdentityServer
cd .\IdentityServer\
dotnet run
- 启动webapi
cd .\webapi\
dotnet run
- 用vs启动client
获取access-token,我们通过http://jwt.calebb.net/解析
这也是api返回的Claims
身份认证的中间对JWT进行了身份认证后,会把解析到的Claims组装进HttpContext,以供下一个中间件(如授权中间件)调用
接下来我们就去触发不同的错误去了解IdentityServer是如何工作的,我选择其中几个比较有意义的测试:
5.1 使用一个无效客户端id或者密钥请求token
没被注册的客户端,访问时,所以是invalid_client
类比场景:去办理门禁卡,物业没找到你这个业主信息,办个鬼呀
5.2 在请求token时指定无效的scope
请求token,指定的scope,在indentityserver中并不存在,所以是invalid_scope
类比场景:去办理门禁卡,小区一共10栋,你去办11栋,办个鬼呀
5.3 请求api时,不传入toekn
不传入token,那么webapi就没收到token,所以返回Unauthorized
未授权
类比场景:进入小区,没有门禁,肯定不让你进
5.4 修改API对scope的验证要求
被保护的资源webapi中配置plicy.RequireClaim("scope","api2");
而客户端指定的scope是api1
客户端是有access-token,具有进入系统凭证,但是,只允许scope为api2的访问,传入的时api1,当然就返回Forbidden
类比场景:小区进入后,进入单元楼,明明是3栋2单元的楼宇,但是你的门禁只能针对3栋1单元,当然也不会刷开2单元的大门
参考链接
https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html#source-code
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2020-07-09-id4-client-credentials/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。