前端页面在运行过程中出现ElementPlusError错误,报[ElOnlyChild] no valid child node found错误消息

本文记录一个前端页面开发出现的问题,涉及的前端框架版本为,
- "vue": "^3.2.22"
- "element-plus": "^2.0.4"

1. 问题

近期在使用vue 3 + element plus进行前端页面开发,在前端页面运行过程浏览器后台中出现如下错误消息,

error.ts:12 ElementPlusError: [ElOnlyChild] no valid child node found
    at debugWarn (error.ts:12:18)
    at Proxy.<anonymous> (only-child.ts:35:9)
    at renderComponentRoot (runtime-core.esm-bundler.js:893:44)
    at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5098:34)
    at ReactiveEffect.run (reactivity.esm-bundler.js:167:25)
    at updateComponent (runtime-core.esm-bundler.js:4968:26)
    at processComponent (runtime-core.esm-bundler.js:4901:13)
    at patch (runtime-core.esm-bundler.js:4489:21)
    at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5107:17)
    at ReactiveEffect.run (reactivity.esm-bundler.js:167:25)

出现这个错误消息概率比较高,但不是每次都必现。页面上各个组件按钮都运行正常,出现这个问题感觉有些纳闷,上面的错误消息也没有直接给出哪里不对,一时不知道问题在哪里。

2. 定位

从错误消息[ElOnlyChild] no valid child node found来看,报的是子组件无效,估计是什么组件使用的不当,没有映射到相应的模型数据,是不是哪里变量命名有拼写错误,使用Ctrl+F检查了一番代码,没有发现任何变量拼写错误。

为了发现问题,开始对页面组件进行二分法排查,把页面组件逐一过滤,定位问题出现在如下一个气泡确认框组件上,

<el-popconfirm title="确定要删除吗?" @confirm="deleteConfig(row)">
   <template #reference>
       <el-button type="text" v-if="row.version != 'new'">删除</el-button>
   </template>
</el-popconfirm>

这个组件没有什么特别,之前已经在很多地方用的很好,唯一不同的是添加了v-if控制,稍微判断了一下,应该就是这个v-if导致。

若把上面的el-button去掉,则直接可以复现这个问题,

<el-popconfirm title="确定要删除吗?" @confirm="deleteConfig(row)">
   <template #reference>
   </template>
</el-popconfirm>

气泡确认框组件的用途,主要是在点击某个元素弹出一个简单的气泡确认框,其需要绑定在一个子组件上,若子组件为空,则会报出如下的错误信息,

[ElOnlyChild] no valid child node found...

在了解到如上的问题后,将v-if移到el-popconfirm组件,正确的书写方法为,

<el-popconfirm title="确定要删除吗?" v-if="row.version != 'new'" @confirm="deleteConfig(row)">
   <template #reference>
       <el-button type="text">删除</el-button>
   </template>
</el-popconfirm>

至此问题解决。

OAuth2授权协议简介

