IdentityServer4系列 | 混合模式

1、前言

在上一篇关于受权码模式中, 已经介绍了关于受权码的基本内容,认识到这是一个拥有更为安全的机制,但这个仍然存在局限,虽然在文中咱们说到经过后端的方式去获取token,这种由web服务器和受权服务器直接通讯,不须要通过用户的浏览器或者其余的地方,可是在这种模式中,受权码仍然是经过前端通道进行传递的,并且在访问资源的中,也会将访问令牌暴露给外界,就仍存在安全隐患。html

快速回顾一下以前初识基础知识点中提到的,IdentityServer4OpenID Connect + OAuth2.0 相结合的认证框架,用户身份认证和API的受权访问,两个结合一块,实现了认证和受权的结合。前端

在几篇关于受权模式篇章中,其中咱们也使用了关于OpenID Connect 的简化流程,在简化流程中,全部令牌(身份令牌、访问令牌)都经过浏览器传输,这对于身份令牌(IdentityToken)来讲是没有问题的,可是若是是访问令牌(AccessToken)直接经过浏览器传输,就增长了必定的安全问题。由于访问令牌比身份令牌更敏感,在非必须的状况下,咱们不但愿将它们暴露给外界。git

因此咱们就会考虑增长安全性,在OpenID Connect 包含一个名为“Hybrid(混合)”的流程,它为咱们提供了一箭双鵰的优点,身份令牌经过浏览器传输,所以客户端能够在进行任何更多工做以前对其进行验证。若是验证成功,客户端会经过令牌服务的以获取访问令牌。github

2、初识

在认识混合模式(Hybrid Flow)时候,能够发现这里跟上一篇的受权码模式有不少类似的地方,具体能够查看受权码模式web

查看使用OpenIDConnect时的安全性和隐私注意事项相关资料能够发现,数据库

受权码模式混合模式的流程步骤分别以下:c#

Authorization Code Flow Steps

The Authorization Code Flow goes through the following steps.后端

  1. Client prepares an Authentication Request containing the desired request parameters.
  2. Client sends the request to the Authorization Server.
  3. Authorization Server Authenticates the End-User.
  4. Authorization Server obtains End-User Consent/Authorization.
  5. Authorization Server sends the End-User back to the Client with an Authorization Code.
  6. Client requests a response using the Authorization Code at the Token Endpoint.
  7. Client receives a response that contains an ID Token and Access Token in the response body.
  8. Client validates the ID token and retrieves the End-User's Subject Identifier.

Hybrid Flow Steps

The Hybrid Flow follows the following steps:api

  1. Client prepares an Authentication Request containing the desired request parameters.
  2. Client sends the request to the Authorization Server.
  3. Authorization Server Authenticates the End-User.
  4. Authorization Server obtains End-User Consent/Authorization.
  5. Authorization Server sends the End-User back to the Client with an Authorization Code and, depending on the Response Type, one or more additional parameters.
  6. Client requests a response using the Authorization Code at the Token Endpoint.
  7. Client receives a response that contains an ID Token and Access Token in the response body.
  8. Client validates the ID Token and retrieves the End-User's Subject Identifier.

由以上对比发现,codehybrid同样都有8个步骤,大部分步骤也是相同的。最主要的区别在于第5步。浏览器

在受权码模式中,成功响应身份验证

HTTP/1.1 302 Found
  Location: https://client.example.org/cb?
    code=SplxlOBeZQQYbYS6WxSbIA
    &state=af0ifjsldkj

在混合模式中,成功响应身份验证:

HTTP/1.1 302 Found
  Location: https://client.example.org/cb#
    code=SplxlOBeZQQYbYS6WxSbIA
    &id_token=eyJ0 ... NiJ9.eyJ1c ... I6IjIifX0.DeWt4Qu ... ZXso
    &state=af0ifjsldkj

其中多了一个id_token

在使用这些模式的时候,成功的身份验证响应,存在指定的差别。这些受权端点的结果以不一样的的依据返回。其中code是必定会返回的,access_token和id_token的返回依据 response_type 参数决定。

