pfan123/Articles

SSO、OAuth2.0、JWT 登录与授权理解

pfan123 opened this issue · 0 comments

早前接触和使用了公司内部登录/授权相关服务,现进行知识梳理,方便后续记忆回顾~

传统用户账号密码登录

image.png最传统的登录方法就是用户名密码校验登录了,流程简单,也有许多细节点,如下:

  • Cookie 存储要 HttpOnly (HttpOnly = true,Cookie只允许服务端修改,JS 无法操作),避免因前端 XSS 漏洞导致 Cookie 泄露
  • 密码不能存储明文,可存为加盐后的哈希值 md5(md5(password)+salt)
  • 所有服务器节点/进程需要能共享 Session,所以需要一个统一的 Session 存储层
  • 尽量使用 Https 协议,防止中间人攻击

这种登录模式下,登录态只对当前域名起作用,如当前应用是 app1.oa.com。假设企业内有其它的应用 app2.oa.com、app1.asd.com,那么得另外开发两套登录系统,不能扩展,这显然是不合适的。那么就需要一套多应用共用同一套登录逻辑的系统。

单点登录(SSO)

单点登录 (SSO,Single Sign On) 是一种身份验证方法,使用户能够使用一次登录和一组凭据,可以同步登录状态到其他服务。

同顶域单点登录

由账号密码单应用登录流程,我们可以很容易想到,想要共享登录态,只需要客户端能共享 Cookie,服务端能共享 Session 存储就行了。

服务端共享 Session 存储很简单,而客户端,只需要在写入 Cookie 的时候,将 Cookie 写到顶域中去就行了,例如 app1.oa.com,Cookie 写到 oa.com下面,这样 app2.oa.com 也可以读取到 Cookie,然后在后端与 app1.oa.com 使用同一套 Session 存储即可。

image.png

同域名下有很多 app 时,可以抽出一个 app,如 sso.oa.com,所有的 appx.oa.com 未登录时都跳转到 sso.oa.com,在 sso.oa.com上验证登录态,再重定向回 appx.oa.com 并种 .oa.com 的 Cookie,这样就实现了同域名下的单点登录。

image.png

不同顶域单点登录

上面讨论的是同顶域下的情况,假设企业内此时还有一个应用,app1.asd.com,也想用 sso.oa.com 来登录,咋办呢?

现在的问题在于,访问 sso.oa.com 的时候,sso 应用不能往 asd.com下跨域写 Cookie了,难么 sso.oa.com 就得换一种方案来告诉 app1.asd.com,用户是否在 sso 上登录、登录者身份等信息。

image.png

Ticket 的实现有很多,例如在 sso.oa.com 中将用户名、有效期等信息对称加密成 Ticket,或者将一条存储记录的索引 id 作为 Ticket,在sso.oa.com/valid 校验成功后返回给 app1.asd.com,这样 asd.com 域下的 app 也能使用 sso.oa.com 作为单点登录服务了。

SSO 在实践的过程中可能会有很多变种,例如访问 sso.oa.com/valid 这一步不再有 appx.asd.com 这些应用服务来实现,而由一个统一的接入层服务来做,并把验证后的结果通过 Header 传递给 appx.asd.com 服务等。

单点登录实现 - CAS