图片来自pixabay.com的tassilo111-80733会员
[![图片来自pixabay.com的tassilo111-80733会员](http://www.hyhblog.cn/wp-content/uploads/2018/04/climber-226804_640.jpg "图片来自pixabay.com的tassilo111-80733会员")](https://pixabay.com/photo-226804/ "图片来自pixabay.com的tassilo111-80733会员")

OAuth 2.0是一个业界标准的授权协议,其定义了四种可以适用于各种应用场景的授权交互模式:授权码模式、应用授信模式、用户授信模式、简化模式。其中,授权码模式被广泛应用于第三方互联网开放平台,通过第三方登录是其最常见应用场景之一,比如使用微信、QQ和淘宝账号进行登录。

本文对OAuth2.0授权协议进行介绍和梳理,从一个简单的授权场景讲起,以了解OAuth 2.0所要解决的授权问题,然后对OAuth 2.0的四种授权交互模式进行一一介绍,最后对各个授权模式的相互关系、区别、以及如何选择进行讲解。

1. 认识OAuth 2.0

OAuth 2.0是一个业界标准的授权协议(authorization protocol),这里的授权是以委派代理(delegation)的方式。可以这样理解,OAuth 2.0提供一种协议交互框架,让某个应用能够以安全地方式获取到用户的委派书,这个委派书在OAuth 2.0中就是访问令牌(access token),随后应用便可以使用该委派书,代表用户来访问用户的相关资源。

在OAuth 2.0的协议交互中,有四个角色的定义,

  • 资源所有者(Resource Owner):顾名思义,资源的所有者,很多时候其就是我们普通的自然人(但不限于自然人,如某些应用程序也会创建资源),拥有资源的所有权。
  • 资源服务器(Resource Server):保存着受保护的用户资源。
  • 应用程序(Client):准备访问用户资源的应用程序,其可能是一个web应用,或是一个后端web服务应用,或是一个移动端应用,也或是一个桌面可执行程序。
  • 授权服务器(Authorization Server):授权服务器,在获取用户的同意授权后,颁发访问令牌给应用程序,以便其获取用户资源。

2. 从一个简单的应用场景谈起

为了方便讨论,我们假设有一个用户Michael,他在一个资源服务器上保存着他自己的账号信息,例如微信账号的姓名、头像等。某个应用程序在用户登录时,需要获取Michael的这些账号信息。

Michael在资源服务器上保存的账号信息是受保护的,为了让应用程序能够获取Michael的账号信息,需要提供用户的访问密码。有一个简单的方法是,在应用服务器和资源服务器之间共享同一访问密码,当Michael登录输入密码后,应用程序复制Michael的登录密码并向资源服务器请求访问,获取Michael的账号信息。这是早些时候比较常见的跨应用授权访问方法。

这样子做有很大的安全隐患,主要有如下三个方面的问题,

  1. 用户在应用程序和资源服务器需要保持一致的密码
  2. 无法控制应用程序的权限,应用程序需要的是读权限,但是拿到用户密码后,获取到的却是用户的所有访问权限
  3. 用户的密码会被应用程序获取到,有用户密码泄露的风险,一旦应用程序多了,安全风险不可控

在简单的应用场景里,在应用程序和资源服务器之间保持一致的密码是可行的,这也确实能够带来一定的便利,至少用户不用记多套用户名和密码,但账号和密码的独立性无法得到保证,应用程序可以直接接触到用户密码等敏感信息,账号的安全性也无法控制。

为了解决第1个问题,用户Michael在应用程序和资源服务器可以使用不同的密码登录,有一个可行的方法是,让用户输入两次密码,第一次输入密码为了登录应用程序,第二次让Michael输入其在资源服务器的登录密码,以便应用程序获取资源服务器的账号信息。

这样就需要用户输入两次密码,给用户的使用带来很大的不便,而且这个方案依然存在第2和第3的问题。

为了解决第2个问题,限制应用程序访问资源服务器的权限,我们可以让用户在资源服务器申请一个只读的受限密码,该受限密码只用来读取用户信息,无法用来进行编辑和删除操作,用户输入这个只读密码给应用程序,让应用程序读取用户在资源服务器上的信息。

该方法解决第2个问题,但再次增加了用户的不便,使得用户不得不面对复杂的密码管理,用户不得不在资源服务器上维护两个密码,一个正常使用的密码和一个只读的受限密码,并且该方法依然没有解决第3个问题。

为了解决第3个问题,让应用程序不再接触用户在资源服务器的密码,一个可行的办法是,资源服务器直接颁发给应用程序一个读权限的key,应用程序拿着这个key去读取资源服务器的所有用户的信息。

这个方法解决了上述提到的第3个问题,但是这个key是一个通用的读权限,权限范围很大,其和用户没有任何关联。在很多时候,我们还是需要用户级别上的受限权限控制。

能否有一个方案,在不影响用户的使用便利性,并且颁发一个在用户级别上的可控权限key?可以考虑的是,用户动态按需地向资源服务器申请读权限key,然后颁发给应用程序,用于应用程序去申请访问用户的信息,该受限密码在颁发后有一定的时效性,甚至可以指定其只能被使用一次。这样子的话,解决问题1、2和3的条件都得到满足。

这个方案已经很接近于OAuth 2.0在设计之初所提供的授权方案,不一样之处的是,受限密码的颁发交给了独立的安全组件:授权服务器。

这里马上就要介绍OAuth 2.0的基本授权方式:授权码模式。

3. OAuth 2.0基本授权流程:授权码模式

让我们看看在增加授权服务器之后,OAuth 2.0的一个基本授权流程,

如图所示,授权流程场景可以描述为如下几个步骤,

  1. 用户在应用程序中,应用程序尝试获取用户保存在资源服务器上的信息,比如用户的身份信息和头像,应用程序首先让重定向用户到授权服务器,告知申请资源的读权限,并提供自己的client id。
  2. 到授权服务器,用户输入用户名和密码,服务器对其认证成功后,提示用户即将要颁发一个读权限给应用程序,在用户确认后,授权服务器颁发一个授权码(authorization code)并重定向用户回到应用程序。
  3. 应用程序获取到授权码之后,使用这个授权码和自己的client id/secret向认证服务器申请访问令牌/刷新令牌(access token/refresh token)。授权服务器对这些信息进行校验,如果一切OK,则颁发给应用程序访问令牌/刷新令牌。
  4. 应用程序在拿到访问令牌之后,向资源服务器申请用户的资源信息
  5. 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起OK,则返回应用程序所需要的资源信息。

这个授权流程在OAuth 2中被称为授权码模式(authorization code grant),其命名的原因是,应用程序使用授权码来向授权服务器申请访问令牌/刷新令牌。

可以看到,在整个过程中应用程序没有接触到用户的密码。

授权码和令牌都是一个唯一标识的值,其各个意义为,

  • 授权码:即用户的委派书,代表着用户的受限权限,有时效性
  • 访问令牌:用于应用程序每次向资源服务器访问时提供,有时效性,如果安全性比较高的话,则每个访问令牌可以被设置为只用一次,或者对令牌设置一个有效期,在有效期可以反复使用。
  • 刷新令牌:用于应用程序向授权服务器申请新的访问令牌,在访问令牌失效或过期的时候,重新获取新的访问令牌。

注意的是,访问令牌对于应用程序来说是透明的,应用程序无需关注访问令牌所带的任何信息,只需在访问资源服务器时带上它。但是资源服务器需要知道访问令牌的组成和加密方式,资源服务器需要解析或解密这个访问令牌,查看并校验里面的信息。

授权服务器和访问令牌,前者为授权的颁发,后者为授权的载体,两者实现了动态按需地代理权限分发,这也是OAuth 2.0解决方案在授权上所带来的创新变化。

4. OAuth 2.0授权服务的EndPoint交互

若要了解OAuth 2.0的整个授权交互,则需要对授权服务器所提供的四个重要授权Endpoint进行了解,

  • /authorize:主要用于颁发授权码,在特殊场景下也可直接颁发访问令牌(这里的特殊场景是指简化模式,详情见后面讨论)
  • /token:用于颁发访问令牌和刷新令牌
  • /introspect:检查令牌的合法性
  • /revoke:吊销令牌

在基本授权码模式下,授权服务器上发生的EndPoint交互流程如下,

| 授权服务器
EndPoint | 授权码模式 | 请求结果 |
| :---: | :----------- | :---: |
| /authorize | GET /authorize?response_type=code&scope=read&client_id=id&redirect_uri&state=xxx HTTP/1.1
Host: localhost | 授权码 |
| /token | POST /token HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=code&redirect_uri=uri | 访问令牌/刷新令牌 |
| /introspect | POST /introspect HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

token=123 | 检查令牌 |
| /revoke | POST /revoke HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Authorization: Basic (client id+secret)

token=123 | 吊销令牌 |

授权服务器
EndPoint
授权码模式 请求结果
/authorize GET /authorize?response_type=code&scope=read&client_id=id&redirect_uri&state=xxx HTTP/1.1
Host: localhost
授权码
/token POST /token HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=code&redirect_uri=uri

访问令牌/刷新令牌
/introspect POST /introspect HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

token=123

检查令牌
/revoke POST /revoke HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Authorization: Basic (client id+secret)

token=123

吊销令牌

5. OAuth 2.0其它授权模式及应用场景

上面讲的OAuth 2.0基本授权方式适用于资源所有者和应用程序两个角色都存在的场景下,在现实世界中,往往资源所有者和应用程序两个角色有缺失或者合二为一了,OAuth 2针对实际的应用场景中提供了不同的授权方式。

5.1 Implicit Grant:简化模式

有一种场景需求是这样子的,应用程序运行在用户端,换句话说在网络拓扑上应用程序这个和资源所有者两个角色是合在了一起,比如浏览器JS应用程序(In-Browser JavaScript App)就是这样子的应用场景。

如下图所示,

应用程序运行在客户端,一个最大的变化就是其变成了公开应用程序(Public Client),应用程序的运行完全暴露在用户的控制之中。在这种场景下,应用程序是无法隐藏自己的一些敏感数据,比如client secret和授权码,在这个方式下,再向授权服务器获取授权码是多此一举。为此OAuth 2.0提供简化模式,授权服务器在校验好用户信息后,直接颁发给应用程序访问资源服务器的访问令牌。换句话说,应用程序在获取访问令牌时无需提供授权码和client secret。

这个授权模式被称为简化模式,其命名的原因主要是由于跳过获取授权码这一中间过程,无需授权码而可以直接获取访问令牌。

整个授权流程如下,

  1. 用户在应用程序中,应用程序尝试获取用户保存在资源服务器上的信息,比如用户的身份信息和头像,应用程序首先让用户重定向到授权服务器,告知申请资源的读权限,并提供自己的client id。在重定向的过程中,应用程序指定使用Implicit Grant授权方式。
  2. 在授权服务器,用户输入用户名和密码,服务器对其认证成功后,提示用户即将要颁发一个读权限给应用程序,在用户确认后,授权服务器直接颁发一个访问令牌并重定向用户回到应用程序。
  3. 应用程序在拿到访问令牌之后,向资源服务器申请用户的资源信息
  4. 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起OK,则返回应用程序所需要的资源信息。

可以看到在简化模式和标准的授权码模式相比,其流程交互简化了很多,但安全性也是随之降低,在使用简化模式时,需要考虑安全性降低带来的应用使用问题,因此,应该尽量在这种模式下提供对安全度要求不高的操作,例如只读操作。

另外需要注意的是,应用程序从授权服务器获取到了访问令牌,但不需要获取刷新令牌,这是由于在这种使用场景下,用户应该是一直在场,一旦访问令牌过期,只要让用户重新登录一下即可,无需使用刷新令牌来定期更新访问令牌。另外对于浏览器JS应用程序,整个会话(session)一般也是短期,不用考虑长时间下使用时令牌刷新机制。

我们看下在简化模式下,授权服务器上EndPoint发生的交互流程,

| 授权服务器
EndPoint | 简化模式 | 请求结果 |
| :---: | :----------- | :---: |
| /authorize | GET /authorize?response_type=token&scope=read&client_id=xyz&redirect_uri=uri&state=guid HTTP/1.1
Host: localhost | 访问令牌 |
| /token | 无 | 无 |
| /introspect | 见授权码模式 | 检查令牌 |
| /revoke | 见授权码模式 | 吊销令牌 |
授权服务器
EndPoint
简化模式 请求结果
/authorize GET /authorize?response_type=token&scope=read&client_id=xyz&redirect_uri=uri&state=guid HTTP/1.1
Host: localhost
访问令牌
/token
/introspect 见授权码模式 检查令牌
/revoke 见授权码模式 吊销令牌

5.2 Client Credentials Grant:应用授信模式

应用授信模式主要解决的是在如下两种情况下,

  • 资源所有者角色不参与授权交互
  • 应用程序角色本身就是资源所有者

如何获取访问令牌的问题。

如下图所示,

整个流程如下,

  1. 应用程序尝试获取在资源服务器上的信息,应用程序直接向授权服务器申请访问令牌,告知申请资源的读权限,并提供自己的授信凭证(client id/secret)。在申请请求中,应用程序指定使用client credentials授权方式。在授权服务器,服务器对其client credentials校验成功后,授权服务器直接颁发一个访问令牌给应用程序。
  2. 应用程序在拿到访问令牌之后,向资源服务器申请用户的资源信息
  3. 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起OK,则返回应用程序所需要的资源信息。

这个授权流程被称为应用授信模式,其命名原因是由于应用程序是通过自己的授信凭证(client id/secret)直接向授权服务器申请访问令牌。这种模式一般用在可信的应用程序。

和简化模式一样,授权服务器无需颁发刷新令牌给应用程序,原因很简单,应用程序想要的话,直接再调用授权服务器一次即可。

我们看下在应用授信模式下,授权服务器上EndPoint发生的交互流程,

| 授权服务器
EndPoint | 应用授信模式 | 请求结果 |
| :---: | :----------- | :---: |
| /authorize | 无调用 | |
| /token | POST /token HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=read | 访问令牌 |
| /introspect | 见授权码模式 | 检查令牌 |
| /revoke | 见授权码模式 | 吊销令牌 |

授权服务器
EndPoint
应用授信模式 请求结果
/authorize 无调用
/token POST /token HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=read

访问令牌
/introspect 见授权码模式 检查令牌
/revoke 见授权码模式 吊销令牌

5.3 Resource Owner Credentials Grant:用户授信模式

在基本的授权码模式中,用户需要跳转到授权服务器上,使用用户名和密码登录后拿到授权码,然后把授权码交给应用程序,然后再去申请访问令牌。但有些时候,能否省去这个来回的跳转过程,把用户名和密码直接交给应用程序,让应用程序去申请访问令牌。

这个就是用户授信模式的应用场景,这个场景其实回到了本文中最先提到的授权使用场景,应用程序有接触到用户的用户名和密码,因此,应用程序必须是完全可信的。

整个流程如下,

  1. 用户在应用程序中,应用程序首先让用户到登录页面输入用户名和密码。
  2. 应用程序拿到资源所有者的用户名和密码,加上自己的client id/secret一同向认证服务器申请访问令牌/刷新令牌。授权服务器对这些信息进行校验,如果一切OK,则颁发给应用程序访问令牌/刷新令牌。
  3. 应用程序在拿到访问令牌/刷新令牌之后,向资源服务器申请用户的资源信息。
  4. 资源服务器在获取到访问令牌后,对令牌进行解析(如果令牌已加密,则需要进行使用相应算法进行解密)并校验,并向授权服务器校验其合法性,如果一起OK,则返回应用程序所需要的资源信息。

这个授权流程被称为用户授信模式,其命名原因是由于应用程序是通过用户的授信凭证(比如:用户名和密码)向授权服务器申请访问令牌。

当应用程序换取到访问令牌之后,从安全的角度考虑,应用程序应该立即删除用户的授信凭证,不再保留。这也是OAuth 2.0所建议的安全规范,应用程序不应该通过用户的用户名和密码,而是应该都通过访问令牌去访问资源。

我们看下在用户授信模式下,授权服务器上EndPoint发生的交互流程,

| 授权服务器
EndPoint | 用户授信模式 | 请求结果 |
| :---: | :----------- | :---: |
| /authorize | 无调用 | |
| /token | POST /token HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

grant_type=password &scope=read&username=Michael&password=secret | 获取到访问令牌和刷新令牌 |
| /introspect | 见授权码模式 | 检查令牌 |
| /revoke | 见授权码模式 | 吊销令牌 |

授权服务器
EndPoint
用户授信模式 请求结果
/authorize 无调用
/token POST /token HTTP/1.1
Host: localhost
Authorization: Basic (client id+secret)
Content-Type: application/x-www-form-urlencoded

grant_type=password &scope=read&username=Michael&password=secret

获取到访问令牌和刷新令牌
/introspect 见授权码模式 检查令牌
/revoke 见授权码模式 吊销令牌

6. 四种授权模式的联系和区别

OAuth 2.0的四种授权模式,有一定的联系,也有区别。前文也对这些联系和区别做了一些描述,这里做下小结。

无论哪种授权模式,都是以获取访问令牌为目的,访问令牌是各个授权模式交互的最终结果。

我们先比较下各个模式获取访问令牌的手段,

  • 授权码模式:授权码+应用的授信凭据
  • 简化模式:应用client id + 用户的授信凭据
  • 应用授信模式:应用的授信凭据
  • 用户授信模式:应用的授信凭据+用户的授信凭据

上面说的,应用的授信凭据是指client id和secret,用户的授信凭据则一般是用户名和密码。

这四种授权模式中,授权码模式是基本的授权模式,

  1. 授权码模式:基本授权模式,它需要有四个角色同时在场才能完成授权:资源所有者、应用程序、授权服务器、资源服务器。

其它三种模式可以在其基本的授权码模式上演绎出来,

  1. 简化模式:开放应用程序,应用程序运行在公开开放的环境。即:无需应用程序的认证。
  2. 应用授信模式:应用程序即为资源所有者,或资源所有者不参与授权交互。即:无资源所有者的认证。
  3. 用户授信模式:无授权码的颁发过程,直接通过用户名和密码换取授权。

下图给出了演绎过程,

下表列出四种授权模式在授权服务器上/authorize和/token两个Endpoint的交互结果,

授权服务器
EndPoint
授权码
模式
简化
模式
应用授信
模式
用户授信
模式
/authorize 授权码 访问令牌
/token 访问令牌+刷新令牌 访问令牌 访问令牌+刷新令牌
最终结果 访问令牌+刷新令牌 访问令牌 访问令牌 访问令牌+刷新令牌

表中无的含义是指无交互。各个模式和Endpoint的更详细交互信息参见上文。

7. 四种授权模式的时序图

下图为四种授权模式在各个角色上的流转时序图,主要包括token的颁发和使用流程。

更清晰的时序图,见点击这里查看。

8. 如何选择合适OAuth 2.0授权方式

面对四种授权方式,在了解OAuth 2.0四个角色和流程后,接下来就是为自己的应用场景选择一种授权模式。

简单的话,可以根据下面的流程图来进行参考选择,

原图自Alex Bilbie的A Guid To OAuth 2.0 Grants

图中各个概念的定义说明如下,

  1. 资源所有者
    • 自然人用户:资源所有者是我们普通的自然人。
    • 应用程序:是指当资源的所有者不是我们普通的自然人,而是应用程序,也就是机器。
  2. 应用程序类型
    • Web应用:运行在后端web服务器上的应用
    • 基于user-agent的应用:运行在浏览器JS应用
    • 原生应用:桌面应用,移动应用(安卓和iOS应用)
  3. 第一方和第三方应用
    • 第一方应用:指可信赖的应用,其可以接触用户的密码等敏感信息,应用安全可靠。
    • 第三方应用:指由第三方开发的应用,其安全性不受自己控制,需将用户的密码等敏感信息和第三方应用隔离开,颁发令牌给第三方应用,第三方应用凭借令牌访问资源。

9. OAuth 2的安全威胁和考虑

在使用OAuth 2之前,有必要了解下OAuth在安全性方面的考虑和注意点,比如在授权endpoints上需配置HTTPS,比如为了防止client受到CSRF攻击,可以在重定向时添加一个state标记位,授权服务器在跳转回到client时会带回这个state,由client校验该标记位,确认为当前请求为前一个跳转的合法回调,以避免被非法者冒用,等等。

可以通过阅读文档RFC6749和RFC6819来获取更多详细资料,

  1. RFC6749:The OAuth 2.0 Authorization Framework中第10章节对Security Considerations的描述
  2. RFC6819:OAuth 2.0 Threat Model and Security Considerations

OAuth 2.0的更多RFC文档列举如下,以便扩展阅读,

  1. RFC 6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage
  2. RFC 7009 - OAuth 2.0 Token Revocation
  3. RFC 7521 - Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants
  4. RFC 7522 - Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants
  5. RFC 7523 - JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
  6. RFC 7591 - OAuth 2.0 Dynamic Client Registration Protocol
  7. RFC 7592 - OAuth 2.0 Dynamic Client Registration Management Protocol
  8. RFC 7628 - A Set of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth
  9. RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients
  10. RFC 7662 - OAuth 2.0 Token Introspection

10. 文中讨论的术语(中英文对照)

  • 授权协议(authorization protocol)
  • 委派代理(delegation)
  • 资源所有者(resource owner)
  • 资源服务器(resource server)
  • 应用程序(client)
  • 授权服务器(authorization server)
  • 访问令牌(access token)
  • 刷新令牌(refresh token)
  • 授权码(authorization code)
  • 开放应用程序(public client)
  • 私有应用程序(private client)
  • 授权码模式(authorization code grant)
  • 简单模式(implicit grant)
  • 应用授信模式(client credentials grant)
  • 用户授信模式(resource owner credentials grant)
  • 浏览器JS应用(in-browser JavaScript app)
  • 会话(session)

11. 参考资料

  1. 《OAuth 2 in Action》,作者Justin Richer,Antonio Sanso
  2. RFC6749:The OAuth 2.0 Authorization Framework
  3. Alex Bilbie的博文:A Guide To OAuth 2.0 Grants

单点登录的通用架构实现

图片来自pixabay.com的skeeze-272447会员

在互联网应用开发中,应用服务的用户登录和权限控制是至关重要的一个环节,直接影响到应用的安全运营,也影响到用户的隐私保护,因此用户登录和权限控制是互联网应用开发的必要功能组件, 其涉及到认证、授权、鉴权和权限控制四个功能环节。本文将梳理互联网应用开发中的单点登录(single-sign-on)技术架构实现,单点用户登录也被称为统一用户登录。

  • 注1:本文讨论的互联网应用开发,主要是指web应用和移动应用开发两个技术领域。
  • 注2:本文所讨论的认证、授权、鉴权和权限控制的概念定义来自其这篇文章。因此,在阅读本文之前,建议先请阅读前篇文章。

1. 什么是单点登录

单点登录不仅仅是实现用户登录这单一功能,它更是一个技术架构,一个解决方案,能够提供统一的认证、授权、鉴权和权限控制,提供相应的组件,方便用户的使用,以及应用的开发快速接入。所以,单点登录不是一个简单的登录模块。

单点登录,从字面来说,是让用户使用同一账号可以访问不同应用服务,而且用户只需登录一次(输入一次用户名和密码)便可实现多次授权。单点登录能够很大程度上方便用户的登录操作。这是从用户的角度出发来看单点登录。

单点登录还需要考虑的另外一个重要需求,是方便应用的对接使用,单点登录架构必须提供相应的客户端组件和配置,使得应用可以简单、方便、快速地接入单点登录功能,同时实现安全的权限控制。

因此,单点登录作为一个安全技术架构,其主要包括用户登录和权限控制两大功能需求,前者涉及认证和授权环节,后者涉及鉴权和权限控制环节。

2. 用户登录

用户登录的过程一般会涉及到用户认证和授权两个环节,这两个环节往往一并发生,即在确认用户身份的同时,也完成用户的授权。

根据登录功能模块的提供方,可以将登录划分为第一方登录和第三方登录两种类型,

  • 第一方登录:应用的登录服务由自己提供。由于是己方登录和己方应用,所以登录服务和应用服务之间相互可信,这大大方便了授权过程。
  • 第三方登录:应用的登录服务由第三方提供,使用第三方的用户账号登录,比如通过微信、QQ、支付宝账号进行登录等等。目前各大互联网开放平台大多采用业界标准的OAuth 2.0授权码模式实现第三方登录。

第一方登录和第三方登录,在用户认证技术的实现上没有太大差别,但是授权流程有区别。具体的授权流程和区别在下文的架构实现中讨论。

2.1 认证方式

在互联网开发中,常见的认证方式有,

  • 用户名和密码
  • 手机短信验证码
  • 手机应用二维码扫描
  • 用户手势
  • 基于时间序列和用户相关的一次性口令

本文为了方便讨论,将使用简单的http基本认证(http basic authentication)方式进行用户登录,即用户名和密码的方式。更多的认证方式,一般可以通过identity provider的扩展接口来实现。

2.2 授权方式

一旦用户登录成功,即可获取到相应的授权信息。

在互联网应用开发领域,授权的实现技术主要包括如下几种,

  • 通过web服务器的session机制,一个访问会话保持着用户的授权信息
  • 通过web浏览器的cookie机制,一个网站的cookie保持着用户的授权信息
  • 颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息

前面两者常见于web开发,需要有浏览器的支持。而对于移动应用及其其它无法使用cookie的场景中,大多可以采用token的实现并通过header方式携带该授权信息在请求中。

本文为了同时支持web应用和移动应用的授权,授权信息将以授权令牌(token)形式颁发,对于web应用,该授权令牌通过cookie携带于请求中,而对于移动应用,则通过header方式携带于请求中。

2.3 第一方登录架构实现

由于是己方登录实现,所以登录服务和应用服务之间相互可信,这大大方便了授权过程。

在第一方登录的场景下,一般可以通过两种方式获得授权,

  • 若应用通过浏览器访问,则可以通过同域cookie方式实现共享授权
  • 若应用不是浏览器访问,或者无法通过cookie技术,则可以直接通过用户名和密码来换取授权。需要注意的是,应用服务可以拿到用户名和密码等敏感信息,这是基于可信应用的先决条件。

代码实现(待开发)

一个简单的架构实现如下,

图中亮红色线和方格为单点登录架构所需实现的组件和功能,其主要包括,

  • 单点登录模块:提供用户登录界面,实现跳转返回,授权令牌(token)的颁发、校验和注销
  • Web filter:获取请求中的授权令牌并校验,若合法则通过请求,否则返回错误消息。

对于web应用,通过共享cookie的方式获取授权,整个流程交互如下,

  1. 用户通过浏览器访问web服务
  2. 后端服务中一个通用的web filter校验请求,若发现请求中没有授权令牌,则跳转至用户登录模块,让用户进行登录
  3. 用户输入用户名和密码,若登录成功,则将授权令牌存储在浏览器的cookie中
  4. 将用户跳转回用户的访问页面,此时请求中cookie将自动带上授权令牌
  5. 后端服务的web filter校验请求cookie中的授权令牌,若合法且有效,则用户可以正常访问后端服务,否则返回错误消息,提示用户再次登录。
  6. 用户注销登录时,web应用服务将存储在浏览器cookie中的授权令牌清除,并注销redis中的令牌。

对于移动应用,通过用户名和密码的方式获取授权,整个流程交互如下,

  1. 用户打开移动应用,应用提示用户登录
  2. 用户输入用户名和密码,若登录成功,则将授权令牌返回给移动应用,存储在手机上的应用存储空间。
  3. 进入应用后,移动应用访问后端服务,授权令牌携带在请求header中
  4. 后端服务中一个通用的web filter对请求header中的授权令牌进行校验,若合法且有效,则用户可以正常访问后端服务,否则提示用户登录。
  5. 用户注销登录时,移动应用将授权令牌清除,并注销redis中的令牌。

由于移动应用无法像web应用一样,通过浏览器cookie的方式自动携带授权信息,移动应用需要自己处理授权信息的存储和请求发送。

2.4 第三方登录架构实现

相比于第一方登录,第三方登录最大的好处在于,可以让用户使用一个常用的第三方账号直接登录,例如微信、QQ、淘宝账号等,省去了用户注册账号的步骤,也免去用户记忆各种应用账号密码的烦恼,方便了用户的快速使用,进而降低用户接入成本。

对于第三方登录,目前各大互联网开放平台大多采用业界标准的OAuth 2.0授权码模式。对于OAuth 2.0的授权码模式和更多详细介绍将有另外一篇文章讨论,这里对其不扩展。

代码实现(待开发)

一个简单的架构实现如下,

图中亮红色线和方格为单点登录架构所需实现的组件和功能,其包括,

  • OAuth 2.0授权服务器:提供用户登录授权,实现跳转返回,授权令牌(token)的颁发、校验和注销
  • Token接口:用于客户端调用,通过授权码换取授权令牌
  • Web filter:获取请求中的授权令牌并校验,若合法则通过请求,否则返回错误消息。

图中有两种应用类型,web应用和移动应用,两种获取授权方式基本一样。

对于web应用,整个流程交互如下,

  1. 用户通过浏览器访问Web服务
  2. 后端服务中一个通用的web filter校验请求,若发现请求中没有授权令牌,则跳转至第三方用户登录服务
  3. 在第三方登录服务中,用户输入用户名和密码,若登录成功,则跳转回web应用,并返回授权码
  4. Web应用将获取到的授权码,调用后端Token接口,该接口将根据授权码+应用ID+应用Secret,向第三方申请授权令牌,若一切正常,第三方颁发授权令牌给Web应用,Web应用将获取到的授权令牌存储在浏览器的cookie中。
  5. 将用户跳转回用户的初始访问页面,此时请求中cookie将自动带上授权令牌
  6. 后端服务的web filter校验请求cookie中的授权令牌,若合法且有效,则用户可以正常访问后端服务,否则返回错误消息,提示用户再次登录。
  7. 用户注销登录时,web应用服务将存储在浏览器cookie中的授权令牌清除,并注销redis中的令牌。

移动应用和web应用的流程类似,整个流程交互如下,

  1. 用户打开移动应用,应用提示用户登录,并将用户导向第三方登录服务
  2. 在第三方登录服务中,用户输入用户名和密码,若登录成功,则跳转回移动应用,并返回授权码给移动应用。
  3. 移动应用将获取到的授权码,调用后端Token接口,该接口将根据授权码+应用ID+应用Secret,向第三方申请授权令牌,若一切正常,第三方颁发授权令牌给移动应用,移动应用将获取到的授权令牌存储在手机上的应用存储空间。
  4. 进入应用后,移动应用访问后端服务,授权令牌携带在请求header中
  5. 后端服务中一个通用的web filter对请求header中的授权令牌进行校验,若合法且有效,则用户可以正常访问后端服务,否则提示用户登录。
  6. 用户注销登录时,移动应用将授权令牌清除,并注销redis中的令牌。

2.5 第一方登录和第三方登录的授权区别

第一方登录和第三方登录之间最大的区别在于授权流程。

第一方登录在用户被认证之后,即刻颁发授权令牌,应用服务一般无需介入授权流程。而第三方登录在用户认证之后,先颁发给授权码给应用服务,应用服务还需根据这个授权码去换取授权令牌,换句话说,应用服务需介入授权流程。

这里有个问题是,为什么在第三方登录中,应用服务会被介入授权流程?主要原因是在第三方登录场景中,除了用户需要被认证,应用服务本身也需要被认证。而在第一方登录场景,只需用户认证,应用服务本身是可信的,其无需被认证。

2.6 登录架构实现小结

为了对比上述两个登录架构实现的不同之处,下面将各自的特点小结为下表,

第一方登录架构 第三方登录架构
登录功能 由己方提供 由第三方提供
使用第三方的用户账户
登录实现 通过http basic auth OAuth 2.0授权码模式
应用是否可信 可信,应用可以接触用户密码等敏感信息,应用本身无需认证 不可信
应用是否需要认证
应用是否可以接触用户密码等敏感信息
授权过程 登录成功后颁发token 1. 登录成功后返回授权码给应用
2. 应用根据授权码和应用ID换取token
Web页面的服务请求 通过cookie带上token 通过cookie带上token
移动应用的服务请求 通过header带上token 通过header带上token

3. 权限控制

在互联网应用开发中,安全控制在后端服务中实现。整个权限控制的过程一般会涉及到鉴权和权限控制两个环节。

鉴权的实现方式一般有两种,

  • 通过授权服务器:由于授权令牌是由授权服务器颁发的,所以由授权服务器校验也是自然而然的事情。这种方式会导致授权服务器的访问热点问题,为了缓解热点,缓存是必要配置。
  • 通过加解密:通过授权令牌的加解密方式,确认令牌的合法性。即,授权服务器颁发一个通过公钥加密的授权令牌,在校验的时候若能够通过私钥解密,则该令牌为合法令牌。这种方式有一个缺点是,一旦令牌注销失效后,信息无法及时通知到解密方。对令牌的注销时效性要求不高的场景下,可以使用这种方式,其大大缓解了授权服务器的访问热点问题。

若根据鉴权的架构方式,则可分为分布式和集中式两大类,

  • 分布式:通过后端web服务的filter实现分布式控制,在各个服务应用的运行实例中进行鉴权
  • 集中式:通过网关实现集中式控制,在访问流量的入口进行鉴权

鉴权后访问控制粒度从大到小有如下三种分类,

  • Http方法级别
  • API接口级别
  • API实现级别

下面将对鉴权的架构实现和控制粒度进行详细讨论。

3.1 分布式鉴权架构

分布式鉴权的架构如下图,

在web应用开发中,可以通过web应用服务的filter功能,对所有请求中的授权令牌进行校验,实现分布式鉴权。分布式鉴权实现简单,可以快速实现并使用,但随着应用服务架构的水平和垂直扩展,filter的升级将会成为头痛的问题。

3.2 集中式鉴权架构

集中式鉴权的架构如下图,

通过网关,在访问流量的入口,对所有请求中的授权令牌进行校验,实现集中式鉴权。集中式鉴权减轻了应用服务的接入成本,但会增加了网关的性能负荷。

3.3 分布式和集中式鉴权的优缺点

分布式鉴权和集中式鉴权有各自的优缺点小结为下表,

分布式鉴权 集中式鉴权
优点 简单,可以分散校验热点 应用服务接入成本低,方便鉴权管理
缺点 filter版本升级困难,需要对所有应用服务进行依赖更新 需要网关的支持,并且鉴权操作将增加网关的性能负荷
应用场景 简单的互联网web应用开发 大型互联网web应用开发

3.4 访问控制的粒度

鉴权后就会得到请求访问的权限,接下来则是根据权限来控制请求访问的允许或禁止。

根据访问控制粒度从大到小,可划分出如下几个控制级别,

  • Http方法级别
  • API接口级别
  • API实现级别

3.4.1 Http方法级别

若web应用服务是按照Restful的规范来开发接口服务,则可以根据Restful的定义对Http的不同方法实现不同的访问控制。

根据Restful的规范定义,Http方法具有如下安全性和幂等性的特点,

HTTP方法 Restful定义 安全性 幂等性
GET 获取资源
POST 创建资源
PUT 更新资源
DELETE 删除资源
HEADER 资源元信息
OPTIONS

表中,安全性和幂等性的概念如下,

  • 安全性是指该方法不改变资源的状态,即不改变后台数据
  • 幂等性是指该方法执行的同一操作多次,其结果保持一致

可以看到,对于安全的HTTP方法,比如GET操作,其访问控制可以宽松。而对于一些非安全操作,则需要根据不同权限来控制访问请求。在实际应用过程中,可以执行如下的权限控制规则,

  • 匿名用户:可以执行GET请求
  • 普通登录用户:可以执行GET、POST、PUT请求
  • 管理员:可以执行所有类别请求

总的来说,根据Http请求的方法和URI进行访问控制。

3.4.2 API接口级别

这个权限控制的粒度深入到代码控制层(Controller),不同的权限,可以访问不同的API接口。

一个spring mvc的代码样例如下,

@Controller
public class TokenApp {

    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public Principal getUserInfo(Principal me){
        return me;
    }

}

上面的代码表示,具有管理员角色的权限才能获取当前用户信息。

这个粒度的权限访问控制基本可以满足大多数应用场景需求。

3.4.3 API实现级别

在应用的实现级别进行控制,这个控制粒度非常细,其根据不同权限返回不一样的调用结果,例如,同样是查看学生列表,若是班主任,则返回一个班的学生列表,若是校长,则返回一个学校的学生列表。

4. 小结

本文主要从用户登录和权限控制两大功能方面梳理其架构实现,这些实现可以结合实际的应用场景,进行相应的匹配。

第一方登录 第三方登录
分布式权限控制 1. 简单web应用场景
2. 登录服务和应用服务相互可信
3. 登录服务和应用服务同域,可以授权共享
1. 简单web应用场景
2. 登录服务和应用服务相互不可信
3. 登录服务和应用服务不同域,各自独立开发部署,无法授权共享
4. 应用和登录服务模块彻底解耦,通过跨域跳转实现用户登录。
集中式权限控制 1. 大型web应用场景,有网关组件部署 1. 大型web应用场景,有网关组件部署
2. 快速接入第三方用户

5. 参考资料

  1. HTTP Authentication: Basic and Digest Access Authentication
  2. TOTP: Time-Based One-Time Password Algorithm