常见登录鉴权方案

4,058

编者注:今天我们分享的是卢士杰同学整理的网站常用鉴权方案的实现原理与实现以及他们的适用场景,帮助大家在业务中做合适的选择。

背景

说起鉴权大家应该都很熟悉,不过作为前端开发来讲,鉴权的流程大头都在后端小哥那边,本文的目的就是为了让大家了解一下常见的鉴权的方式和原理。

认知:HTTP 是一个无状态协议,所以客户端每次发出请求时,下一次请求无法得知上一次请求所包含的状态数据。

一、HTTP Auth Authentication

简介

HTTP 提供一个用于权限控制和认证的通用框架。最常用的HTTP认证方案是HTTP Basic Authentication

鉴权流程

加解密过程

// Authorization 加密过程
let email = "postmail@test.com"
let password = "12345678"
let auth = `${email}:${password}`
const buf = Buffer.from(auth, 'ascii');
console.info(buf.toString('base64')); // cG9zdG1haWxAdGVzdC5jb206MTIzNDU2Nzg=

// Authorization 解密过程
const buf = Buffer.from(authorization.split(' ')[1] || ''),  'base64');
const user = buf.toString('ascii').split(':');

其他 HTTP 认证

通用 HTTP 身份验证框架有多个验证方案使用。不同的验证方案会在安全强度上有所不同。

IANA 维护了一系列的验证方案,除此之外还有其他类型的验证方案由虚拟主机服务提供,例如 Amazon AWS ,常见的验证方案包括:

  • Basic (查看 RFC 7617, Base64 编码凭证. 详情请参阅下文.),
  • Bearer (查看 RFC 6750, bearer 令牌通过OAuth 2.0保护资源),
  • Digest (查看 RFC 7616, 只有 md5 散列 在Firefox中支持, 查看 bug 472823 用于SHA加密支持),
  • HOBA (查看 RFC 7486 (草案), HTTP Origin-Bound 认证, 基于数字签名),
  • Mutual (查看 draft-ietf-httpauth-mutual),
  • AWS4-HMAC-SHA256 (查看 AWS docs)

二、Cookie + Session

注册流程

思考:为什么要在密码里加点“盐”?

鉴权流程

Session 存储

最常用的 Session 存储方式是 KV 存储,如Redis,在分布式、API 支持、性能方面都是比较好的,除此之外还有 mysql、file 存储。

如果服务是分布式的,使用 file 存储,多个服务间存在同步 session 的问题;高并发情况下错误读写锁的控制。

Session Refresh

我们上面提到的流程中,缺少 Session 的刷新的环节,我们不能在用户登录之后经过一个 expires 时间就把用户踢出去,如果在 Session 有效期间用户一直在操作,这时候 expires 时间就应该刷新。

以 Koa 为例,刷新 Session 的机制也比较简单: 开发一个 middleware(默认情况下所有请求都会经过该 middleware),如果校验 Session 有效,就更新 Session 的 expires: 当前时间+过期时间。

优化:

  1. 频繁更新 session 会影响性能,可以在 session 快过期的时候再更新过期时间。
  2. 如果某个用户一直在操作,同一个 sessionID 可能会长期有效,如果相关 cookie 泄露,可能导致比较大的风险,可以在生成 sessionID 的同时生成一个 refreshID,在 sessionID 过期之后使用 refreshID 请求服务端生成新的 sessionID(这个方案需要前端判断 sessionID 失效,并携带 refreshID 发请求)。

单设备登录

有些情况下,只允许一个帐号在一个端下登录,如果换了一个端,需要把之前登录的端踢下线(默认情况下,同一个帐号可以在不同的端下同时登录的)。

这时候可以借助一个服务保存用户唯一标识和 sessionId 值的对应关系,如果同一个用户,但 sessionId 不一样,则不允许登录或者把之前的踢下线(删除旧 session )。

三、JWT

简介

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT 组成

JWT 由三部分组成,分别是 header(头部),payload(载荷),signature(签证) 这三部分以小数点连接起来。

例如使用名为 jwt-token 的cookie来存储 JWT 例如:

jwt-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0.WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8;

使用.分割值可以得到三部分组成元素,按照顺序分别为:

  • header

    • 值:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    • Base64 解码: {"alg": "HS256", "type": "JWT"}
  • payload

    • 值:eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0
    • Base64 解码:
      {
        "name": "lushijie", 
        "iat": 1532595255, // 发布时间
        "exp": 1532595270 // 过期时间
      }
      
  • signature

    • 值:WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8
    • 解码:
      const headerEncode = base64Encode(header);
      const payloadEncode = base64Encode(payload);
      let signature = HMACSHA256(headerEncode + '.' + payloadEncode, '密钥');
      

