快速理解 IdentityServer4 中的认证 & 授权

前言

在实际的生产环境中,存在各种各样的应用程序相互访问,当用户访问 app 应用的时候,为了安全性考虑,通常都会要求搭配授权码或者安全令牌服务一并访问,这样可有效地对 Server 端的 API 资源起到一定程度的有效保护,大概流程如下:
快速理解 IdentityServer4 中的认证 & 授权_第1张图片

接下来我们就针对这个 API 请求访问的过程进行详细的了解,那么通常会存在哪些交互模式呢?

常见的交互模式

在应用请求访问的过程中,最常见的交互模式有以下几种:

  • browser(浏览器)与 web app (web 应用程序)通信;
  • web appweb APIs 进行通信;
  • 基于 browser(浏览器)的 app 应用程序与 web APIs 通信;
  • native app (原生应用)与 web APIs 通信;
  • 基于 service 服务的 app 应用程序与 web APIs 通信;
  • web APIsweb APIs 进行通信;

通常情况,每个层(前端、中间层和后端)都必须保护资源,并实现身份 认证Authentication)和 授权Authorization),所以它们通常是针对同一个 用户User)进行存储。将这些基本安全功能外包给 STSSecurity Token Service,安全令牌服务),可以防止在这些 App 应用程序和 端点Endpoint)之间复制该功能。

依据上面罗列的几种常见的交互模式,接下来我们对 app 应用程序进行重构以支持 STS,这将形成以下体系结构和协议:

快速理解 IdentityServer4 中的认证 & 授权_第2张图片

认证 & 授权

在应用程序中,通常的 认证(Authentication)& 授权(Authorization) 服务流程如下:

快速理解 IdentityServer4 中的认证 & 授权_第3张图片

认证与授权是两个概念,不能混淆为一,接下来我们对这两个概念分别逐个了解。

什么是 Authentication(认证)?

对请求方或访问者的身份鉴别,意思就是确认你的身份是你。举个例子,比如早晨你去公司上班,到公司门口需要刷厂牌或工牌,然后公司的门禁卡会识别你个人的身份信息,接着鉴别或确认你是否所属公司的成员,如果是那么就可以进入公司范围,反之就不能进入。这个过程就称为认证。

常见的身份验证协议

  • SAML2p,安全断言标记语言(英语:Security Assertion Markup Language,简称:SAML);
  • WS-Federation,联合身份验证是 (安全域) 领域集合,这些领域已建立安全共享资源的关系。
  • OpenID Connect,通常也叫 OIDC,是一套基于 OAuth 2.0 协议的轻量级规范,提供通过 API 进行身份交互的框架。

其中 SAML2p 是最流行和最广泛部署的,而 OIDC 是三款中最新的,但被认为是未来的趋势,因为它具有现代应用的最大潜力。 它是从一开始就构建用于移动应用场景的,并且被设计为 API 友好。

相关文章:

  • 干货|理解 SAML2 协议,https://baijiahao.baidu.com/s?id=1742115008490452461&wfr=spider&for=pc
  • API 网关 OpenID Connect 使用指南,https://help.aliyun.com/document_detail/48019.html
  • OpenID Connect 是什么?http://t.zoukankan.com/lexiaofei-p-7233230.html
  • SAML2.0对接,https://help.aliyun.com/document_detail/114853.html?scm=20140722.184.2.173
  • OAuth2.0对接,https://help.aliyun.com/document_detail/114852.html

什么是 Authorization(授权)?

在确认你的身份信息之后对你进行相应的授权。接着上面的例子,当公司的门禁卡鉴别了你的个人身份信息后,确定你是公司的所属成员,就可以顺利的进入公司,而公司里面又有很多的部门,每个部门有不同的职责范围,当然你对应的也所属其中某一个(或多个)部门,其中每个部门具有一定的权限范围,那么这个部门范围的划分过程就类似授权。

