【One by One系列】IdentityServer4(二)使用Client Credentials保护API资源

2022-06-23 16:55:42 浏览数 (1)

书接上回,我们将会正式开始介绍IdentityServer4。

IdentityServer4是实现了OAuth2.0 OpenId Connect两种协议的优秀第三方库,属于.net生态中的优秀成员。可以很容易集成至ASP.NET Core,颁发token。

使用Id4基本步骤如下:

  • **1.**在Startup.Configure方法中调用
代码语言:javascript复制
app.UseIdentityServer();

添加中间件,把Id4添加至http请求处理管道,这使得Id4可以为OpenID Connect和OAuth2协议描述的端点(如/connect/token)请求提供服务。

  • **2.**在Startup.ConfigureServices中注册IdentityServer4
代码语言:javascript复制
 services.AddIdentityServer(options=>
                            {
                                ...
                            });
  • **3.**配置Identity Server
    • Identity资源表示提供给客户端进行用户识别的信息(声明)。声明可能包括用户名称、电子邮件地址等。
    • API资源表示用户可通过访问令牌访问的受保护数据或功能。API 资源的一个示例是要求授权的 Web API(或 API集合)。
    • 用于签名的凭据(credentials)
    • 用户可能会请求访问的Identity资源和API资源
    • 会请求获取token的客户端
    • 用户信息的存储机制,如ASP.NET Core Identity或者其他机制

当你指明Id4使用的客户端和资源,可以将IEnumerable<T>传递给接受内存中的客户端或资源存储的方法,如果在更复杂的场景,可以通过依赖注入的方式提供客户端和资源提供程序类型。

IdentityServer4 使用自定义 IClientStore 类型提供的内存中资源和客户端的示例配置:

代码语言:javascript复制
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模板

代码语言:javascript复制
dotnet new -i IdentityServer4.Templates
  • AdminUI:测试,生产环境需要交钱,商业软件
  • ASP.NET Core Identity:结合ASP.NET Core Indentity
  • Empty:空模板
  • Entity Frame Store:使用ef数据持久化身份认证信息
  • In-Memory Stores and Test Users:添加内存中的用户认证信息,和测试用户
  • Quickstart UI (UI assets only):UI

2.创建ASP.NET Core应用,搭载Id4

2.1 创建项目

  • 使用IdentityServer4的空模板创建应用
代码语言:javascript复制
md quickstart
cd quickstart

md src
cd src

#空模板 项目
dotnet new is4empty -n IdentityServer
The template "IdentityServer4 Empty" was created successfully.
  • 项目添加解决方案
代码语言:javascript复制
cd ..
dotnet new sln -n QuickStart

dotnet sln add .IdentityServerIdentityServer.csproj

2.2 修改launchSettings.json

  • 测试环境,使用http,删掉IIS相关的配置
代码语言:javascript复制
{
  "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增加
代码语言:javascript复制
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
    //新增
    new ApiScope("api1", "My API")
};
  • 第二个参数,是displayname

2.4 定义一个客户端

要让我们的IdentityServer给客户端颁发token,就要让客户端在IdentityServer注册。

客户端,模板中的客户端与scope一样为空,在Config.cs增加客户端,代码如下:

代码语言:javascript复制
 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()所有代码,增加代码:加载定义的资源和客户端,代码如下:

代码语言:javascript复制
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 新建项目

代码语言:javascript复制
dotnet new webapi -n webapi
cd ..
dotnet sln add  .webapiwebapi.csproj

3.2 修改launchSettings.json

代码语言:javascript复制
{
  "profiles": {
    "Api": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:6001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

3.3 新增api

IdentityController.cs

代码语言:javascript复制
    [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的具体身份认证

代码语言:javascript复制
dotnet add .webapiwebapi.csproj package Microsoft.AspNetCore.Authentication.JwtBearer

3.5 注册服务和添加中间件

最后一步是将身份认证服务添加到依赖注入中,并将身份认证中间件添加到管道中。以便:

  • 验证传入的token,确保token来自可信的颁布者(服务器)
  • 验证这个token在这个api中使用是有效的(也就是受众)

看代码:

代码语言:javascript复制
{
    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 新建项目

代码语言:javascript复制
dotnet new console -n Client
dotnet sln add .ClientClient.csproj

4.2 引入nuget包

需要引入IdentityModel包,一个客户端类,以请求disconvery endpoint

代码语言:javascript复制
cd .Client
dotnet add package IdentityModel

4.3 编码-请求Idisconvery endpoint

只需要知道IdentityServer的基础地址,实际的各类端点地址就可以从元数据中读取:

代码语言:javascript复制
    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:

代码语言:javascript复制
// 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资源:

代码语言:javascript复制
// 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
代码语言:javascript复制
cd .IdentityServer
dotnet run
  • 启动webapi
代码语言:javascript复制
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

0 人点赞