前面说到的是原理的简单理解,真正的业界企业级开源单点登录解决方案叫 CAS(Central Authentication Service,**认证服务,一种独立开放指令协议)。CAS 是 [耶鲁大学](https://baike.baidu.com/item/耶鲁大学/411935)(Yale University)发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的[单点登录](https://baike.baidu.com/item/单点登录/4940767)方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。

CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。CAS 最基本的协议过程:

img

OAuth2.0 授权

单说单点登录,使用 OAuth2.0 (Open Authority 2.0) 授权,也是可以做 CAS 解决方案中登录校验的部分的,但是 OAuth2.0 除了做身份校验外,还有个很重要的特性,是可以做细粒度的,用户级别的授权,这个是 CAS 做不到的。

OAuth2.0 将整个系统中的角色分为 HTTP 服务提供商(认证服务器 + 资源服务器 )、第三方应用、用户、用户代理(如浏览器),协议要解决的问题是:HTTP 服务提供商(如微信),授权第三方应用(如某 H5 活动页),在用户代理(如浏览器或微信客户端)中,由用户(经过认证服务器)允许后,从资源服务器中获取某些用户资源(如昵称、头像)

OAuth 2.0 的运行流程如下图,摘自 [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)。

OAuth运行流程

(A)用户打开客户端以后,客户端要求用户给予授权。

(B)用户同意给予客户端授权。

(C)客户端使用上一步获得的授权,向认证服务器申请令牌。

(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

(E)客户端使用令牌,向资源服务器申请获取资源。

(F)资源服务器确认令牌无误,同意向客户端开放资源。

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

简化模式

**简化模式(implicit grant type)**不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

简化模式

它的步骤如下:

(A)客户端将用户导向认证服务器。

(B)用户决定是否给于客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。

(F)浏览器执行上一步获得的脚本,提取出令牌。

(G)浏览器将令牌发给客户端。

下面是上面这些步骤所需要的参数。

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • response_type:表示授权类型,此处的值固定为"token",必选项。
  • client_id:表示客户端的ID,必选项。
  • redirect_uri:表示重定向的URI,可选项。
  • scope:表示权限范围,可选项。
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

下面是一个例子。

GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
    &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

C步骤中,认证服务器回应客户端的URI,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

下面是一个例子。

 HTTP/1.1 302 Found
 Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
           &state=xyz&token_type=example&expires_in=3600

在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。

根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,服务提供商的资源服务器发送过来的代码,会提取出Hash中的令牌。

密码模式

**密码模式(Resource Owner Password Credentials Grant)**中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

密码模式

它的步骤如下:

(A)用户向客户端提供用户名和密码。

(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。

(C)认证服务器确认无误后,向客户端提供访问令牌。

B步骤中,客户端发出的HTTP请求,包含以下参数:

  • grant_type:表示授权类型,此处的值固定为"password",必选项。
  • username:表示用户名,必选项。
  • password:表示用户的密码,必选项。
  • scope:表示权限范围,可选项。

下面是一个例子。

 POST /token HTTP/1.1
 Host: server.example.com
 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
 Content-Type: application/x-www-form-urlencoded

 grant_type=password&username=johndoe&password=A3ddj3w

C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

 HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 Cache-Control: no-store
 Pragma: no-cache

 {
   "access_token":"2YotnFZFEjr1zCsicMWpAA",
   "token_type":"example",
   "expires_in":3600,
   "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
   "example_parameter":"example_value"
 }

上面代码中,各个参数的含义参见《授权码模式》一节。

整个过程中,客户端不得保存用户的密码。

客户端模式

**客户端模式(Client Credentials Grant)**指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

客户端模式

它的步骤如下:

(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。

(B)认证服务器确认无误后,向客户端提供访问令牌。

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • granttype:表示授权类型,此处的值固定为"clientcredentials",必选项。

  • scope:表示权限范围,可选项。

    POST /token HTTP/1.1
    Host: server.example.com
    Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
    Content-Type: application/x-www-form-urlencoded

    grant_type=client_credentials

认证服务器必须以某种方式,验证客户端身份。

B步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

 HTTP/1.1 200 OK
 Content-Type: application/json;charset=UTF-8
 Cache-Control: no-store
 Pragma: no-cache

 {
   "access_token":"2YotnFZFEjr1zCsicMWpAA",
   "token_type":"example",
   "expires_in":3600,
   "example_parameter":"example_value"
 }

上面代码中,各个参数的含义参见《授权码模式》一节。

Ps: 如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。

客户端发出更新令牌的HTTP请求,包含以下参数:

  • granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。

OAuth2.0 扩展

CAS 与 OAuth2 对比

  • 流程复杂程度: 两者差不多,OAuth2.0 会相对多一步获取资源,因为两者有一点根本差别在于: CAS 场景中资源是在于Server,OAuth2.0 场景中资源是在于Authorization Server
  • 安全性:
    1. CAS
      1. ticket只能使用一次
      2. ticket有效期设置,条件内越短越安全
      3. ticket生成需要足够随机,如果被攻击者猜出规律,则可以计算出下一个ticket值
    2. OAuth2.0
      1. 需要预先申请client_idclient_secret,同时也可以对scope和域名进行匹配校验
      2. code有效期较短,code只能被使用一次,使用第二次会使第一次获得的access_token失效。
      3. access_token通过后端方式接口获取和接口使用,不会展示在前端,暴露可能性低
      4. access_token只有scope资源使用权限,并不会影响到其他部分
      5. 通过state参数可以有效防止csrf攻击
  • 应用范围: 可以理解为 OAuth2.0 涵盖了 CAS 的功能。OAuth2.0 只是授权框架,基于此,可以完成所有数据交换。
  • 用户体验:
    1. 第一次授权流程基本一致;
    2. 但是 CAS 方式不方便持久化数据,Session 过期或是清除后需要再次获取授权;
    3. OAuth2.0 方式可以通过 refresh_token 后端更新 token,无需再走一遍完整流程。

OAuth2.0 服务开发

一般情况下我们只会作为用户,或者第三方应用开发者来使用 OAuth2.0 协议,假如我们处于一个需要做 OAuth2.0开放平台的互联网服务团队,或者想深入了解 OAuth2.0 细节的实现,可以基于一个 OAuth2.0 的开源模块实现个认证服务器,并阅读下模块源码,对照着rfc文档参考各个逻辑的实现。例如 [node-oauth2-server模块](https://github.com/oauthjs/node-oauth2-server)

JSON Web Token (JWT)

前面提到的各种登录/认证方法,第三方应用的服务器端总是有状态的,需要维护登录用户的 Session 对象。要么前置一个无状态接入层,通过 sessionid hash 到各服务节点,服务节点 LRU 缓存 Session,要么每个节点就得从存储层中读取 Session 信息,请求量大的时候这种 IO 压力不小。

很自然的想法是,把用户信息存放在客户端去中心化,每次请求的时候随 Cookie 或 Http Header 等渠道发送到服务器上,这样就可以让服务器变成无状态的存在了。JWT 就是实现这种想法的一个标准协议。

JSON Web Token(缩写 JWT),是一种基于 JSON 的、用于在网络上声明某种主张的令牌(token),详见 [rfc7519](https://tools.ietf.org/html/[rfc7519](https://tools.ietf.org/html/rfc7519))。JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{
  "姓名": "张三",
  "角色": "管理员",
  "到期时间": "2018年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

服务器就不保存任何 Session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT 的数据结构

JWT 通常由三部分组成:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

写成一行,就是下面的样子。

Header.Payload.Signature

img

Header 中存储加密算法,Signature是根据 Header 中指定的加密算法,及存在服务端的秘钥计算出来的加密串。具体算法在这里不赘述了,详见 rfc7519

JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT 的几个特点

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  • JWT 不加密的情况下,不能将秘密数据写入 JWT。

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

References