.Net6 微服务之Ocelot+IdentityServer4入门看这篇就够了

发布时间 2023-03-22 19:12:03作者: Mamba24⁸

前言

.Net 6 使用 Consul 实现服务注册与发现 看这篇就够了
.Net6 使用 Ocelot + Consul 看这篇就够了
.Net6 微服务之Polly入门看这篇就够了


书接上文,本文将继续建立在 .Net6 使用 Ocelot + Consul 看这篇就够了文章中的项目基础上来进行OcelotIdentityServer4的介绍。项目也都比较简单,不熟悉的同学可以去翻翻我之前的文章,相信都能一看就会。其实网上关于Id4的文章还是比较多了,本文只会对其进行简单的介绍,不会过多深究,感兴趣的同学可以自行研究。然后本文只是个人学习与分享,不喜勿喷,谢谢!

什么是IdentityServer4?

terminology.png
IdentityServer 是一个 OpenID Connect 提供者—— 它实现了 OpenID Connect 和 OAuth 2.0 协议。

  • User:用户
  • Client:客户端
  • Resources:Identity Data(身份数据)、Apis
  • Identity Server:认证授权服务器
  • Token:Access Token(访问令牌)和 Identity Token(身份令牌)

基于以上我们先来简单了解 OAuth2.0 与 OpenID Connect是个什么东西,它们能干些什么?解决了哪些问题以及我们怎么使用它们?我们需要带着一些问题来学习新东西才会事半功倍。

OAuth2

OAuth是一个开放授权标准,是一个授权协议,并不是认证协议,它无法提供完善的身份认证功能,它解决的问题是授权 。本文介绍的版本为2.0。
它允许用户让第三方应用访问该用户在某服务的特定私有资源,但是不提供账号密码给第三方用户。然后它是通过给用户提供Token(令牌)的方式来访问他们存放在特定服务商上的数据,每一个Token授权一个特定的网站内访问特定的资源,而不是一味的放开所有内容
下面是OAuth 2.0的运行流程图以及相关介绍:

Client:客户端
Resource Owner资源所有者
Authorization Server认证服务器,即服务提供商专门用来处理认证的服务器。
Resource Server资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

168328-20161228154537320-2101529434.jpg

(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。

简单了解之后,我们可以发现B步骤是关键,即用户怎么才能给客户端授权。只有拿到这个授权之后,客户端才能获取到令牌,从而凭借令牌获取资源。所以OAuth2.0为此定义了四种授权方式,如下:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

这里我们只做简单了解,就不展开了,感兴趣的同学可以参考官网或者大佬的文章。

OpenId Connect(OIDC)

OpenId Connect = OIDC = Authentication + Authorization + OAuth2.0
OIDC是一个基于OAuth2协议身份认证标准协议,解决的问题是用户认证,而不关心授权
我们都知道OAuth2是一个授权协议,它无法提供完善的身份认证功能,所以OIDC干的活就是使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动APP,JS应用),且完全兼容OAuth2,也就是说你搭建了一个OIDC的服务后,也可以当作一个OAuth2的服务来用。

下面是OIDC的运行流程图以及相关介绍:

  • EU:End User:一个人类用户。
  • RP:Relying Party ,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方;
  • OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息;
  • ID Token:JWT格式的数据,包含EU身份认证的信息。
  • UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access Token访问时,返回授权用户的信息,此接口必须使用HTTPS。
  • AuthN:Authentication 认证
  • AuthZ:Authorization 授权

168328-20170530092134102-504300351.jpg

  • RP发送一个认证请求给OP;
  • OP对EU进行身份认证,然后提供授权;
  • OP把ID TokenAccess Token(需要的话)返回给RP;
  • RP使用Access Token发送一个请求UserInfo EndPoint;
  • UserInfo EndPoint返回EU的Claims。

注意这里面RP发往OP的请求,是属于Authentication类型的请求,虽然在OIDC中是复用OAuth2的Authorization请求通道,但是用途是不一样的,且OIDC的AuthN请求中scope参数必须要有一个值为的openid的参数,用来区分这是一个OIDC的Authentication请求,而不是OAuth2的Authorization请求。
然后我们简单了解下 ID Token

