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

一、前言

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

所以在这一篇中,我们将通过多种授权模式中的资源所有者密码凭证授权模式进行说明,主要针对介绍IdentityServer保护API的资源,资源密码凭证授权访问API资源。

二、初识

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

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

2.1 适用范围

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

这种模式下,应用client可能存了用户密码这不安全性问题,所以才需要高可信的应用。

主要适用于用来做遗留项目升级为oauth2的适配授权使用,当然如果client是自家的应用,也是可以的,同时支持refresh token。

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

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和密钥。

验证用户名密码

示例:客户端身份验证两种方式

1、Authorization: Bearer base64(resourcesServer:123) 

2、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": "刷新令牌"

}

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

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

三、实践

在示例实践中,我们将创建一个授权访问服务,定义一个API和要访问它的客户端,客户端通过IdentityServer上请求访问令牌,并使用它来访问API。

3.1 搭建 Authorization Server 服务

搭建认证授权服务

3.1.1 安装Nuget包

IdentityServer4程序包

3.1.2 配置内容

建立配置内容文件Config.cs

publicstaticclassConfig

{

publicstaticIEnumerable IdentityResources =>

newIdentityResource[]

{

newIdentityResources.OpenId(),

newIdentityResources.Profile(),

};

publicstaticIEnumerable ApiScopes =>

newApiScope[]

{

newApiScope("password_scope1")

};

publicstaticIEnumerable ApiResources =>

newApiResource[]

{

newApiResource("api1","api1")

{

Scopes={"password_scope1"},

ApiSecrets={newSecret("apipwd".Sha256())}//api密钥

}

};

publicstaticIEnumerable Clients =>

newClient[]

{

newClient

{

ClientId ="password_client",

ClientName ="Resource Owner Password",

AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

ClientSecrets = {newSecret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },

AllowedScopes = {"password_scope1"}

},

};

}

因为是资源所有者密码凭证授权的方式,所以我们通过代码的方式来创建几个测试用户。

新建测试用户文件TestUsers.cs

publicclassTestUsers

{

publicstaticList Users

{

get

{

varaddress =new

{

street_address ="One Hacker Way",

locality ="Heidelberg",

postal_code =69118,

country ="Germany"

};

returnnewList

{

newTestUser

{

SubjectId ="1",

Username ="i3yuan",

Password ="123456",

Claims =

{

newClaim(JwtClaimTypes.Name,"i3yuan Smith"),

newClaim(JwtClaimTypes.GivenName,"i3yuan"),

newClaim(JwtClaimTypes.FamilyName,"Smith"),

newClaim(JwtClaimTypes.Email,"i3yuan@email.com"),

newClaim(JwtClaimTypes.EmailVerified,"true", ClaimValueTypes.Boolean),

newClaim(JwtClaimTypes.WebSite,"http://i3yuan.top"),

newClaim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)

}

}

};

}

}

}

返回一个TestUser的集合。

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

3.1.3 注册服务

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

publicvoidConfigureServices(IServiceCollection services)