常见的授权协议

  • OAuthOpen Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。OAuth 在全世界得到广泛应用,目前的版本是 2.1 版(https://oauth.net/2.1/)。
  • OAuth 2.0 协议官方地址,https://www.rfc-editor.org/rfc/rfc6749

快速理解 IdentityServer4 中的认证 & 授权_第4张图片

OAuth 2.0 协议特点

  • 简单:不管是 OAuth 服务提供者还是应用开发者,都很易于理解与使用;
  • 安全:没有涉及到用户密钥等信息,更安全更灵活;
  • 开放:任何服务提供商都可以实现 OAuth,任何软件开发商都可以使用 OAuth

OIDCOAuth 2.0 非常相似,实际上 OIDC 是在 OAuth 2.0 之上的扩展。两个基本的安全考虑,身份验证和API 访问,被组合成了一个单一的协议 IdentityServer4( 简称 IDS4,文章下面会讲述),通常与 STS(安全令牌服务)一起单一往返。

相关文章:

  • OAuth 2.0,https://oauth.net/2/
  • OAuth2.0 详解,https://zhuanlan.zhihu.com/p/509212673

asp.net core 项目中的认证 & 授权

通过对前面的 认证(Authentication)& 授权(Authorization) 的了解,我们来回顾下 asp.net core mvc/webapi 项目中 Filter认证 & 授权 流程(你是否有似曾相识的感觉呢?),如下所示:

快速理解 IdentityServer4 中的认证 & 授权_第5张图片

IdentityServer4 框架

什么是 IdentityServer4 ?

通常情况下,我们会把 OIDCOAuth 2.0 搭配使用,认为该组合是在可预见的未来保护现代应用程序的最佳方法。而 IdentityServer4 是这两个协议的实现,并且经过高度优化,可以解决当今 移动(mobile)、原生(native)和 Web 应用程序 的典型安全问题。

快速理解 IdentityServer4 中的认证 & 授权_第6张图片

  • 官方解释IdentityServer4 是基于 ASP.NET Core 实现的认证和授权框架,是对 OpenID ConnectOAuth 2.0 协议的实现。
  • 通俗理解:服务端 Server 对需要认证授权的资源(Resource,客户端请求资源)在外层使用 IdentityServer4 框架进行封装加壳,用户只能通过获取 IdentityServer4 颁发的令牌(Token)后,才能有效地进行后续的资源访问。

总之 IdentityServer 是一个身份 认证(Authentication)& 授权(Authorization) 程序(或框架),该程序实现了 OIDC(OpenID Connect)OAuth 2.0 协议。

说明:同一种概念,不同的文献使用不同的术语,比如有些文献把他叫做 安全令牌服务(STS,Security Token Service)、身份提供(IP,Identity Provider)、授权服务器(Authorization Server)、IP-STS 等等。其实他们都是一个意思,目的 都是 在软件应用中为客户端颁发 Token 令牌并用于安全访问的

IdentityServer4 有哪些功能?

IdentityServer4 提供如下功能:

  • Resource 资源保护;
  • 使用本地帐户或通过外部身份提供程序对用户(User)进行身份验证;
  • 提供会话管理和单点登录(SSO) & 注销;
  • 管理和验证客户机(Clients);
  • 向客户颁发标识(Identity)和访问令牌(Access token);
  • 验证 Token 令牌;

IdentityServer4 工作原理

快速理解 IdentityServer4 中的认证 & 授权_第7张图片

关于 IDS4 的术语解释:

  • User(用户):用户是使用注册的客户端访问资源的人。

  • Clients(客户端):客户端是从 IdentityServer 请求令牌的软件,用于验证用户(请求身份令牌)或访问资源(请求访问令牌)。 必须首先向 IdentityServer 注册客户端才能请求令牌。
    客户端的示例可以是:Web 应用程序、移动或桌面应用程序、SPA、服务器进程等。

  • Resources(资源):资源是要用 IdentityServer 保护的资源,包括用户的身份信息或API。
    每个资源都有一个唯一的名称,客户端使用这个名称来指定他们想要访问的资源。
    用户的身份信息,包括名称或电子邮件等。
    API 资源则是客户端想要调用的功能,它们通常是 Web API,但不一定。

  • Identity Token(身份令牌):身份令牌表示身份验证过程的结果。
    它至少包含:1、用户的标识;2、用户如何以及何时进行身份验证的信息。它也可以包含其他身份信息。

  • Access Token(访问令牌):访问令牌允许用户访问 API 资源,客户端请求访问令牌并将其转发到 API。
    访问令牌包含有关客户端和用户的信息,API 使用该信息来授权用户访问它的数据。

关于 IdentityServer 更多信息请参考相关文档:

  • http://www.identityserver.com.cn/Home/Detail/shuyu
  • https://identityserver4docs.readthedocs.io/zh_CN/latest/index.html
  • https://identityserver4.readthedocs.io/en/latest/

IdentityServer4 应用示例

为了演示下面模式,创建项目结构如下:

快速理解 IdentityServer4 中的认证 & 授权_第8张图片

1、基于内存模式(Sample.WebIdentityServer

  • 1.1 Client 模式Sample.ConsoleApp 访问 Sample.WebIdentityServer
  • 1.2 Server 模式Sample.WebApi 访问 Sample.WebIdentityServer

2、基于 db 模式(Sample.WebIdentityServer

  • 2.1 数据持久化 db

1、安装 IdentityServer4 模板(可选)

说明:此处使用 dotnet cli 安装 IdentityServer4 的前提是务必确保宿主机已经安装 dotnet sdk。

此处为了更快的入门 IdentityServer4 框架,我们参照官方文档的快速入门部分,首先通过 dotnet cli 安装模板,执行如下命令:

dotnet new -i IdentityServer4.Templates

安装成功后,输出如下信息:

将安装以下模板包:
   IdentityServer4.Templates

成功: IdentityServer4.Templates::4.0.1 已安装以下模板:
模板名                                                短名称    语言  标记
----------------------------------------------------  --------  ----  -------------------
IdentityServer4 Empty                                 is4empty  [C#]  Web/IdentityServer4
IdentityServer4 Quickstart UI (UI assets only)        is4ui     [C#]  Web/IdentityServer4
IdentityServer4 with AdminUI                          is4admin  [C#]  Web/IdentityServer4
IdentityServer4 with ASP.NET Core Identity            is4aspid  [C#]  Web/IdentityServer4
IdentityServer4 with Entity Framework Stores          is4ef     [C#]  Web/IdentityServer4
IdentityServer4 with In-Memory Stores and Test Users  is4inmem  [C#]  Web/IdentityServer4

2、创建 Sample.WebIdentityServer 项目

首先为应用程序创建一个目录,然后使用 is4empty 模板创建一个包含基本 IdentityServer 设置的 ASP.NET Core 应用程序。cli 执行如下命令:

cd E:\CodeStuday # 进入目标盘符(此处是E盘 CodeStuday 文件夹)
md quickstart # mkdir 简写 md,创建 quickstart 文件夹
cd quickstart # 进入 quickstart 文件夹

md src # 创建 src 文件夹
cd src # 进入 src 文件夹

# cli 查看模板创建列表
dotnet new -l
# 使用 is4empty 模板创建一个包含基本 IdentityServer 设置的 ASP.NET Core 应用程序
dotnet new is4empty -n Sample.WebIdentityServer

或者通过 vs2022 手动创建一个 asp.net core empty 项目(等效同上),这里先给出改造好的项目结构如下:

快速理解 IdentityServer4 中的认证 & 授权_第9张图片

新建的 Sample.WebIdentityServer 项目改造如下:

2.1、添加 Nuget 包 IdentityServer4

快速理解 IdentityServer4 中的认证 & 授权_第10张图片
添加完成后,项目 Sample.WebIdentityServer.csproj 工程文件如下:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IdentityServer4" Version="4.1.2" />
  </ItemGroup>

</Project>
2.2、新增 IdentityServerConfig.cs 类,添加如下代码:
using IdentityServer4.Models;
using System.Collections.Generic;

namespace Sample.WebIdentityServer;

public static class IdentityServerConfig
{
    public static IEnumerable<ApiScope> ApiScopes => new[]
    {
       new ApiScope
       {
          Name = "sample_api",
          DisplayName = "Sample API"
       }
    };

    public static IEnumerable<Client> Clients => new[]
    {
       new Client
       {
          ClientId = "sample_client",
          ClientSecrets =
          {
              new Secret("sample_client_secret".Sha256())
          },
          AllowedGrantTypes = GrantTypes.ClientCredentials,
          AllowedScopes = { "sample_api" }
       }
    };
}
2.3、改造为 Program.cs & Startup.cs 模式

由于 .net6 中的 asp.net core 默认只有 Program.cs,这里我们改造为 Program.cs & Startup.cs 模式。

3.1 先添加 Startup.cs 文件,代码如下:

namespace Sample.WebIdentityServer;

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // Add services to the container. 注册服务到 Ioc 容器
    public void RegisterServices(IServiceCollection services, IHostBuilder host)
    {
        # 添加 IdentityServer 
        var builder = services.AddIdentityServer();
        # 添加开发人员签名凭据
        builder.AddDeveloperSigningCredential(); 
        # 使用内存模式,注册 ApiScopes 和 Clients 
        builder.AddInMemoryApiScopes(IdentityServerConfig.ApiScopes);
        builder.AddInMemoryClients(IdentityServerConfig.Clients);
    }

    // Configure the HTTP request pipeline. 配置 HTTP 请求管道(中间件管道即中间件委托链)
    public void SetupMiddlewares(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        # 使用 IdentityServer 
        app.UseIdentityServer();
    }
}

3.2 接下来改造 Program.cs 文件中代码,修改如下:

namespace Sample.WebIdentityServer;

public class Program
{
    /*
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        var app = builder.Build();

        app.MapGet("/", () => "Hello World!");

        app.Run();
    }*/

    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        var startup = new Startup(builder.Configuration);
        startup.RegisterServices(builder.Services, builder.Host);

        var app = builder.Build();
        startup.SetupMiddlewares(app, builder.Environment);

        app.Run();
    }
}

到此处 Sample.WebIdentityServer 项目就改造完毕,可以尝试启动运行测试,成功运行后,会在该项目的根目录生成一个 tempkey.jwk 文件,文件内容如下:

{
	"alg": "RS256",
	"d": "gc7bg0NCxsOf8AMxX76ZubsTHblmN5WLSkbbk9miPBYIVhO4TdYAZX7rU65rU5v9Z6Kn9Tm-4gslOEfnivdTQAlq5SWkDe6S152so32z0xlH5gUL02686_IY6dHi89gwMNJziEU39PqrtQXhLpoVz2H1JXDzrqt5_QacueuZUG_tS1xEps1xelrjCk1isIRb3y05xLqWH4c4zNDXLqRaF9PR43FZ7Ea6mxAa3meZ0HZ23QN0075BByI0jkhV3TpQjYZ7oWjsXCeFrLGJILo6bWoP7Y-sQRgXmZChRvHXuYmEPeQlFSA1xwEJL8eadd8YL_T5o_wFWEg1rNwncoFYZQ",
	"dp": "I6s-o6dW1CjvO_SGLunlmwAJeHFT3sFjC9MXAIzjW2AhXWbocGEP5oS6jnHOJ-x3F5_FhRzabTOj-drZ9eAVD9HPDbhX0a6GvOX5l9orFC6Bd5EogZ2PfkaaDWRa-AjJULiLkEvS3EgSa0T223V1gKMQyj3bEjU0o2ZMoLnAiIk",
	"dq": "cmT7rXskO9aBk980wR32x3emigZK9GJCHg_U308zeGm4_WfPAbQrvd8zSQ8sdLMGi1bloiBeaGqHtLrQBQYpRw1W8IP6qvTwGHwqc5X9WQvuEtqBAmp0Dsc0eCHwrBDVtgzIxQGC63BAPIzMErnr2-pPGG1-yTmNuQkB7zf1fZ8",
	"e": "AQAB",
	"kid": "7D56F0331FF5B9BFBCFCB25DF9D81E44",
	"kty": "RSA",
	"n": "183z7AQ4qrQzOOoNg3ven3wgFeavnf2xncJ85bAL4df18rAKtlPaH6xc1ELhu01RUmpvHJCv4dQ9F4JTs0kXZMw3ZcIKw6fDuVKZThEMfkAkJ3ANkAVnwlOb_eoELi4ER5S_VLgX6YWXsP3GHZhLIdNjwBzXNW8E61IM0pbtuDsG8gmMMhoe6qxY_6IehLtL-FU1As9MxsRVfRFnnUl0paHSjUQsxHcqMDy8Oa-FKbnCIaj9ZI8Y5y1HF5YEOH2XA_6T8AUUCaMTZd92ojleMEh9INnrSvpc81epAZVFXMV4f1bwac_O1N5VKRWGxnqczBWaUwDucjHSYJTxTUlCmQ",
	"p": "_4j2ratx8BkrO4sylG5FwwpPtmx5yjGGzlwXYoBjoZ9rR0_ALQprAASB744tRVzErQXaTU2POZfud71kbYgV79l2TeQrtkyToPxDjJYT4O0ET8XLd5tJpmWl4_ifTBw-82c2wsDYCpqpHlFTZ6mci-_rxqZ8sxtkbKk_44XMy_8",
	"q": "2DJ7Qp4ZhVbZjDwY9HdyW1ah98CqhiW-G1-lZYuwPM90ym6Ejhc0yr4qL150VMacZEsZ4gcEacCsrxmV6dqlr2N6XMn6EkpsKbKjowOtC0hKHEdm1iFfPB_T0ZjfpJZZPsL1-wL1tWMfDIkcXfpkyJTaCaVSyMoX3UOE85qo0Wc",
	"qi": "hyrZ9xBy3P4ql7tN0TxUIiiHaIi9HUs2DBHibaRH61_g_Hlf9nzY6KCSlVjRVPC5TSGQy3sj-z7Im-Cpu9r1PQWuXk3zGCxmDcX5ppeJXqoxXT92SZh4IYQMKkgOZwUMKB7ebvw25yJf-BJOmsRu8KhKNNMWkWeOERQw8jrnz0A"
}
2.4 启动【Sample.WebIdentityServer】项目运行测试

创建好项目后,习惯性的启动项目运行测试,正常启动后输出如下信息:

PS E:\CodeStuday\dotnet6\Sample.WebIdentityServer> dotnet run
正在生成...
info: IdentityServer4.Startup[0]
      Starting IdentityServer4 version 4.1.2+997a6cdd643e46cd5762b710c4ddc43574cbec2e
info: IdentityServer4.Startup[0]
      You are using the in-memory version of the persisted grant store. This will store consent decisions, authorizaon codes, refresh and reference tokens in memory only. If you are using any of those features in production, you wanto switch to a different store implementatio
info: IdentityServer4.Startup[0]
      Using the default authentication scheme idsrv for IdentityServer
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5019
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5018
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: E:\CodeStuday\dotnet6\Sample.WebIdentityServer\

从上面的日志信息可以看出:

1. Starting IdentityServer4 version 4.1.2+997a6cdd643e46cd5762b710c4ddc43574cbec2e.
2. You are using the in-memory version of the persisted grant store.
3. Now listening on: https://localhost:5019 & http://localhost:5018.

接下来使用 ApiPost6 工具访问服务监听端口,操作如下:

2.4.1、请求【.well-known/openid-configuration】配置文档:
# url 格式
http://ip:port/.well-known/openid-configuration
# 这里是本地主机,端口 5018
http://localhost:5018/.well-known/openid-configuration

ApiPost 工具访问 url 地址后,控制台输出如下日志信息:

info: IdentityServer4.Hosting.IdentityServerMiddleware[0]
      Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration

快速理解 IdentityServer4 中的认证 & 授权_第11张图片

/.well-known/openid-configuration】接口返回如下信息:

{
	"issuer": "http://localhost:5018",
	"jwks_uri": "http://localhost:5018/.well-known/openid-configuration/jwks",
	"authorization_endpoint": "http://localhost:5018/connect/authorize",
	"token_endpoint": "http://localhost:5018/connect/token",
	"userinfo_endpoint": "http://localhost:5018/connect/userinfo",
	"end_session_endpoint": "http://localhost:5018/connect/endsession",
	"check_session_iframe": "http://localhost:5018/connect/checksession",
	"revocation_endpoint": "http://localhost:5018/connect/revocation",
	"introspection_endpoint": "http://localhost:5018/connect/introspect",
	"device_authorization_endpoint": "http://localhost:5018/connect/deviceauthorization",
	"frontchannel_logout_supported": true,
	"frontchannel_logout_session_supported": true,
	"backchannel_logout_supported": true,
	"backchannel_logout_session_supported": true,
	"scopes_supported": [
		"sample_api",
		"offline_access"
	],
	"claims_supported": [],
	"grant_types_supported": [
		"authorization_code",
		"client_credentials",
		"refresh_token",
		"implicit",
		"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
}
2.4.2、请求【token_endpoint】接口,获取 access_token

ApiPostBody 中配置参数信息(参见下图),使用 POST 方式访问如下地址:

  • http://localhost:5018/connect/token
  • https://localhost:5019/connect/token

快速理解 IdentityServer4 中的认证 & 授权_第12张图片

connect/token】接口响应信息:

{
	"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdENTZGMDMzMUZGNUI5QkZCQ0ZDQjI1REY5RDgxRTQ0IiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NjcxOTMyNTMsImV4cCI6MTY2NzE5Njg1MywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAxOSIsImNsaWVudF9pZCI6InNhbXBsZV9jbGllbnQiLCJqdGkiOiJFRTRBNkEwOUFFMDU2RUYxRTg5NjQzQUQ4RDc4MzkzMSIsImlhdCI6MTY2NzE5MzI1Mywic2NvcGUiOlsic2FtcGxlX2FwaSJdfQ.LKxAAFNjDj3YWXubprAdekWuSEw2ymPAv0YE5u9pEK2zn9ycuYKeqSN29yfaZc0oj4G_QoGKTmlAAdQ5uy72scT4hJYrFXtEjw880GY49rDVd579pFpv7jVWW_324LbNgrAhhJr7l37X3G8wC2sW9ZEvhZM89m6I9DgIUVMUboFUEADcTD5h4twQZ1RjQtcl0DLohcz6c3LPfERqs1QFF8A1bgu_Wszt4ZJADmPLE7wInxPcHpcnEkHl-3xKr1sP0MxhHqQkMpH8SYYnpmUiIwiqRm0qWAfqxEfQmOeVaSqqyGP25vRx2mgGez9DOm7jyOJdiswsGUA2WOQAys8EjA",
	"expires_in": 3600,
	"token_type": "Bearer",
	"scope": "sample_api"
}

3、创建【Sample.WebApi】项目

3.1 使用 cli 创建项目(和上面的项目保存在同一个跟目录)
dotnet new webapi -n Sample.WebApi
3.2 使用 vs 2022 打开项目,添加 nuget 依赖包:

快速理解 IdentityServer4 中的认证 & 授权_第13张图片

项目【Sample.WebApi.csproj】配置文件如下:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0TargetFramework>
    <Nullable>enableNullable>
    <ImplicitUsings>enableImplicitUsings>
  PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.10" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  ItemGroup>

Project>
3.3 项目改造为 Program.cs & Startup.cs 模式

项目同样改造为 Program.cs & Startup.cs 模式,详细操作请看下面。

3.3.1 添加 Startup.cs 文件,代码如下:
  • 新增【Startup.cs】文件,添加如下代码:
namespace Sample.WebApi;

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // Add services to the container. 注册服务到 Ioc 容器
    public void RegisterServices(IServiceCollection services, IHostBuilder host)
    {
        services.AddControllers();

        // 1.添加 IDS4 认证服务
        services.AddAuthentication(defaultScheme: "Bearer")
            .AddJwtBearer(authenticationScheme: "Bearer", configureOptions: options => 
            {
                options.Authority = "https://localhost:5019";
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters 
                {
                   ValidateAudience = false //不验证
                };
            });

        // 2.添加 IDS4 授权策略
        services.AddAuthorization(options => 
        {
            options.AddPolicy("ApiScope", builder =>
            {
                builder.RequireAuthenticatedUser();
                builder.RequireClaim("scope","sample_api");
            });
        });

        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
        services.AddEndpointsApiExplorer();
        // 配置 Swagger 中间件
        services.AddSwaggerGen(options => {
            options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
            {
                Title = "Sample.WebApi",
                Version = "v1"
            });
        });
    }

    // Configure the HTTP request pipeline. 配置 HTTP 请求管道(中间件管道即中间件委托链)
    public void SetupMiddlewares(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            //app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(options => {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "Sample.WebApi");
            });
        }
        //app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseAuthentication(); // 认证
        app.UseAuthorization();  // 授权
        app.UseEndpoints(endopints => 
        {
           endopints.MapControllers(); 
        });
    }
}

注意:务必确保下面两点的配置信息和 IdentityServer 项目的 Config 配置保持一致。

  1. 添加 IDS4 认证服务
  2. 添加 IDS4 授权策略
3.3.2 修改 Program.cs 文件
namespace Sample.WebApi;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        var startup = new Startup(builder.Configuration);
        // Add services to the container.
        startup.RegisterServices(builder.Services, builder.Host);

        var app = builder.Build();
        // Configure the HTTP request pipeline.
        startup.SetupMiddlewares(app, builder.Environment);

        app.Run();
    }
}
3.3.3 新增 IdentityController.cs 文件(webapi)

此处为了和项目默认的 WeatherForecastController(webapi) 参照对比,因此新增 IdentityController.cs 文件,添加如下代码:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Sample.WebApi.Controllers;

/// 
/// identity
/// 
//[Route(template:"Identity")]
[Route("[controller]")]
[Authorize("ApiScope")]
[ApiController]
public class IdentityController : ControllerBase
{
    [Authorize]
    [HttpGet]
    public IActionResult Get() 
    {
        return new JsonResult(from claim in User.Claims select new { claim.Type, claim.Issuer, claim.ValueType });
    }
}

WeatherForecastController.cs 默认代码如下:

using Microsoft.AspNetCore.Mvc;

namespace Sample.WebApi.Controllers;

/// 
/// WeatherForecast
/// 
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
3.4 启动【Sample.WebApi】项目运行测试
PS E:\CodeStuday\dotnet6\Sample.WebApi> dotnet run
正在生成...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5006
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: E:\CodeStuday\dotnet6\Sample.WebApi\

访问 Swagger 页面:

http://localhost:5006/swagger/index.html

快速理解 IdentityServer4 中的认证 & 授权_第14张图片

分别访问上面两个 api 接口,响应信息如下:

  • Identity
curl -X 'GET' \
  'http://localhost:5006/Identity' \
  -H 'accept: */*'

接口显示 Unauthorized 未授权。

快速理解 IdentityServer4 中的认证 & 授权_第15张图片

  • WeatherForecast
curl -X 'GET' \
  'http://localhost:5006/WeatherForecast' \
  -H 'accept: text/plain'

由于该接口未设置特性权限,所以可正常访问。

快速理解 IdentityServer4 中的认证 & 授权_第16张图片

通过上面两个 api 接口的对比测试,说明代码中的授权中间件生效了。

  • 配置 ApiPost6 工具访问 Identity 接口

ApiPost6 工具的【认证】栏添加授权访问的 access_token,使用 GET 方式访问 Identity 接口:

快速理解 IdentityServer4 中的认证 & 授权_第17张图片

上面步骤就演示了基于内存模式使用 IdentityServer4 的案例,该案例是 WebAPI 访问 WebAPI 的项目场景,接下来我们继续创建一个控制台项目,模拟 Console 项目访问 WebAPI 的场景。

4、创建【Sample.ConsoleApp】项目

4.1 创建【Sample.ConsoleApp】项目

使用 dotnet cli 创建项目,执行命令:

 dotnet new console -n Sample.ConsoleApp
4.2 【Sample.ConsoleApp】项目添加 nuget 包

项目新建好后,添加 nuget 包依赖:

在这里插入图片描述

4.3 改造【Sample.ConsoleApp】项目

然后修改 Program.cs 文件,在 Main 方法中添加如下代码:

using IdentityModel.Client;

namespace Sample.ConsoleApp;

internal class Program
{
    static async Task Main(string[] args)
    {
        //阶段一:访问 IDS4 获取身份认证信息
        var client = new HttpClient();
        var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5019");
        if (disco.IsError)
        {
            Console.WriteLine(disco.Error);
            return;
        }

        var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { 
           Address = disco.TokenEndpoint,
           ClientId = "sample_client",
           ClientSecret = "sample_client_secret"
        });

        if (tokenResponse.IsError)
        {
            Console.WriteLine(tokenResponse.Error);
            return;
        }

        Console.WriteLine(tokenResponse.Json);

        //阶段二:访问受保护权限的 api 接口
        var apiClient = new HttpClient();
        apiClient.SetBearerToken(tokenResponse.AccessToken);

        var response = await apiClient.GetAsync("http://localhost:5006/Identity");
        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine(response.StatusCode);
            return;
        }

        var content = await response.Content.ReadAsStringAsync();
        Console.WriteLine(content);

        Console.ReadKey();
    }
}