混合模式根据response_type的不一样,authorization endpoint返回能够分为三种状况。

  1. response_type = code + id_token ,即包含Access Token和ID Token
  2. response_type = code + token ,即包含Authorization Code和Access Token
  3. response_type = code + id_token + token,即包含Authorization Code、identity Token和Access Token

3、实践

接着咱们进行一些简单的实践,由于有了前面受权码模式代码的经验,编写混合模式也是很简单的。

(这里重复以前的代码,防止被爬抓后内容的缺失不完整)

在示例实践中,咱们将建立一个受权访问服务,定义一个MVC客户端,MVC客户端经过IdentityServer上请求访问令牌,并使用它来访问API。

3.1 搭建 Authorization Server 服务

搭建认证受权服务

3.1.1 安装Nuget包

IdentityServer4 程序包

3.1.2 配置内容

创建配置内容文件Config.cs

public static class Config
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        new IdentityResource[]
    {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
    };

    public static IEnumerable<ApiScope> ApiScopes =>
        new ApiScope[]
    {
        new ApiScope("hybrid_scope1")
    };

    public static IEnumerable<ApiResource> ApiResources =>
        new ApiResource[]
    {
        new ApiResource("api1","api1")
        {
            Scopes={ "hybrid_scope1" },
            UserClaims={JwtClaimTypes.Role},  //添加Cliam 角色类型
            ApiSecrets={new Secret("apipwd".Sha256())}
        }
    };

    public static IEnumerable<Client> Clients =>
        new Client[]
    {
        new Client
        {
            ClientId = "hybrid_client",
            ClientName = "hybrid Auth",
			ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
            AllowedGrantTypes = GrantTypes.Hybrid,

            RedirectUris ={
                "http://localhost:5002/signin-oidc", //跳转登陆到的客户端的地址
            },
            // RedirectUris = {"http://localhost:5002/auth.html" }, //跳转登出到的客户端的地址
            PostLogoutRedirectUris ={
                "http://localhost:5002/signout-callback-oidc",
            },
            ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },

            AllowedScopes = {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                "hybrid_scope1"
            },
            //容许将token经过浏览器传递
            AllowAccessTokensViaBrowser=true,
            // 是否须要赞成受权 (默认是false)
            RequireConsent=true
        }
    };
}

RedirectUris : 登陆成功回调处理的客户端地址,处理回调返回的数据,能够有多个。

PostLogoutRedirectUris :跳转登出到的客户端的地址。

这两个都是配置的客户端的地址,且是identityserver4组件里面封装好的地址,做用分别是登陆,注销的回调

由于是混合受权的方式,因此咱们经过代码的方式来建立几个测试用户。

新建测试用户文件TestUsers.cs

public class TestUsers
    {
        public static List<TestUser> Users
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality = "Heidelberg",
                    postal_code = 69118,
                    country = "Germany"
                };

                return new List<TestUser>
                {
                    new TestUser
                    {
                        SubjectId = "1",
                        Username = "i3yuan",
                        Password = "123456",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "i3yuan Smith"),
                            new Claim(JwtClaimTypes.GivenName, "i3yuan"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                    }
                };
            }
        }
    }

返回一个TestUser的集合。

经过以上添加好配置和测试用户后,咱们须要将用户注册到IdentityServer4服务中,接下来继续介绍。

3.1.3 注册服务

在startup.cs中ConfigureServices方法添加以下代码:

public void ConfigureServices(IServiceCollection services)
        {
            var builder = services.AddIdentityServer()
               .AddTestUsers(TestUsers.Users); //添加测试用户

            // in-memory, code config
            builder.AddInMemoryIdentityResources(Config.IdentityResources);
            builder.AddInMemoryApiScopes(Config.ApiScopes);
            builder.AddInMemoryApiResources(Config.ApiResources);
            builder.AddInMemoryClients(Config.Clients);

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();
            services.ConfigureNonBreakingSameSiteCookies();
        }

3.1.4 配置管道

在startup.cs中Configure方法添加以下代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            }); 
        }

以上内容是快速搭建简易IdentityServer项目服务的方式。

