IdentityServer4系列 | 资源密码凭证模式

1、前言

从上一篇关于客户端凭证模式中,咱们经过建立一个认证受权访问服务,定义一个API和要访问它的客户端,客户端经过IdentityServer上请求访问令牌,并使用它来控制访问API。其中,咱们也注意到了在4.x版本中于以前3.x版本之间存在的差别。javascript

因此在这一篇中,咱们将经过多种受权模式中的资源全部者密码凭证受权模式进行说明,主要针对介绍IdentityServer保护API的资源,资源密码凭证受权访问API资源。html

2、初识

若是你高度信任某个应用Client,也容许用户把用户名和密码,直接告诉该应用Client。该应用Client就使用你的密码,申请令牌,这种方式称为"密码式"(password)。java

这种模式适用于鉴权服务器与资源服务器是高度相互信任的,例如两个服务都是同个团队或者同一公司开发的。git

2.1 适用范围

资源全部者密码凭证受权模式,适用于当资源全部者与客户端具备良好信任关系的场景,好比客户端是设备的操做系统或具有高权限的应用。受权服务器在开放此种受权模式时必须格外当心,而且只有在别的模式不可用时才容许这种模式。github

这种模式下,应用client可能存了用户密码这不安全性问题,因此才须要高可信的应用。数据库

主要适用于用来作遗留项目升级为oauth2的适配受权使用,固然若是client是自家的应用,也是能够的,同时支持refresh token。c#

例如,A站点 须要添加了 OAuth 2.0 做为对其现有基础架构的一个受权机制。对于现有的客户端转变为这种受权方案,资源全部者密码凭据受权将是最方便的,由于他们只需使用现有的账户详细信息(好比用户名和密码)来获取访问令牌。api

2.2 密码受权流程:

+----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          v
          |    Resource Owner
         (A) Password Credentials
          |
          v
     +---------+                                  +---------------+
     |         |>--(B)---- Resource Owner ------->|               |
     |         |         Password Credentials     | Authorization |
     | Client  |                                  |     Server    |
     |         |<--(C)---- Access Token ---------<|               |
     |         |    (w/ Optional Refresh Token)   |               |
     +---------+                                  +---------------+

资源全部者密码凭证受权流程描述安全

(A)资源全部者向客户端提供其用户名和密码。服务器

(B)客户端从受权中请求访问令牌服务器的令牌端点,以获取访问令牌。当发起该请求时,受权服务器须要认证客户端的身份。

(C) 受权服务器验证客户端身份,同时也验证资源全部者的凭据,若是都经过,则签发访问令牌。

2.2.1 过程详解


访问令牌请求
参数 是否必须 含义
grant_type 必需 受权类型,值固定为“password”。
username 必需 用户名
password 必需 密码
scope 可选 表示受权范围。

同时将容许其余请求参数client_idclient_secret,或在HTTP Basic auth标头中接受客户端ID和密钥。

验证用户名密码

示例:
客户端身份验证两种方式
一、Authorization: Bearer base64(resourcesServer:123)
二、client_id(客户端标识),client_secret(客户端秘钥),username(用户名),password(密码)。

(用户的操做:输入帐号和密码)

A 网站要求用户提供 B 网站的用户名和密码。拿到之后,A 就直接向 B 请求令牌。

POST /oauth/token HTTP/1.1
Host: authorization-server.com
 
grant_type=password
&username=user@example.com
&password=1234luggage
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx

上面URL中,grant_type参数是受权方式,这里的password是“密码式”,username和password是B的用户名和密码。

2.2.2 访问令牌响应

第二步,B 网站验证身份经过后,直接给出令牌。注意,这时不须要跳转,而是把令牌放在 JSON 数据里面,做为 HTTP 回应,A 所以拿到令牌。

响应给用户令牌信息(access_token),以下所示

{
  "access_token": "访问令牌",
  "token_type": "Bearer",
  "expires_in": 4200,
  "scope": "server",
  "refresh_token": "刷新令牌"
}

