Client Credentials
在了解了Identity Server
的基础知识后,本节我们简单演示如何使用最简单的Client Credentials
方式来保护API资源。相关案例代码已分享到Github。
1. Identity Server
首先我们建立Identity Server
,官方提供了IdentityServer templates
项目模板。
# 安装 IdentityServer 官方项目模板
dotnet new -i IdentityServer4.Templates
我们使用IdentityServer4 Empty
模板建立一个空的Identity Server4项目。
接下来我们配置被保护的资源和客户端。项目模板中默认会创建一个Config.cs
文件用于演示在内存中配置被保护资源和客户端,生产项目中可以根据实际情况从配置文件或DB中加载配置。
public static class Config
{
//Identity data
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[] {new IdentityResources.OpenId(),};
//APIs
public static IEnumerable<ApiScope> ApiScopes =>
new[]
{
new ApiScope("WeatherApi", "天气预报")
};
//Clients
public static IEnumerable<Client> Clients =>
new[]
{
new Client
{
ClientId = "ClientCredentialConsoleClient",
ClientSecrets = {new Secret("ClientCredentialConsoleClient".Sha256())},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = {"WeatherApi"}
}
};
}
接下来我们注册Identity Server
服务和中间件。
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer(options => options.EmitStaticAudienceClaim = true)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
if (environment.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseIdentityServer();
}
至此IdentityServer
已配置完成,运行后访问https://localhost:5001/.well-known/openid-configuration
可以看到discovery document
配置,注册到IdentityServer
的客户端和API都通过此discovery document
获取必要的配置数据。
2. API
我们创建一个标准的Asp.Net Web API程序,在Startup
中注册认证授权服务和中间件。这里我们使用JWT
认证方案,并通过其Authority
属性将认证服务指向IdentityServer
即可。
IdentityServer
认证客户端访问被保护的API资源时会携带名为scope
的Claim
对象。在API中可以以此进行鉴权,本案例中我们使用scope
建立对应授权策略进行鉴权。
{
"IdentityServerOptions": {
"Address": "https://localhost:5000",
"Scopes": [
"WeatherApi"
]
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var identityServerOptions =
Configuration.GetSection(nameof(IdentityServerOptions)).Get<IdentityServerOptions>();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = identityServerOptions.Address;
options.TokenValidationParameters.ValidateAudience = false;
});
services.AddAuthorization(options =>
{
foreach (var scope in identityServerOptions.Scopes)
options.AddPolicy(scope, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", scope);
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
public class IdentityServerOptions
{
public string Address { get; set; }
public IEnumerable<string> Scopes { get; set; }
}
API我们使用默认的WeatherForecastController
即可,在控制器中使用AuthorizationAttribute
鉴权。
[ApiController]
[Route("[controller]")]
[Authorize("WeatherApi")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
至此API项目配置完成,启动应用并访问https://localhost:10000/WeatherForecast
得到401
响应码,说明API需要授权且已被IdentityServer
保护。
3. Client
本节我们创建一个控制台程序作为客户端。IdentityModel
Nuget包对HttpClient
进行了扩展用于与IdentityServer
交互。
IdentityServer
和API相关配置如下。
"IdentityServerOptions": {
"Address": "https://localhost:5000",
"ClientId": "ClientCredentialConsoleClient",
"ClientSecret": "ClientCredentialConsoleClient",
"Scopes": [
{
"Name": "WeatherApi",
"Url": "https://localhost:10000/WeatherForecast"
}
]
}
使用HttpClient
的GetDiscoveryDocumentAsync
方法连接IdentitySever
的discovery endpoint
可以获得相关认证服务器的相关配置。IdentityServer
默认仅支持Https协议,本地开发环境第一次可以运行dotnet dev-certs https --trust
信任开发证书。或者通过如下16
行代码关闭Https验证。
static async Task Main(string[] args)
{
var options = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build()
.GetSection(nameof(IdentityServerOptions))
.Get<IdentityServerOptions>();
using var client = new HttpClient();
//发现IdentityServer配置
var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = options.Address,
// Policy = new DiscoveryPolicy
// {
// RequireHttps = false,
// ValidateEndpoints = false,
// ValidateIssuerName = false
// }
});
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
//获取Token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = options.ClientId,
ClientSecret = options.ClientSecret,
Scope = options.Scope
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
//API调用
using var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync(options["WeatherApi"]);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
return;
}
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
通过HttpClient
的RequestClientCredentialsTokenAsync
方法,使用ClientId/ClientSecret/Scope
等认证数据在IdentityServer
获取Access Token
。获取AccessToken
后就可以使用令牌调用被保护的API了。
4. Discovery Endpoint
IdentityServer4
提供了一个Discovery Endpoint
用于服务发现,它定义了一个API(/.well-known/openid-configuration
),这个API返回一个json
数据结构,其中包含了一些OIDC中提供的服务以及其支持情况的描述信息,这样只需要知道IdentityServer
基地址就可以通过 Discovery
方便的使用各种端点和配置,而不再需要硬编码服务接口信息。
{
"issuer": "https://localhost:5000",
"jwks_uri": "https://localhost:5000/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://localhost:5000/connect/authorize",
"token_endpoint": "https://localhost:5000/connect/token",
"userinfo_endpoint": "https://localhost:5000/connect/userinfo",
"end_session_endpoint": "https://localhost:5000/connect/endsession",
"check_session_iframe": "https://localhost:5000/connect/checksession",
"revocation_endpoint": "https://localhost:5000/connect/revocation",
"introspection_endpoint": "https://localhost:5000/connect/introspect",
"device_authorization_endpoint": "https://localhost:5000/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"openid",
"profile",
"WeatherApi",
"offline_access"
],
"claims_supported": [
"sub",
"name",
"family_name",
"given_name",
"middle_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"gender",
"birthdate",
"zoneinfo",
"locale",
"updated_at"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"implicit",
"password",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code",
"token",
"id_token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"response_modes_supported": [
"form_post",
"query",
"fragment"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"subject_types_supported": [
"public"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"request_parameter_supported": true
}
以上访问Discovery Endpoint
得到的示例数据。可以看到其包含了IdentitySever中各服务端点和支持的内容等配置信息。
使用HttpClient.GetDiscoveryDocumentAsync()
方法会先请求/.well-known/openid-configuration
发现服务,然后请求/.well-known/openid-configuration/jwks
(公钥开放端点)获取解析JWT
令牌的公钥。
发现IdentityServer
配置并非必需,也可以直接请求具体服务端点,如请求https://localhost:5000/connect/token
(token_endpoint)获取AccessToken
。
5. 证书管理
IdentitySever4
中Token
一般采用JWT
方案,它使用私钥来签名JWT token
,公钥验证签名,一般情况下我们通过一个证书提供私钥和公钥。在开发环境中一般通过IIdentityServerBuilder.AddDeveloperSigningCredential()
注册开发密钥签名。在程序第一次启动时IdentityServer
会自动创建一个tempkey.jwk
文件保存密钥,此文件不存在则会在程序启动时自动重建。打开tempkey.jwk
文件即可得到密钥内容,此方式安全性较低,攻击者获得文件会导致密钥直接泄露。在生产环境中我们一般会生成一个加密的安全证书来提供私钥和公钥。
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer(options => options.EmitStaticAudienceClaim = true)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
if (_env.IsDevelopment() || _env.IsStaging())
builder.AddDeveloperSigningCredential();
else
builder.AddSigningCredential(
new X509Certificate2(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "is4.pfx"),
"5C6CE27CBA3DD15B4EFBE5A7EC679CBBE79D14F5"));
}
在上面代码中我们使用了Pfx
证书,Pfx
证书除了包含cer/crt
证书的公钥,还可以选择性的包含key
密钥文件,同时还可以设置证书密码保护。简而言之通过Pfx
证书可以安全的提供一对公钥私钥。
生成Pfx
证书的方式有很多,这里我们推荐一款免费跨平台的证书管理工具——KeyManager。
生成Pfx
证书流程如上图所示,导出证书时的加密私钥从思源管理中获取即可。如果不确定哪个私钥可以通过相关证书确认。
参考文献