上述代码,为了模拟两阶段的请求(先认证,再授权),此处直接 new 了两个 HttpClient 对象,生产环境可以使用构造函数 DI 方式注入该对象,或者使用扩展 nuget 包:

  • Refit.HttpClientFactory
4.4 启动【Sample.ConsoleApp】项目运行测试

关于该 nuget 包的使用,请自行查看相关资料,这里不再叙述,接下来我们启动项目运行测试,输出如下信息:

{
	"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdENTZGMDMzMUZGNUI5QkZCQ0ZDQjI1REY5RDgxRTQ0IiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NjcyMDAwMDQsImV4cCI6MTY2NzIwMzYwNCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAxOSIsImNsaWVudF9pZCI6InNhbXBsZV9jbGllbnQiLCJqdGkiOiJFNDk5QjAwRTAyQTc5NkFBRjhCMTIxNkIwODhCRjY4NyIsImlhdCI6MTY2NzIwMDAwNCwic2NvcGUiOlsic2FtcGxlX2FwaSJdfQ.sZ3XKDIjrhiVu-1Twt6BUZMiqQ99coFb_kkThZECO6N68TOpch1_4h0rHggyyu4j-jDHh4itkv5iuCE4nGYiGVF196bh0fd68AqVl6A7IlMN3WfmwnLBgEMSIMNBJMWToEPb8l7y3jB9Uv6QoPbKzFw1k4ghePiFq0Qn7s3qXAefnSfYreUdHaIewJXk5egSOsePQpU_Rm33CvfCT3pKecJ1-mbwpjqn_euCuYmE4sWm8aGwi9DGnZt1W9jUZCoVW8_MoONW1pKnmcN5s-07JBwjFDhdY-EyPwY3b-U7B7jfvTxp7krr4XYOWKIvUZdN8afPDbr-lNbx7dC0mSdWPg",
	"expires_in": 3600,
	"token_type": "Bearer",
	"scope": "sample_api"
}