鉴权流程

Token 校验

对于验证一个 JWT 是否有效也是比较简单的,服务端根据前面介绍的计算方法计算出 signature,和要校验的JWT中的 signature 部分进行对比就可以了,如果 signature 部分相等则是一个有效的 JWT。

Token Refresh

为了减少 JWT Token 泄露风险,一般有效期会设置的比较短。 这样就会存在 JWT Token 过期的情况,我们不可能让用户频繁去登录获取新的 JWT Token。

解决方案:

可以同时生成 JWT Token 与 Refresh Token,其中 Refresh Roken 的有效时间长于 JWT Token,这样当 JWT Token 过期之后,使用 Refresh Token 获取新的 JWT Token 与 Refresh Token,其中 Refresh Token 只能使用一次。

四、OAuth

简介

有时候,我们登录某个网站,但我们又不想注册该网站的账号,这时我们可以使用第三方账号登录,比如 github、微博、微信、QQ等。

开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

OAuth是OpenID的一个补充,但是完全不同的服务。

—— 摘自 维基百科

授权流程

名词解释:

  • Third-party application:第三方应用程序又称"客户端"(client),比如打开知乎,使用第三方登录,选择 Github 登录,这时候知乎就是客户端。
  • Resource Owner:资源所有者,本文中又称"用户"(user),即登录用户。
  • Authorization server:认证服务器,即 Github 专门用来处理认证的服务器。
  • Resource server:资源服务器,即 Github 存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

  • A. A网站让用户跳转到 GitHub,请求授权码;GitHub 要求用户登录,然后询问“知乎网站要求获得 xx 权限,你是否同意?”;
  • B. 用户同意,GitHub 就会重定向回 A 网站,同时发回一个授权码;
  • C. A 网站使用授权码,向 GitHub 请求令牌;
  • D. GitHub 返回令牌;
  • E. A 网站使用令牌,向 GitHub 请求用户数据;

其他授权模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。除了我们上面所说的授权码模式,其实还有其他授权模式:

  1. 简化模式(Implicit grant type)
    有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤
  2. 密码模式(Resource Owner Password Credentials Grant)
    如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌
  3. 客户端模式(Client Credentials Grant)
    适用于没有前端的命令行应用,即在命令行下请求令牌

关于这些模式详细请见:OAuth2.0 的四种方式

单点登录

单点登录(Single Sign On, SSO),即:单一标记(单点)登录。例如:QQ,我在QQ空间登录一次,我可以去访问QQ产品的其他服务:QQ邮箱、腾讯新闻等,都能保证你的账户保持登录状态。

延伸阅读:

五、总结对比

没有最好,只有最合适!!!

  • HTTP Auth Authentication:
    • 梳理总结:
      通用 HTTP 身份验证框架有多个验证方案使用。不同的验证方案会在安全强度上有所不同。HTTP Auth Authentication 是最常用的 HTTP认证方案,为了减少泄露风险一般要求 HTTPS 协议。
    • 适用场景:
      一般多被用在内部安全性要求不高的的系统上,如路由器网页管理接口
    • 问题:
      1. 请求上携带验证信息,容易被嗅探到
      2. 无法注销
  • Cookie + Session:
    • 梳理总结:
      • 服务端存储 session ,客户端存储 cookie,其中 cookie 保存的为 sessionID
      • 可以灵活 revoke 权限,更新信息后可以方便的同步 session 中相应内容
      • 分布式 session 一般使用 redis(或其他KV) 存储
    • 使用场景:
      适合传统系统独立鉴权
  • JWT:
    • 梳理总结:
      • 服务器不再需要存储 session,服务器认证鉴权业务可以方便扩展
      • JWT 并不依赖 cookie,也可以使用 header 传递
      • 为减少盗用,要使用 HTTPS 协议传输
    • 适用场景:
      • 适合做简单的 RESTful API 认证
      • 适合一次性验证,例如注册激活链接
    • 问题:
      1. 使用过程中无法废弃某个 token,有效期内 token 一直有效
      2. payload 信息更新时,已下发的 token 无法同步
  • OAuth:
    • 梳理总结:
      • OAuth是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。
      • GitHub OAuth 文档 Identifying and authorizing users for GitHub Apps
    • 适用场景:
      OAuth 分为下面四种模式
      1. 简化模式,不安全,适用于纯静态页面应用
      2. 授权码模式,功能最完整、流程最严密的授权模式,通常使用在公网的开放平台中
      3. 密码模式,一般在内部系统中使用,调用者是以用户为单位。
      4. 客户端模式,一般在内部系统之间的 API 调用。两个平台之间调用,以平台为单位。