OAuth 的全方位渗透及node实现

1,018 阅读10分钟

背景

我们先从一个非常典型例子出发,假如你在某网盘上传了你的很多图片,网盘提供了图片存储,另一个某打印店提供了在线打印图片。

由于存储和答应是由两家不同的服务商提供的,两家各自都提供了用户的注册功能,所以如果你要想在打印店上去打印你放在网盘的照片时,此时你会想到2种方案:

  • 假设你的账户密码都不一样,你可以先将待打印的图片从网盘上下载下来,然后在上传到打印店的网站上,之后进行打印。这种模式是最原始,也是效率最低下。
  • 为了省事,你可以将你网盘的账户密码交给打印店老板,告诉他你要打印的图片,让他给你操作,省事,但是风险太大,帐号密码都泄漏了,就不怕别人去篡改个人信息或者查看你的隐私?

之前很多公司包括GoogleYahooMicrosoft都尝试解决这个问题,这也促使OAuth的诞生。

什么是 OAuth ?

OAuthOpen Authorization)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,目前在全世界范围内得到广泛应用。

OAuth 解决思路

OAuth客户端(Client)打印店资源服务器(Resource Server网盘 之间,设置了一个授权层(Authorization Layer),通过 授权服务器(Authorization Server 提供用户授权。

客户端 要想访问 资源服务器 必须要经过 授权层用户资源拥有者(Resource Owner) 可以在登录的时候,指定授权层令牌的权限范围和有效期,当用户同意授权后才向客户端开放用户储存的资料。

授权层

通俗来说:当打印店要访问用户的你网盘的图片时,通过OAuth机制,打印店要向网盘的授权服务器请求授权,网盘服务商将引导你在网盘的网站上登录,并询问你是否将访问图片的服务授权给打印店。当你点击同意后,打印店就可以访问你网盘上的图片服务。整个过程打印店没有触及到你网盘的帐号信息,安全便捷。

OAuth 授权流程

Abstract Protocol Flow

 +--------+                               +---------------+
 |        |--(A)- Authorization Request ->|   Resource    |
 |        |                               |     Owner     |
 |        |<-(B)-- Authorization Grant ---|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(C)-- Authorization Grant -->| Authorization |
 | Client |                               |     Server    |
 |        |<-(D)----- Access Token -------|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(E)----- Access Token ------>|    Resource   |
 |        |                               |     Server    |
 |        |<-(F)--- Protected Resource ---|               |
 +--------+                               +---------------+

大体流程如下:

  • Authrization Request:用户打开客户端,客户端向用户请求对资源服务器的authorization grant,要求用户给予授权。
  • Authorization Grant(Get):用户同意授权请求,客户端将收到一个authorization grant授权许可。
  • Authorization Grant(Post):客户端向授权服务器发送它自己的客户端身份标识和上一步中的 authorization grant授权许可,请求访问令牌。
  • Access Token(Get):认证服务器对客户端身份进行认证,如果认证通过并且 authorization grant 也被验证通过,授权服务器将为客户端派发 access token 访问令牌,授权阶段至此全部结束。
  • Access Token(Post && Validate):客户端向资源服务器发送access token用于验证并请求资源信息。
  • Protected Resource(Get):资源服务器进行请求验证,如果 access token验证通过,资源服务器将向客户端返回资源信息。

授权模式

根据应用请求授权的方式和授权方服务支持的 Grant Type的不同,OAuth 2 定义了四种 Grant Type(授权模式),每一种都有适用的应用场景:

  • 授权码模式(Authorization Code):最常使用的一种授权许可类型,结合普通服务器端应用使用。
  • 隐式授权模式(Implicit):跳过授权码这个步骤,适用于移动应用或 Web App
  • 密码模式(Resource Owner Password Credentials):客户端提供帐号密码,向服务商索要授权,适用于受信任客户端应用
  • 客户端模式(Client Credentials):客户端直接向服务商索取授权,适用于客户端调用主服务API型应用

授权码模式(Authorization Code)

是目前互联网上最常使用的一种授权模式,比如QQ,微博,Facebook和豆瓣等等,它要求第三方应用先申请一个授权码(Authorization Code),然后再用该码获取令牌,请求数据,流程如下:

 +----------+
 | Resource |
 |   Owner  |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+                                          +---------------+
 |         -+----(A)-- User Authorization Request ---->|               |
 |  User-   |                                          | Authorization |
 |  Agent  -+----(B)-- User authenticates ------------>|     Server    |
 |          |                                          |               |
 |         -+----(C)-- Authorization Code Grant ------<|               |
 +-|----|---+                                          +---------------+
   |    |                                                 ^      v
  (A)  (C)                                                |      |
   |    |                                                 |      |
   ^    v                                                 |      |
 +---------+                                              |      |
 |         |>---(D)-- Access Token Request ---------------'      |
 |  Client |                                                     |
 |         |                                                     |
 |         |< ---(E)-- Access Token Grant -----------------------'
 +---------+       (w/ Optional Refresh Token)

User Authorization Request :用户访问客户端,客户端构造了一个用于请求authorization codeURL并引导用户跳转访问,链接格式大概如下:

https://open.server-name.com/oauth2/authorize
    ?response_type=code
    &client_id=AppID
    &redirect_uri=REDIRECT_URI
    &scope=SCOPE
    &state=STATE
  • response_type:授权模式,必选,此时值固定为code
  • client_id:客户端身份标识,一般是注册时候的分配的 AppID
  • redirect_uri:授权成功后重定向地址,必选,注意需要将uri进行URLEncode
  • scope:授权范围,可选
  • state:客户端的状态值,必选,一般是随机字符串,成功授权后回调时会原样带回,为了防止CSRF攻击

User authenticates:用户决定是否给客户端授权,授权服务会提示用户授权或拒绝应用程序访问其帐户信息

Authorization Code Grant:用户确认授权,授权服务器将重定向之前客户端提供的redirect_uri地址,并附带codestate参数,客户端便能取到authorization code的值,链接格式大概如下:

https://client-name.com/redirect_url
 ?code=520DD95263C1CFEA087
 &state=STATE
  • code:授权服务器生成的authorization code,即授权码,code 有效期较短,一般维持在 5 - 10分钟,授权服务器可自行配置
  • state:即客户端之前携带的 state 值,可进行检查对比与最初设置的状态值相匹配,防止CSRF攻击

Access Token Request:客户端获取到授权码 code 后,可向服务器请求(post)获取 access_token ,一般来说请求参数如下:

参数 是否必须 含义
grant_type 必须 授权类型,此值固定为authorization_code
code 必须 授权码
client_id 必须 客户端标识,一般是第三方分配的AppID
client_secret 必须 客户端密钥,一般是第三方分配的AppSecret
redirect_uri 必须 回调地址,与之前的 redirect_uri保持一致

Access Token Grant:服务器会验证客户端传过来的参数,验证通过后,给客户端返回 access token,一般返回数据格式如下:

{
  "access_token":"2YotnFZFEjr1zCsicMWpAA",
  "token_type":"example""expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
  "scope":"user_info"
}
  • access_token:访问令牌
  • token_type:令牌类型
  • expires_in:令牌过期时间
  • refresh_token:刷新令牌
  • scope:权限范围

其中 refresh_token 用于在授权自动续期步骤中,获取新的Access_Token时需要提供的参数。

至此,授权流程全部结束。

隐式授权模式(Implicit)

跟前面的Authorization Code模式非常相似,只是省略掉了颁发授权码(Authorization Code)给客户端的过程,而是直接返回访问令牌和可选的刷新令牌。适用于没有Server服务器来接受处理Authorization Code的第三方应用,其整个流程如下:

 +----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+                                         +---------------+
 |         -+----(A)-- User Authorization Request --->|               |
 |  User-   |                                         | Authorization |
 |  Agent  -|----(B)-- User Authenticates ----------->|     Server    |
 |          |                                         |               |
 |          |<---(C)-- Redirect URI With ------------<|               |
 |          |          Access Token                   +---------------+
 |          |            
 |          |                                         +---------------+
 |          |----(D)---Follow  Redirect URI --------->|   Web-Hosted  |
 |          |                                         |     Client    |
 |          |                                         |    Resource   |
 |          |<---(E)-- Send Token Extract Script ----<|               |
 |          |                                         +---------------+
 +-|--------+
   |    |
  (A)  (F) Pass Token to Application
   |    |
   ^    v
 +---------+
 |         |
 |  Client |
 |         |

User Authorization Request:客户端提构造了一个用于请求授权的链接,链接格式大概如下:

https://open.server-name.com/oauth2/authorize
    ?response_type=token
    &client_id=AppID
    &redirect_uri=REDIRECT_URI
    &scope=SCOPE
    &state=STATE

与之前的授权码模式相比,只是将 response_type 换成了 token

User Authenticates:与之前的相同,用户决定是否给客户端授权

Redirect URI With Access Token:用户同意授权,认证服务器将用户重定向到客户端指定的重定向redirect_uri,并在uri的中添加访问令牌,链接格式大概如下:

http://client.name.com/redirect_url/#access_token=2YotnFZFEjr1zCsicMWpAA&token_type=Bearer&expires_in=3600&scope=SCOPE&state=STATE

此时的 token_type 恒为 Bearer

要注意的是,返回值放到了REDIRECT_URIhash 部分,而不是作为 ?query 参数,这样浏览器在访问重定向指定的url时,就不会把这些数据发送到服务器。

Follow Redirect URI:浏览器请求redirect_uri标识的客户端地址,此时不包含hash值,并保留access_token相关信息。

Send Token Extract Script:客户端返回一个包含 token 的页面,页面执行脚本获取 redirect_uri 中的 access_token

Pass Token to Application:浏览器获取的令牌发给客户端。

至此,授权流程全部结束。

密码模式(Resource Owner Password Credentials)

这种模式更加简化,客户端直接使用用户提供的usernamepassword来直接请求获取access_token信息,这种模式一般适用于用户高度信任第三方客户端的情况,其整个流程如下:

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

Resource Owner Password Credentials From User Input:用户向客户端提供用户名与密码作为授权凭据。

Resource Owner Password Credentials From Client To Server:客户端向授权服务器发送用户输入的授权凭据以请求 access token(要求客户端必须已经在服务器端进行注册),其请求参数主要如下:

参数 是否必须 含义
grant_type 必须 授权类型,此值固定为password
username 必须 用户登陆名
passward 必须 用户登陆密码
scope 非必须 授权范围

Access Token Passed To Application:授权服务器对客户端进行认证并检验用户凭据的合法性,如果检验通过,将向客户端返回 access token

至此,授权流程结束。

客户端模式(Client Credentials)

这是最简单的一种授权模式,客户端直接已自己的名义,而不是用户的名义直接去授权服务器发起授权,获取 access_token,流程也是非常简单:

 +---------+                                  +---------------+
 |         |                                  |               |
 |         |>--(A)- Client Authentication --->| Authorization |
 | Client  |                                  |     Server    |
 |         |<--(B)---- Access Token ---------<|               |
 |         |                                  |               |
 +---------+                                  +---------------+

Client Authentication:客户端直接发起授权请求,此时的 grant_type 的值为 client_credentials

Access Token:认证服务器确认身份后,向客户端发放 access_token

小试牛刀

鉴于授权码模式(Authorization Code) 是目前来说使用最为广泛,流程也时最为最完整、流程最严密的一种授权模式,我以它为例采用的 koa2 ,自定义实现了一个简单的 Authorization Code 授权方案。

主要思路还是通过 client 引导用户点击进入授权页,在授权页面通过确认授权后向后端请求 code , 重定向到 redirect_uri , 并获取服务端的 code,之后通过 code 再去请求服务端获取令牌 access_token,之后通过 access_token 获取用户信息,整个效果如下图所示:

这里我就不放代码了,容易占地方!!详情请戳这里

pass:代码里面的数据都是mock的,还是有较多异常case未做处理,旨在了解整个授权的过程和实现思路,仅供参考

结语

OAuth 的授权方案目前已经非常成熟,在整个互联网行业中也是非常常见的,早些年做微信生态的公众号的时候初次接触,到现在的深入了解,也算是一些进步吧。

希望阅读完本文也能让你对 OAuth 授权有一个深刻全面的认识,或者是加深巩固 OAuth 授权方面的知识。

理论结合实践,才是我们做为coder应有的追求,加油!

参考资料及文献