用户使用这个令牌访问资源服务器,当令牌失效时使用刷新令牌去换取新的令牌。

这种方式须要用户给出本身的用户名/密码,显然风险很大,所以只适用于其余受权方式都没法采用的状况,并且必须是用户高度信任的应用。

3、实践

在示例实践中,咱们将建立一个受权访问服务,定义一个API和要访问它的客户端,客户端经过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("password_scope1")
            };

        public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {

                new ApiResource("api1","api1")
                {
                    Scopes={ "password_scope1" },
                    ApiSecrets={new Secret("apipwd".Sha256())}  //api密钥
                }
            };

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                 new Client
                {
                    ClientId = "password_client",
                    ClientName = "Resource Owner Password",

                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },

                    AllowedScopes = { "password_scope1" }
                },
            };
    }

由于是资源全部者密码凭证受权的方式,因此咱们经过代码的方式来建立几个测试用户。

新建测试用户文件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();
        }

3.1.4 配置管道

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

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseRouting();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }

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

这搭建 Authorization Server 服务跟上一篇客户端凭证模式有何不一样之处呢?

  1. 在Config中配置客户端(client)中定义了一个AllowedGrantTypes的属性,这个属性决定了Client能够被哪一种模式被访问,GrantTypes.ClientCredentials客户端凭证模式GrantTypes.ResourceOwnerPassword资源全部者密码凭证受权。因此在本文中咱们须要添加一个Client用于支持资源全部者密码凭证模式(ResourceOwnerPassword)。
  2. 由于资源全部者密码凭证模式须要用到用户名和密码因此要添加用户,而客户端凭证模式不须要,这也是二者的不一样之处。

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.AddAuthorization();

        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] , 在进行请求资源的时候,需进行认证受权经过后,才能进行访问。

这搭建API资源跟上一篇客户端凭证模式有何不一样之处呢?

咱们能够发现这跟上一篇基本类似,可是可能须要注意的地方应该是ApiNameApiSecret,要跟你配置的API资源名称和API资源密钥相同。

3.3 搭建Client客户端

实现对API资源的访问和获取资源

3.3.1 搭建一个窗体程序

3.3.2 安装Nuget包

IdentityModel

3.3.3 获取令牌

客户端与受权服务器进行身份验证并向令牌端点请求访问令牌。受权服务器对客户端进行身份验证,若是有效,颁发访问令牌。

IdentityModel 包括用于发现 IdentityServer 各个终结点(EndPoint)的客户端库。

咱们可使用从 IdentityServer 元数据获取到的Token终结点请求令牌:

private void getToken_Click(object sender, EventArgs e)
        {
            var client = new HttpClient();
            var disco = client.GetDiscoveryDocumentAsync(this.txtIdentityServer.Text).Result;
            if (disco.IsError)
            {
                this.tokenList.Text = disco.Error;
                return;
            }
            //请求token
            tokenResponse = client.RequestPasswordTokenAsync(new PasswordTokenRequest
            {
                Address = disco.TokenEndpoint,
                ClientId =this.txtClientId.Text,
                ClientSecret = this.txtClientSecret.Text,
                Scope = this.txtApiScopes.Text,
                UserName=this.txtUserName.Text,
                Password=this.txtPassword.Text
            }).Result;

            if (tokenResponse.IsError)
            {
                this.tokenList.Text = disco.Error;
                return;
            }
            this.tokenList.Text = JsonConvert.SerializeObject(tokenResponse.Json);
            this.txtToken.Text = tokenResponse.AccessToken;
        }

3.3.4 调用API

要将Token发送到API,一般使用HTTP Authorization标头。 这是使用SetBearerToken扩展方法完成。