ID Token

OIDC对OAuth2最主要的扩展就是提供了ID Token.
ID Token是一个安全令牌,是一个授权服务器提供的包含用户信息(由一组Cliams构成以及其他辅助的Cliams)的JWT格式的数据结构。
ID Token的主要构成部分如下(使用OAuth2流程的OIDC)。

  • iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是一个https的url(不包含querystring和fragment部分)。
  • sub = Subject Identifier:必须。iss提供的EU的标识,在iss范围内唯一。它会被RP用来标识唯一的用户。最长为255个ASCII个字符。
  • aud = Audience(s):必须。标识ID Token的受众。必须包含OAuth2的client_id。
  • exp = Expiration time:必须。过期时间,超过此时间的ID Token会作废不再被验证通过。
  • iat = Issued At Time:必须。JWT的构建的时间。
  • auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送AuthN请求的时候携带max_age的参数,则此Claim是必须的。
  • nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID Token和RP本身的Session信息。
  • acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。
  • amr = Authentication Methods References:可选。表示一组认证方法。
  • azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。

这里只是简单介绍,感兴趣的同学可以去官网了解更多。下面开始实操。
抛玉引砖 O(∩_∩)O !

项目准备

.Net 6
Visual Studio 2022
https://github.com/fengzhonghao8-24/Consul.Ocelot

搭建IdentityServer4项目

我们基于上篇项目来新增一个IdentityServer4Center项目
image.png
引入IdentityServer4 Nuget包,然后新增一个配置文件 Config.cs 配置模拟数据
定义API范围

//https://www.cnblogs.com/Mamba8-24
public static IEnumerable<ApiScope> ApiScopes =>
    new List<ApiScope>
    {
       new ApiScope("serviceA"),
       new ApiScope("serviceB")
    };

定义API资源

//https://www.cnblogs.com/Mamba8-24
public static IEnumerable<ApiResource> GetApiResources()
{
    return new List<ApiResource>()
    {
        new ApiResource("serviceA","serviceA"){ Scopes={ "serviceA" } },
        new ApiResource("serviceB","serviceB"){ Scopes={ "serviceB" } }
    };
}

定义客户端

//https://www.cnblogs.com/Mamba8-24
public static IEnumerable<Client> GetClients()
{
    return new List<Client>
    {
       new Client{
           ClientId="web_client",//客户端唯一标识
           ClientName="AuthCenter",
           //AllowedGrantTypes=new List<string>{ "paas_password", "paas_auth_code", "client_credentials"},
           AllowedGrantTypes=GrantTypes.ClientCredentials,
           ClientSecrets=new[]{new Secret("Mamba24".Sha256()) },//客户端密码,进行了加密
           AccessTokenLifetime=3600,
           AllowedScopes=new List<string>//允许访问的资源
           {
                "serviceA",
                "serviceB"
           },
           Claims=new List<ClientClaim>(){
           new ClientClaim(IdentityModel.JwtClaimTypes.Role,"Admin"),
           new ClientClaim(IdentityModel.JwtClaimTypes.NickName,"Mamba24"),
           }
       }
    };
}

然后在Program.cs进行DI

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddIdentityServer()
    .AddDeveloperSigningCredential()
    .AddInMemoryClients(Config.GetClients())//Client模式
    .AddInMemoryApiScopes(Config.ApiScopes)//作用域
    .AddInMemoryApiResources(Config.GetApiResources());//资源

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseIdentityServer();//使用Id4

然后我们将项目run起来看看效果:
image.png
使用Postman请求Token
image.png
这里请求参数内容都是在定义客户端的时候定义好的,只需要注意 grant_type 这里为 client_credentials 客户端模式,如果在定义客户端的时候AllowedGrantTypes 配置的是 ResourceOwnerPassword grant_typepassword,具体参考GrantTypes.cs。
image.png
OK,IdentityServer4Center项目搭建完成。接下来我们将网关与 IdentityServer4 结合使用

Ocelot结合dentityServer4