[{
	"type": "nbf",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
	"type": "exp",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
	"type": "iss",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
	"type": "client_id",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
	"type": "jti",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
	"type": "iat",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
	"type": "scope",
	"issuer": "https://localhost:5019",
	"valueType": "http://www.w3.org/2001/XMLSchema#string"
}]

从上面输出的信息中,我们来分下 access_token 是个啥东东,其实它是一个 jwt 格式的数据,我们使用在线格式化工具来更佳直观的查看数据,如下图所示:

https://jwt.ms/

格式化解析数据:

快速理解 IdentityServer4 中的认证 & 授权_第18张图片

JWT 数据格式

我们简单的回顾下 JWT 的数据格式,由以下三部分组成。

JWT 的组成结构

注意:推荐 jwt 使用 https 协议,不应使用 http ,生产环境安全性考虑,防止信息泄漏。

快速理解 IdentityServer4 中的认证 & 授权_第19张图片

  • Header,头部包含元信息,对明文信息使用 base64 编码值;
  • Payload,负载、载荷配置一些客户端相关信息使用 base64 编码值;
  • Signature,签名部分是使用 Header 部分的加密算法对(Header + Payload )的编码值数据再次加密;

JWT 参数解释

  • 1、头部(Header)
    => alg:签名(加密)算法;
    => kid:令牌ID;
    => typ:令牌类型;

  • 2、载荷(Payload)
    => nbf:jwt 生效(签发)的时间,再此之前 jwt 不可用;
    => exp:jwt 过期时间(大于 nbf 签发时间),在此之后 jwt 失效;
    => iss:jwt 签发人,通常情况为签发服务器地址;
    => client_id:客户端地址;
    => jti:jwt 的唯一标识,作用辨识每一次的 jwt 不会重复;
    => iat:jwt 的签发时间,一般情况和 nbf 相同;
    => scope:api 范围(理解成分组),在该范围内合法有效;

总结

通过上面的项目示例,可以看出 IdentityServer4 框架的核心部分依然是 JWT,通过该框架整合了 认证和授权 两阶段的相关操作,给用户的感觉就是一步到位,无感体验。对应常规性需求的项目,使用 内存模式 亦可满足,只需在 IdentityServerConfig.cs 类中注册相应的配置信息即可,但对应动态改变的配置信息,还可以持久化配置 DB模式,关于该模式的改造步骤,感兴趣的小伙伴自行参看相关文档进行研究,本篇文章的目的是快速入门 IdentityServer4 框架,以帮助更多的小伙伴快速上手体验。

你可能感兴趣的:(.NET,Core,跨平台,认证与授权,认证与授权,IDS4,Security,Token,Identity,Token,JWT)