{

varbuilder = 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方法添加如下代码:

publicvoidConfigure(IApplicationBuilder app, IWebHostEnvironment env)

{

if(env.IsDevelopment())

{

app.UseDeveloperExceptionPage();

}

app.UseRouting();

app.UseIdentityServer();

app.UseEndpoints(endpoints =>

{

endpoints.MapGet("/",asynccontext =>

{

awaitcontext.Response.WriteAsync("Hello World!");

});

});

}

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

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

在Config中配置客户端(client)中定义了一个AllowedGrantTypes的属性,这个属性决定了Client可以被哪种模式被访问,GrantTypes.ClientCredentials客户端凭证模式GrantTypes.ResourceOwnerPassword资源所有者密码凭证授权。所以在本文中我们需要添加一个Client用于支持资源所有者密码凭证授权模式(ResourceOwnerPassword)。

因为资源所有者密码凭证授权需要用到用户名和密码所以要添加用户,而客户端凭证模式不需要,这也是两者的不同之处。

3.2 搭建API资源

实现对API资源进行保护

3.2.1 快速搭建一个API项目

3.2.2 安装Nuget包

IdentityServer4.AccessTokenValidation 包

3.2.3 注册服务

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

publicvoidConfigureServices(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方法添加如下代码:

publicvoidConfigure(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]

publicclassIdentityController:ControllerBase

{

[HttpGet("getUserClaims")]

[Authorize]

publicIActionResultGetUserClaims()

{

returnnewJsonResult(fromcinUser.Claimsselectnew{ c.Type, c.Value });

}

}

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

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

我们可以发现这跟上一篇基本相似,但是可能需要注意的地方应该是ApiName和ApiSecret,要跟你配置的API资源名称和API资源密钥相同。

3.3 搭建Client客户端

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

3.3.1 搭建一个窗体程序

3.3.2 安装Nuget包

IdentityModel

3.3.3 获取令牌

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

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

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

privatevoidgetToken_Click(objectsender, EventArgs e)

{

varclient =newHttpClient();

vardisco = client.GetDiscoveryDocumentAsync(this.txtIdentityServer.Text).Result;

if(disco.IsError)

{

this.tokenList.Text = disco.Error;

return;

}

//请求token

tokenResponse = client.RequestPasswordTokenAsync(newPasswordTokenRequest

{

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扩展方法完成。

privatevoidgetApi_Click(objectsender, EventArgs e)

{

//调用认证api

if(string.IsNullOrEmpty(txtToken.Text))

{

MessageBox.Show("token值不能为空");

return;

}

varapiClient =newHttpClient();

//apiClient.SetBearerToken(tokenResponse.AccessToken);

apiClient.SetBearerToken(this.txtToken.Text);

varresponse = apiClient.GetAsync(this.txtApi.Text).Result;

if(!response.IsSuccessStatusCode)

{

this.resourceList.Text = response.StatusCode.ToString();

}

else

{

this.resourceList.Text = response.Content.ReadAsStringAsync().Result;

}

}

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

客户端请求token多了两个参数,一个用户名,一个密码

请求Token中使用IdentityModel包的方法RequestPasswordTokenAsync,实现用户密码方式获取令牌。

以上展示的代码有不明白的,可以看本篇项目源码,项目地址为 :资源所有者密码凭证模式

https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/ResourceOwnerPasswords

3.4 效果

3.4.1 项目测试

3.4.2 postman测试

四、拓展

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

但是如果仔细对比两种模式在获取token,以及访问api返回的数据可以发现,资源所有者密码凭证模式返回的Claim的数量信息要多一些,但是客户端模式返回的明显少了一些,这是因为客户端不涉及用户信息。所以资源密码凭证模式可以根据用户信息做具体的资源权限判断。

比如,在TestUser有一个Claims属性,允许自已添加Claim,有一个ClaimTypes枚举列出了可以直接添加的Claim。所以我们可以为用户设置角色,来判断角色的权限功能,做简单的权限管理。

4.1 添加用户角色

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

publicclassTestUsers

{

publicstaticList Users

{

get

{

varaddress =new

{

street_address ="One Hacker Way",

locality ="Heidelberg",

postal_code =69118,

country ="Germany"

};

returnnewList

{

newTestUser

{

SubjectId ="1",

Username ="i3yuan",

Password ="123456",

Claims =

{

newClaim(JwtClaimTypes.Name,"i3yuan Smith"),

newClaim(JwtClaimTypes.GivenName,"i3yuan"),

newClaim(JwtClaimTypes.FamilyName,"Smith"),

newClaim(JwtClaimTypes.Email,"i3yuan@email.com"),

newClaim(JwtClaimTypes.EmailVerified,"true", ClaimValueTypes.Boolean),

newClaim(JwtClaimTypes.WebSite,"http://i3yuan.top"),

newClaim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json),

newClaim(JwtClaimTypes.Role,"admin")//添加角色

},

}

};

}

}

}

4.2 配置API资源需要的Cliam

因为要用到ApiResources,ApiResources的构造函数有一个重载支持传进一个Claim集合,用于允许该Api资源可以携带那些Claim, 所以在项目下的Config类的ApiResources做出如下修改:

publicstaticIEnumerable ApiResources =>

newApiResource[]

{

newApiResource("api1","api1")

{

Scopes={"password_scope1"},

UserClaims={JwtClaimTypes.Role},//添加Cliam 角色类型

ApiSecrets={newSecret("apipwd".Sha256())}

}

};

4.3 添加支持Role验证

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

[HttpGet("getUserClaims")]

//[Authorize]

[Authorize(Roles ="admin")]

publicIActionResultGetUserClaims()

{

returnnewJsonResult(fromcinUser.Claimsselectnew{ c.Type, c.Value });

}

4.4 效果

可以看到,为我们添加了一个Role Claim,效果如下:

五、总结

本篇主要阐述以资源所有者密码凭证授权,编写一个客户端,以及受保护的资源,并通过客户端请求IdentityServer上请求获取访问令牌,从而获取受保护的资源。

这种模式主要使用client_id和client_secret以及用户名密码通过应用Client(客户端)直接获取秘钥,但是存在client可能存了用户密码这不安全性问题,如果client是自家高可信的应用,也是可以使用的,同时如果遗留项目升级为oauth2的授权机制也是适配适用的。

在后续会对其中的其他授权模式,数据库持久化问题,以及如何应用在API资源服务器中和配置在客户端中,会进一步说明。

如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。

项目地址

https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/ResourceOwnerPasswords

六、附加

Resource Owner Password Validation资料

Password Grant资料


转载https://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA==&mid=2654081089&idx=6&sn=e3a6cf965c64be17cf3c767f910fbac9&chksm=80d83414b7afbd021ccaf37311c644629da2a28b7668ffce1cea513fe349e19a304dfe81dfb9&mpshare=1&scene=1&srcid=1104Hlu6tVOeQMOUmVdw6zCg&sharer_sharetime=1604451653242&sharer_shareid=108986146b27c51e2241dd818f15e738&key=276dab6679cad080ab7f770988646b8dbbd5830b4b8b21001082a386ee30626cec4a869ba981fdd302b2d3ff89de0ddd28e09d61bb3c6964aa303b9223bb43cde7278ef3fdf03c13b2577def855c6e1617aa21b1558e566ab5c4768ac0fe7927199b994337386ab802bc132f250f65d2874717cfa9b966e7d125f9a13a08f05a&ascene=1&uin=MzEzNzU3NzUzMg%3D%3D&devicetype=Windows+10+x64&version=62090070&lang=zh_CN&exportkey=A2ZTvW8%2FaPNmolQtC3Y16hQ%3D&pass_ticket=GvzCGWc%2F2eN%2BkhDeSxEwXm7OTpUGu86T9h9H%2B%2BWQ%2Ftyyl3wcfqrg7TPkoKO42qxs&wx_header=0

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342