OAuth 2.0系列(二)--- 授权服务器

本来打算这篇直接上代码的,但是在看书的过程中发现有很多理论性的东西需要深挖,所以接下来我会先依次介绍OAuth中的授权服务器、客户端、受保护资源这三大服务器具体要做的事情和细节,本篇先来说说授权服务器。

授权服务器在OAuth中主要有以下四个作用:

  • 管理已注册的客户端
  • 用户对客户端授权
  • 为获得授权的客户端颁发令牌
  • 颁发刷新令牌并响应令牌刷新请求

其中前两者由授权端点完成,后两者由令牌端点完成,如下图:

 👆👆👆 上面这张图梳理出了每个端点在完成每一步的过程中需要做的事情,接下来对每一步进行详细解读。

一、授权端点

1.1 管理客户端

主要提供客户端注册功能,用下面的请求来表示:

curl --location --request POST 'http://authendpoint.com/createClient' \
--header 'Content-Type: application/json' \
--data-raw '{
    "clientName": "客户端名称",
    "redirectUris": [
        "http://client.com/callback1",
        "http://client.com/callback2",
        "http://client.com/callback3"
    ],
    "grantTypes": [
        "authorization_code",
        "implicit",
        "password",
        "refresh_token"
    ],
    "responseTypes": [
        "code",
        "token"
    ],
    "scopes": [
        "read",
        "write"
    ]
}'
  • clientName:客户端名称
  • reirectUris:客户端重定向地址,可以是多个,用来处理不同场景的请求
  • grantTypes:许可类型,像上一篇中介绍的授权码模式就是其中的authorization_code,其他四种许可类型会在后面的文章中进行详细讲解
  • responseTypes:请求授权服务器时的响应类型,一般有两种:授权码code和令牌token
  • scopes:授权的范围,比如读权限、写权限等

当客户端注册成功后,授权服务器会返回如下信息给客户端:

{
    "clientId": "daffdafd3554543dafdaf",
    "clientSecret": "DAAGAS34fdsf"
}
  • clientId:授权服务器为客户端分配的唯一标识
  • clientSecret:授权服务器用来对客户端进行身份认证的密钥

以上两者都是为了在客户端请求授权服务器时进行身份认证使用的必要参数,客户端必须好好保管,且clientSecret不能被泄露!!!

1.2 客户端授权

授权有两步:请求授权用户确认授权,请求授权会渲染一个授权页面,用户在页面上选择是否进行授权。

1.2.1 请求授权

用户在OAuth授权过程中的第一站是授权端点。授权端点是一个前端信道端点,客户端会将用户浏览器重定向至该端点,以发出授权请求。授权请求通常是一个GET请求,请求示例如下:

http://authendpoint.com/authorize?client_id=daffdafd3554543dafdaf&redirect_uri=http://client.com/callback1&state=dfdgjudbetrdd&response_type=code&scope=read write
  • client_id:客户端唯一标识,客户端授权服务器注册时由注册服务器为之分配,必传项!
  • redirect_uri:客户端重定向地址,客户端注册时提供给授权服务器的多个之中的一个,最好是精确匹配,否则会出现授权码劫持漏洞(后面介绍),必传项!
  • state:客户端生成的随机串,用来在回调函数(重定向地址所请求的方法)中进行验证是否是同一个请求对应的授权码,必传项!
  • response_type:请求授权服务器响应的类型,OAuth2中有两种:授权码code和令牌token,前者适用于授权码许可类型,后者适用于隐式许可类型(后面介绍)必传项!
  • scope:资源拥有者委托客户端请求的权限范围,非必传项

授权端点接收到请求之后需要做以下几件事:

1.从请求参数中获取到client_id

2.检查客户端

  • 检查该client_id对应的客户端是否存在。如果不存在,则不能授予任何访问权限,并要向用户显示错误信息
  • 检查请求的redirect_uri是否在客户端注册时提供的redirect_uris里面。OAuth规范允许客户端注册时信息中包含多个redirect_uri值,这样就可以让客户端在不同场景下使用不同的URL提供服务,有利于功能聚合。

3.当客户端检查通过之后需要渲染出一个页面来请求用户进行授权

  • 用户需要与这个页面交互,并向授权服务器提交授权决策,这将需要浏览器向授权服务器发送一个HTTP请求。在这一步,授权端点需要生成一个requestId返回给确认授权页面,并保存该值,以便在用户提交授权决策时能够找到两次请求之间的关系。

1.2.2 确认授权

 用户在授权页面选择授权结果,以及要授权客户端的权限范围,并发送确认/拒绝授权的请求给授权端点,授权端点需要提供处理确认授权的方法,请求示例如下:

