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

简介: 在实际的生产环境中,存在各种各样的应用程序相互访问,当用户访问 `app` 应用的时候,为了安全性考虑,通常都会要求搭配授权码或者安全令牌服务一并访问,这样可有效地对 `Server` 端的 `API` 资源起到一定程度的有效保护...

前言

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

image.png

接下来我们就针对这个 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,这将形成以下体系结构和协议:

image.png

认证 & 授权

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

image.png

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

什么是 Authentication(认证)?

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

常见的身份验证协议

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

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

相关文章:

什么是 Authorization(授权)?

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

常见的授权协议

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

image.png

OAuth 2.0 协议特点

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

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

相关文章:

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

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

image.png

IdentityServer4 框架

什么是 IdentityServer4 ?

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

image.png

  • 官方解释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 工作原理

image.png

关于 IDS4 的术语解释:

  • User(用户):用户是使用注册的客户端访问资源的人。
  • Clients(客户端):客户端是从 IdentityServer 请求令牌的软件,用于验证用户(请求身份令牌)或访问资源(请求访问令牌)。 必须首先向 IdentityServer 注册客户端才能请求令牌。

客户端的示例可以是:Web 应用程序、移动或桌面应用程序、SPA、服务器进程等。

  • Resources(资源):资源是要用 IdentityServer 保护的资源,包括用户的身份信息或API。

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

  • Identity Token(身份令牌):身份令牌表示身份验证过程的结果。
    它至少包含:1、用户的标识;2、用户如何以及何时进行身份验证的信息。它也可以包含其他身份信息。
  • Access Token(访问令牌):访问令牌允许用户访问 API 资源,客户端请求访问令牌并将其转发到 API。
    访问令牌包含有关客户端和用户的信息,API 使用该信息来授权用户访问它的数据。

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

IdentityServer4 应用示例

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

image.png

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 项目(等效同上),这里先给出改造好的项目结构如下:

image.png

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

2.1、添加 Nuget 包 IdentityServer4

image.png

添加完成后,项目 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

image.png

/.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 方式访问如下地址:

image.png

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 依赖包:

image.png

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

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

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </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;

/// <summary>
/// identity
/// </summary>
//[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;

/// <summary>
/// WeatherForecast
/// </summary>
[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

image.png

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

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

接口显示 Unauthorized 未授权。

image.png

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

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

image.png

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

  • 配置 ApiPost6 工具访问 Identity 接口

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

image.png

上面步骤就演示了基于内存模式使用 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 包依赖:

image.png

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/

格式化解析数据:

image.png

JWT 数据格式

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

JWT 的组成结构

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

image.png

  • 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 框架,以帮助更多的小伙伴快速上手体验。

目录
相关文章
|
数据安全/隐私保护
关于 OAuth 2.0 统一认证授权
随着互联网的巨头大佬逐渐积累了海量的用户与数据,用户的需求越来越多样化,为了满足用户在不同平台活动的需求,平台级的厂商则需要以接口的形式开放给第三方开发者,这样满足了用户的多样性需求,也可以让自己获得利益,让数据流动起来,形成给一个良性的生态环境,最终达到用户、平台商、第三方开发者共赢。
2965 0
|
23天前
|
安全 API 数据安全/隐私保护
基于Keycloak的认证与授权
【10月更文挑战第27天】Keycloak 是一个开源的身份和访问管理解决方案,提供用户认证、授权、单点登录等功能,保护应用程序和服务的安全。其认证流程包括用户登录、凭证验证、身份验证令牌生成、令牌返回给应用、应用验证令牌、用户身份确认。Keycloak 支持资源定义、权限定义、角色创建与分配、用户角色分配、访问请求与授权决策等授权流程。其优势在于集中式管理、高安全性、良好扩展性和社区支持。适用于企业应用集成、微服务架构、移动应用及 API 安全等多种场景。
|
4月前
|
存储 Java Maven
使用Java实现OAuth 2.0认证授权
使用Java实现OAuth 2.0认证授权
|
3月前
|
安全 生物认证 数据安全/隐私保护
用户认证与授权
【8月更文挑战第10天】
56 1
|
3月前
|
数据安全/隐私保护
OAuth 2.0身份验证及授权
8月更文挑战第24天
148 0
|
4月前
|
安全 前端开发 Java
实现基于OAuth2的安全认证与授权
实现基于OAuth2的安全认证与授权
|
JSON 前端开发 数据格式
SpringSecurity基础-认证授权结果处理
在传统的应用中,认证成功后页面需要跳转到认证成功页面或者跳转到个人中心页,但是在前后端分离的项目通常是使用Ajax请求完成认证,这时候我们需要返回一个JSON结果告知前端认证结果,然后前端自行跳转页面。 要做到上述功能,我们需要自定义认证成功处理器实现AuthenticationSuccessHandler接口复写 onAuthenticationSuccess方法,该方法其中一个参数是Authentication ,他里面封装了认证信息,用户信息UserDetails等,我们需要在这个方法中使用Response写出json数据即可
139 0
|
JSON 前端开发 数据格式
六.SpringSecurity基础-认证授权结果处理
SpringSecurity基础-认证授权结果处理
|
Python
基于flask-oidc的OIDC协议授权码模式单点登录SSO实现
基于flask-oidc的OIDC协议授权码模式单点登录SSO实现
316 0
|
XML 安全 C++
认证与授权——单点登录协议盘点:OpenID vs OAuth2 vs SAML
无论是Web端还是移动端,现在第三方应用账户登录已经成为了标配,任意打开个网站都可以看到,QQ/微信账号登录的字样。使用第三方账户的登录的过程,既要限制用户身份只让有效注册用户才能登录,还要根据注册用户的不同身份来控制能浏览的内容,这就需要认证和授权 相关文章链接: OAuth2.
2218 0