这搭建 Authorization Server 服务跟上一篇受权码模式有何不一样之处呢?

  1. 在Config中配置客户端(client)中定义了一个AllowedGrantTypes的属性,这个属性决定了Client能够被哪一种模式被访问,GrantTypes.Hybrid混合模式。因此在本文中咱们须要添加一个Client用于支持受权码模式(Hybrid)。

3.2 搭建API资源

实现对API资源进行保护

3.2.1 快速搭建一个API项目

3.2.2 安装Nuget包

IdentityServer4.AccessTokenValidation 包

3.2.3 注册服务

在startup.cs中ConfigureServices方法添加以下代码:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        services.AddAuthentication("Bearer")
          .AddIdentityServerAuthentication(options =>
          {
              options.Authority = "http://localhost:5001";
              options.RequireHttpsMetadata = false;
              options.ApiName = "api1";
              options.ApiSecret = "apipwd"; //对应ApiResources中的密钥
          });
    }

AddAuthentication把Bearer配置成默认模式,将身份认证服务添加到DI中。

AddIdentityServerAuthentication把IdentityServer的access token添加到DI中,供身份认证服务使用。

3.2.4 配置管道

在startup.cs中Configure方法添加以下代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }    
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }

UseAuthentication将身份验证中间件添加到管道中;

UseAuthorization 将启动受权中间件添加到管道中,以便在每次调用主机时执行身份验证受权功能。

3.2.5 添加API资源接口

[Route("api/[Controller]")]
[ApiController]
public class IdentityController:ControllerBase
{
    [HttpGet("getUserClaims")]
    [Authorize]
    public IActionResult GetUserClaims()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

在IdentityController 控制器中添加 [Authorize] , 在进行请求资源的时候,需进行认证受权经过后,才能进行访问。

3.3 搭建MVC 客户端

实现对客户端认证受权访问资源

3.3.1 快速搭建一个MVC项目

3.3.2 安装Nuget包

IdentityServer4.AccessTokenValidation 包

3.3.3 注册服务

要将对 OpenID Connect 身份认证的支持添加到MVC应用程序中。

在startup.cs中ConfigureServices方法添加以下代码:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAuthorization();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
               .AddCookie("Cookies")  //使用Cookie做为验证用户的首选方式
              .AddOpenIdConnect("oidc", options =>
              {
                  options.Authority = "http://localhost:5001";  //受权服务器地址
                  options.RequireHttpsMetadata = false;  //暂时不用https
                  options.ClientId = "hybrid_client";
                  options.ClientSecret = "511536EF-F270-4058-80CA-1C89C192F69A";
                  options.ResponseType = "code id_token"; //表明
                  options.Scope.Add("hybrid_scope1"); //添加受权资源
                  options.SaveTokens = true; //表示把获取的Token存到Cookie中
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
         services.ConfigureNonBreakingSameSiteCookies();
    }
  1. AddAuthentication注入添加认证受权,当须要用户登陆时,使用 cookie 来本地登陆用户(经过“Cookies”做为DefaultScheme),并将 DefaultChallengeScheme 设置为“oidc”,
  2. 使用 AddCookie 添加能够处理 cookie 的处理程序。
  3. AddOpenIdConnect用于配置执行 OpenID Connect 协议的处理程序和相关参数。Authority代表以前搭建的 IdentityServer 受权服务地址。而后咱们经过ClientIdClientSecret,识别这个客户端。 SaveTokens用于保存从IdentityServer获取的token至cookie,ture标识ASP.NETCore将会自动存储身份认证session的access和refresh token。
  4. 咱们在配置ResponseType时须要使用Hybrid定义的三种状况之一,具体代码如上所述。

3.3.4 配置管道

而后要确保认证服务执行对每一个请求的验证,加入UseAuthenticationUseAuthorizationConfigure中,在startup.cs中Configure方法添加以下代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }

UseAuthentication将身份验证中间件添加到管道中;

UseAuthorization 将启动受权中间件添加到管道中,以便在每次调用主机时执行身份验证受权功能。

3.3.5 添加受权

在HomeController控制器并添加[Authorize]特性到其中一个方法。在进行请求的时候,需进行认证受权经过后,才能进行访问。

[Authorize]
        public IActionResult Privacy()
        {
            ViewData["Message"] = "Secure page.";
            return View();
        }

还要修改主视图以显示用户的Claim以及cookie属性。

@using Microsoft.AspNetCore.Authentication

<h2>Claims</h2>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

<h2>Properties</h2>

<dl>
    @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
    {
        <dt>@prop.Key</dt>
        <dd>@prop.Value</dd>
    }
</dl>

访问 Privacy 页面,跳转到认证服务地址,进行帐号密码登陆,Logout 用于用户的注销操做。

3.3.6 添加资源访问

HomeController控制器添加对API资源访问的接口方法。在进行请求的时候,访问API受保护资源。

/// <summary>
        /// 测试请求API资源(api1)
        /// </summary>
        /// <returns></returns>
        public async Task<IActionResult> getApi()
        {
            var client = new HttpClient();
            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (string.IsNullOrEmpty(accessToken))
            {
                return Json(new { msg = "accesstoken 获取失败" });
            }
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            var httpResponse = await client.GetAsync("http://localhost:5003/api/identity/GetUserClaims"); 
            var result = await httpResponse.Content.ReadAsStringAsync();
            if (!httpResponse.IsSuccessStatusCode)
            {
                return Json(new { msg = "请求 api1 失败。", error = result });
            }
            return Json(new
            {
                msg = "成功",
                data = JsonConvert.DeserializeObject(result)
            });
        }

测试这里经过获取accessToken以后,设置client请求头的认证,访问API资源受保护的地址,获取资源。

3.4 效果

咱们经过对比受权码模式与混合模式 能够发现,在大部分步骤是相同的,但也存在一些差别。

在整个过程当中,咱们使用抓取请求,能够看到在Authorization Endpoint中二者的区别以下:

受权码模式:

混合模式:

在Authorization EndPoint返回的Id_Token和Token EndPoint返回的id_Token中,能够看到两次值是可能不相同的,可是其中包含的用户信息都是同样的。

在使用Hybrid时咱们看到受权终结点返回的Id Token中包含at_hash(Access Token的哈希值)和s_hash(State的哈希值),规范中定义了如下的一些检验规则。