curl --location --request GET 'http://authendpoint.com/approve?request_id=%E8%AF%B7%E6%B1%82%E6%8E%88%E6%9D%83%E6%97%B6%E8%BF%94%E5%9B%9E%E7%9A%84requestId&approve=Approve%28Deny%29'
  • request_id:在请求授权时由授权端点返回的标识,透传即可
  • approve:确认授权的结果,Approve或Deny

授权端点接受到请求之后需要做下面的这些事情:

1.Deny

如果客户拒绝授权,则需要告诉客户端实际情况,由于使用前端信道进行通信,无法直接向客户端发送信息,但是可以采用客户端向授权服务器发送请求的方式:从客户端托管的redirect_uri中选择一个,在该URL上添加一些特殊的查询参数,然后将用户的浏览器重定向至这个经过改造地址!

2.Approve

如果客户端同意授权,则首先检查在1.2.1中请求的响应类型是什么。

如果是授权码,则授权端点需要生成一个授权码并保存起来,以便进行后续流程的处理,同时将这个授权码和在1.2.1中的请求参数state一起拼接到1.2.1中的请求参数redirect_uri后面,一起重定向至客户端提供的地址,这样客户端就能拿到对应的授权请求的授权码进行令牌换取的操作。

如果是令牌,则直接返回一个令牌返回给客户端(后面介绍)

二、令牌端点

在请求授权中,授权服务器已经将用户重定向到客户端提供的地址,并且携带了授权码,客户端拿到授权码并完成自己的state校验之后,就向授权服务器的令牌端点请求令牌token,该请求为POST类型,且属于后端信道通信,实在客户端和授权服务器之间进行的,不需要浏览器参与。

请求如下:

curl --location --request POST 'http://tokenendpoint.com/token' \
--header 'Content-Type: application/json' \
--header 'Authorization: basic encodeCredentials(clientId,clientSecret)' \
--data-raw '{
    "grantType":"authorization_code",
    "code":"56abcgrh",
    "redirect_uri":"http://client.com/callback1"
}'

2.1 颁发令牌

 令牌端点接收到上面的请求之后,需要做两件事:对客户端进行身份认证处理授权许可请求

2.1.1 对客户端进行身份认证

  • 解析请求HEADER,获取clientId和clientSecret。传递客户端的方式有两种:使用HTTP基本认证方式和表单参数方式,为了遵循良好的服务端编程原则,支持多种输入类型,允许客户端按照自己的方式传递凭据。上面的请求示例中使用的是HTTP基本方式,将clientId和clientSecret用冒号分割之后进行BASE64编码。
  • 验证接受到的clientId与clientSecret是否与客户端注册时的数据一致

2.1.2 处理授权许可请求

 当令牌端点完成身份认证之后(验证过程中令牌端点会向授权端点请求客户端的注册信息),就要对请求体中的参数进行下面的校验:

  • 首先检查grantType参数,以保证客户端请求的许可类型是授权服务器所支持的
  • 然后检查授权码,如果是authorization_code类型,则要获取授权码code,并查询该code是否存在,如果找不到,则返回错误信息;如果能找到,则要确定它是否是为该客户端颁发的。(注意:授权码是一次性有效,只要使用过了就会将其移除,当客户端请求的授权码与客户端本身的信息不一致时,说明该授权码已经泄露,同样需要被丢弃)
  • 当授权码验证通过之后,令牌端点需要生成一个访问令牌accessToken,并将其保存起来以便后续使用。(注意:令牌内容可以具有内部结构,比如JWT或SAML断言,这些令牌可以被签名、加密,或者既被签名又被加密,而且在使用时仍然对客户端不透明,客户端只要能拿到并用之,无需知道其内容)

完成上面的处理之后,令牌端点就可以将生成的访问令牌以JSON的形式返回给客户端,一般情况下为了防止令牌泄露,最好给该令牌设置一个比较短的有效期,即指定过期时间,响应格式如下:

{
    "accessToken": "68bjh4543",
    "tokenType": "Bearer ",
    "expiration": "2021-11-07 9:33:00",
    "expiresInSeconds": "3600",
    "scope": "write read",
    "state": "teststate"
}
  • accessToken:访问令牌,用来获取受保护的资源
  • tokenType:令牌使用的方式,该响应体中是Bearer 的方式,使用时将HTTP HEADER Authorization的值设为【Bearer 68bjh4543】即可
  • expiration:过期时间
  • expiresInSecond:在这个值之后过期(单位:秒)
  • scope:该token适用的权限范围
  • state:客户端的state,这些都是在之前的请求中,将状态值state和授权码code一起保存在授权服务器,在这一步中透传给客户端,方便客户端校验是否为同一个请求对应的token