private void getApi_Click(object sender, EventArgs e)
        {
            //调用认证api
            if (string.IsNullOrEmpty(txtToken.Text))
            {
                MessageBox.Show("token值不能为空");
                return;
            }
            var apiClient = new HttpClient();
            //apiClient.SetBearerToken(tokenResponse.AccessToken);
            apiClient.SetBearerToken(this.txtToken.Text);

            var response = apiClient.GetAsync(this.txtApi.Text).Result;
            if (!response.IsSuccessStatusCode)
            {
                this.resourceList.Text = response.StatusCode.ToString();
            }
            else
            {
                this.resourceList.Text = response.Content.ReadAsStringAsync().Result;
            }

        }

这搭建Client客户端跟上一篇客户端凭证模式有何不一样之处呢?

  1. 客户端请求token多了两个参数,一个用户名,一个密码
  2. 请求Token中使用IdentityModel包的方法RequestPasswordTokenAsync,实现用户密码方式获取令牌。

以上展现的代码有不明白的,能够看本篇项目源码,项目地址为 :资源全部者密码凭证模式

3.4 效果

3.4.1 项目测试

3.4.2 postman测试

4、拓展

从上一篇的客户端凭证模式到这一篇的资源全部者资源密码凭证模式,咱们都已经初步掌握了大体的受权流程,以及项目搭建获取访问受保护的资源,可是咱们也可能发现了,若是是仅仅为了访问保护的API资源的话,加不加用户和密码好像也没什么区别呢。

可是若是仔细对比两种模式在获取token,以及访问api返回的数据能够发现,资源全部者密码凭证模式返回的Claim的数量信息要多一些,可是客户端模式返回的明显少了一些,这是由于客户端不涉及用户信息。因此资源密码凭证模式

能够根据用户信息作具体的资源权限判断。

好比,在TestUser有一个Claims属性,容许自已添加Claim,有一个ClaimTypes枚举列出了能够直接添加的Claim。因此咱们能够为用户设置角色,来判断角色的权限功能,作简单的权限管理。

4.1 添加用户角色

在以前建立的TestUsers.cs文件的User方法中,添加Cliam的角色熟悉,以下:

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),
                       
                        new Claim(JwtClaimTypes.Role,"admin")  //添加角色
                    },

                }
            };
        }
    }
}

4.2 配置API资源须要的Cliam

由于要用到ApiResourcesApiResources的构造函数有一个重载支持传进一个Claim集合,用于容许该Api资源能够携带那些Claim, 因此在项目下的Config类的ApiResources作出以下修改:

public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {

                new ApiResource("api1","api1")
                {
                    Scopes={ "password_scope1" },
                    UserClaims={JwtClaimTypes.Role},  //添加Cliam 角色类型
                    ApiSecrets={new Secret("apipwd".Sha256())}
                }
            };

4.3 添加支持Role验证

在API资源项目中,修改下被保护Api的,使其支持Role验证。

[HttpGet("getUserClaims")]
    //[Authorize]
    [Authorize(Roles ="admin")]
    public IActionResult GetUserClaims()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }

4.4 效果

能够看到,为咱们添加了一个Role Claim,效果以下:

5、总结

  1. 本篇主要阐述以资源全部者密码凭证受权,编写一个客户端,以及受保护的资源,并经过客户端请求IdentityServer上请求获取访问令牌,从而获取受保护的资源。
  2. 这种模式主要使用client_id和client_secret以及用户名密码经过应用Client(客户端)直接获取秘钥,可是存在client可能存了用户密码这不安全性问题,若是client是自家高可信的应用,也是可使用的,同时若是遗留项目升级为oauth2的受权机制也是适配适用的。
  3. 在后续会对其中的其余受权模式,数据库持久化问题,以及如何应用在API资源服务器中和配置在客户端中,会进一步说明。
  4. 若是有不对的或不理解的地方,但愿你们能够多多指正,提出问题,一块儿讨论,不断学习,共同进步。
  5. 项目地址

6、附加

Resource Owner Password Validation资料

Password Grant资料