  1. 两个id_token中的 iss 和 sub 必须相同。
  2. 若是任何一个 id token 中包含关于终端用户的声明,两个令牌中提供的值必须相同。
  3. 关于验证事件的声明必须都提供。
  4. at_hash 和 s_hash 声明可能会从 token 端点返回的令牌中忽略,即便从 authorize 端点返回的令牌中已经声明。

4、问题

4.1 设置RequirePkce

在指定基于受权码的令牌是否须要验证密钥,默认为true。

解决方法:

修改Config中的RequirePkce为false便可。这样服务端便不在须要客户端提供code challeng。

RequirePkce = false,//v4.x须要配置这个

4.2 设置ResponseType

在上文中提到的MVC客户端中配置ResponseType时可使用Hybrid定义的三种状况。

而当设置为"code token", "code id_token token"中的一种,即只要包含token,都会报以下错误:

解决方法:

受权服务端中的Config中增长容许将token经过浏览器传递

AllowAccessTokensViaBrowser = true,

5、总结

  1. 因为令牌都经过浏览器传输,为了提升更好的安全性,咱们不想暴露访问令牌, OpenID Connect包含一个名为“Hybrid(混合)”的流程,它可让身份令牌(id_token)经过前端浏览器通道传输,所以客户端能够在作更多的工做以前验证它。 若是验证成功,客户端会打开令牌服务的后端服务器通道来检索访问令牌(access_token)。

  2. 在后续会对这方面进行介绍继续说明,数据库持久化问题,以及如何应用在API资源服务器中和配置在客户端中,会进一步说明。

  3. 若是有不对的或不理解的地方,但愿你们能够多多指正,提出问题,一块儿讨论,不断学习,共同进步。

  4. 项目地址

6、附加

OpenID Connect资料

Grant Types 类型

相关文章
相关标签/搜索