接下来会模拟通过集成了Id4的网关来访问ServiceA或者ServiceB服务以达到演示效果。
首先我们在Gateway项目中装两个包
image.png
然后在ocelot.json 文件中增加Id4鉴权资源

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{url}", //下游(转发的服务地址模板)
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5550
        }
      ],
      "UpstreamPathTemplate": "/ocelot/{url}", //上游(请求路径模板)
      "UpstreamHttpMethod": [ "Get", "Post" ],
      //鉴权
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "serviceA", //指定一个key
        "AllowedScopes": [ "serviceA" ] //id4的作用域名称
      }
    },
    {
      "DownstreamPathTemplate": "/{url}", //下游(转发的服务地址模板)
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5551
        }
      ],
      "UpstreamPathTemplate": "/ocelotB/{url}", //上游(请求路径模板)
      "UpstreamHttpMethod": [ "Get", "Post" ],
      //鉴权
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "serviceB", //指定一个key
        "AllowedScopes": [ "serviceB" ] //id4的作用域名称
      }
    }
  ]
}

然后在 Program.cs进行配置
由于Ocelot网关默认集成了Id4所以我们只需要在之前代码的基础上增加对需要进行鉴权服务的配置。

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

var authenticationProviderKey = "serviceA";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
              .AddIdentityServerAuthentication(authenticationProviderKey, options =>
              {
                  options.Authority = "http://localhost:5269";//id4服务地址
                  options.ApiName = "serviceA";//id4 api资源里的apiname
                  options.RequireHttpsMetadata = false; //不使用https
                  options.SupportedTokens = SupportedTokens.Both;
              });

authenticationProviderKey = "serviceB";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
              .AddIdentityServerAuthentication(authenticationProviderKey, options =>
              {
                  options.Authority = "http://localhost:5269";//id4服务地址
                  options.ApiName = "serviceB";//id4 api资源里的apiname
                  options.RequireHttpsMetadata = false; //不使用https
                  options.SupportedTokens = SupportedTokens.Both;
              });

builder.Services.AddOcelot().AddConsul();

到这里我们就差不多简单的配置完成了,接下来看看效果。
同时启动GatewayIdentityServer4CenterServiceAServiceB项目
网关项目启动地址端口为5055
image.png
我们现在直接通过网关来请求ServiceA服务的接口(不带token) http://localhost:5055/ocelot/testA
结果出现401,达到预期效果
image.png
接着我们带上token再次访问
返回200,请求成功。
image.png
然后接下来我们给ServiceB 服务单独增加授权认证,模拟请求绕过网关直接访问下游服务。
Program.cs
image.png
image.png
代码很简单,我们单独访问ServiceB看看效果
image.png
结果出现 401
然后我们拿着token去访问
image.png
可以正常访问
到此,我们基于 客户端模式(client credentials)模拟的一个简单的示例就完成了,这个模式没有用户参与也就是说此模式只适用于在资源或者资源服务器不属于某个人或者用户,没有资源所有者对资源进行控制,但是客户端需要访问这个受保护资源的情况下。
接下来我们可以尝试在客户端模式(client credentials)下访问Id4的身份认证资源(对应用户)
我们稍微调整代码
Config.cs
image.png
image.png
模拟用户
image.png
Program.cs
image.png
然后我们先在 client credentials 客户端模式 下请求token,去访问Id4的身份认证资源。
image.png
出现 403异常,所以在这个模式下这样的请求是不合理的,这也就验证了我们上述所说。
image.png

接下来我们可以尝试模拟 密码模式(resource owner password credentials)这个模式会有用户参与
image.png
然后再次访问Id4的身份认证资源
image.png
OK,成功拿到资源信息。

结尾

由于文章篇幅有限,涉及到知识内容也不是很深入,感兴趣的同学可以自行研究。
然后本文都是基于我的个人理解,然后也有参考官网以及大佬的文章和视频,文章如有什么不妥的地方欢迎指正,共同进步。后续有时间还会继续学习相关技术知识,欢迎Star与关注。谢谢??!

参考链接

https://www.bilibili.com/video/BV16b411k7yM?p=5
https://identityserver4docs.readthedocs.io/zh_CN/latest/index.html
https://www.cnblogs.com/linianhui/category/929878.html

源码地址

https://github.com/fengzhonghao8-24/Consul.Ocelot.IdentityServer4.git