至此,授权服务器就完成了对一次令牌请求的基本处理,但是访问令牌是有有效期的,意味着当它过期之后,如果用户还要继续授权客户端请求资源,就要重新走一遍授权流程,这对用户来说是非常糟糕的体验,于是OAuth提供了刷新令牌的机制来解决这种尴尬。

2.2 刷新令牌

 刷新令牌是当访问令牌过期之后,用户无感知从授权服务器请求一个新的访问令牌的机制。刷新令牌和访问令牌都是在第一次获取访问令牌时,由令牌端点一起生成并返回给客户端的,而且它也有一个有效期,只不过这个有效期会比较长,可能是一年或者更长,当过了这个有效期之后,再请求访问令牌,就需要重新走完整的授权流程了,但是这个在用户体验上是完全可以接受的,包含刷新令牌时令牌端点的返回结构如下:

{
    "accessToken": "68bjh4543",
    "tokenType": "Bearer ",
    "expiration": "2021-11-07 9:33:00",
    "expiresInSeconds": 3600,
    "scope": "write read",
    "state": "teststate",
    "refreshToken": "68bjh4543fdsfsfrrer",
    "refreshTokenExpiration": "2022-11-07 9:33:00",
    "refreshTokenExpiresInSeconds": 86765
}

在2.1.2的基础上增加刷新令牌的值、过期时间和过期时长

  • refreshToken:刷新token的值,这个值的长度一般比访问令牌长,与访问令牌进行视觉上的区分
  • refreshTokenExpiration:刷新令牌的过期时间
  • refreshTokenExpiresInSeconds: 在这个值之后过期(单位:秒)

既然支持刷新令牌,令牌端点当然还要支持对刷新令牌请求的处理,一般客户端的刷新令牌请求为下面的格式

curl --location --request GET 'http://tokenendpoint.com/refreshToken?refreshToken=68bjh4543fdsfsfrrer&grant_type=refresh_token' \
--header 'Content-Type: application/json' \
--header 'Authorization: basic BASE64(clientId:clientSecret)'

刷新token的请求可以新增方法,也可以在之前的/token中实现,增加一种认证类型为refreshToken即可

  • 参数grantType:刷新令牌请求中的该值为refresh_token
  • 参数refreshToken:为颁发访问令牌时返回的refreshToken的值
  • header Authorization:与请求访问令牌时的header一致

当授权端点接受到上面的请求之后,需要做如下的处理:

  • 首先,检查该访问令牌是否存在,如果存在,还要检查该刷新令牌是否是颁发给该客户端的,如果这项校验不通过,则认为该刷新令牌已经泄露,需要直接删除
  • 当所有的检查都通过之后,可以基于该刷新令牌生成一个新的访问令牌,存储并返回给客户端。当刷新令牌被使用之后,令牌端点可以决定是否将该刷新令牌删除并颁发新的刷新令牌给客户端。

至此,OAuth2授权码模式中,授权服务器必须要完成的流程都已介绍完毕,下面介绍一些附加的功能。

2.3 授权范围的支持

 OAuth 2.0中一个很重要的机制就是权限范围,权限范围标识与特定授权相关联的访问权限的子集。当客户端的请求中包含权限范围时,授权服务器需要做如下处理:

  • 首先,校验注册请求中scope。在1.1中客户端注册时,提供了授权范围,当授权服务器接收到该参数后,不能直接将其保存,而是需要检查客户端请求的权限范围是否是受保护资源所能提供服务的子集,甚至还要限制客户端只能申请特定的权限范围
  • 然后,在客户端向授权服务器请求授权时,需要确保客户端请求的权限范围没有超出被允许的范围
  • 其次,当所有校验通过之后,在颁发授权码时需要将这些权限范围与生产的授权码保存在一起,以便在令牌端点收到请求时能查询权限范围
  • 最后,在颁发令牌时,令牌端点需要将令牌所绑定的权限范围告诉客户端

可以在刷新令牌请求中指定一组权限范围,并应用于新的访问令牌,这样客户端就能使用刷新令牌请求新的访问令牌时,新访问令牌的权限小于其被许可的权限范围,遵循了最小权限安全原则。

三、总结

OAuth 授权服务器无疑是OAuth系统中最复杂的部分:

  • 处理前端信道和后端信道的不同请求
  • 授权码许可流程需要在多个步骤中维护数据状态,最终才得以生成令牌
  • 授权服务器上存在很多可能被攻击的漏洞,每一处都需要进行适当的防护
  • 刷新令牌随访问令牌一起颁发,可以避免在无用户参与场景下(如定时任务)用于生成新的访问令牌
  • 全新范围用于限制访问令牌的权限

至此,授权服务器的介绍基本完成,下一篇我将详细介绍另外两个服务:受保护资源服务和客户端服务。

posted @ 2021-11-07 09:49  bug改了我  阅读(1188)  评论(0编辑  收藏  举报