1. 认证和授权

说简单点就是:

认证 (Authentication): 你是谁。

授权 (Authorization): 你有权限干什么。

稍微正式点的说法就是:

  • Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。
  • Authorization(授权) 发生在 Authentication(认证) 之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。

2. Cookie

cookie 是将信息存储在本地的,很不安全。

cookie 在登录中的主要作用是用来存 SESSIONID 或者 Token 的,如果你还在用 cookie 存储账号密码来实现自动登录的话,请绕道。

cookie 可以设置域名,再访问所设置的域名的时候,会自动将 cookie 携带过去,这时候根据 cookie 中的 SESSIONID 获取服务器中对应的 session 或者验证 cookie 中的 token 即可获取用户的登录信息。

3. Session

因为 session 是存储在服务器端的,所以用来做登录信息的存储比较安全,当用户登录完成以后,将用户登录信息存储在 session 中,给客户端返回一个 SESSIONID,客户端再次访问的时候拿着 SESSIONID 就可以获取到对应 session 的值,就可以获取到用户的登录信息,听起来很好,但是有两大弊端。

  1. 客户端只需要存储一个 SESSIONID 即可,但服务器端要存储所有用户的登录信息,对于服务器来说是一笔很大的开支。
  2. 一个项目可能有多个服务器,服务器端的 session 不共享也是一个问题,此处不仔细讨论。

4. Token

4.1 什么是 token

token 即是一种常用的登录技术,即用户第一次登录输入账户密码,然后服务器会给用户返回一个 token,客户端把这个 token 存储在 cookie 中,记录用户的登录状态,用户下次再访问的时候,就不需要再次登录了,只需要将 token 携带过去即可。

一般来说,token 会携带用户的 ID,当客户端把 token 携带到服务器的时候,服务器会对 token 进行验证、解密,拿到 token 中的用户 ID。

4.2 token 认证的优势

相比于 Session 认证的方式来说,使用 token 进行身份认证主要有下面几个优势:

1.无状态

token 自身包含了身份验证所需要的所有信息,使得我们的服务器不需要存储 Session 信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。但是,也正是由于 token 的无状态,也导致了它最大的缺点:当后端在 token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 Logout 的话,token 也还有效。除非,我们在后端增加额外的处理逻辑。

2.有效避免了CSRF 攻击

CSRF

3.适合移动端应用

使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。

但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用。

4.单点登录友好

使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题。

4.3 token 时效性问题

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

我们先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。

  1. 类似于 Session 认证中的做法:这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。
  2. 每次请求都返回新 token :这种方案的的思路很简单,但是,很明显,开销会比较大。
  3. token 有效期设置到半夜 :这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
  4. 用户登录返回两个 token :第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken 和 refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:1.需要客户端来配合;2.用户注销的时候需要同时保证两个 token 都无效;3.重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的 accessToken)。

4.4 注销后 token 还有效问题

与之类似的具体相关场景有:

  1. 退出登录;
  2. 修改密码;
  3. 服务端修改了某个用户具有的权限或者角色;
  4. 用户的帐户被删除/暂停。
  5. 用户由管理员注销;

这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 token 认证的方式就不好解决了。我们也说过了,token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。那么,我们如何解决这个问题呢?查阅了很多资料,总结了下面几种方案:

  • 将 token 存入内存数据库:将 token 存入 DB 中,redis 内存数据库在这里是是不错的选择。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。
  • 黑名单机制:和上面的方式类似,使用内存数据库比如 redis 维护一个黑名单,如果想让某个 token 失效的话就直接将这个 token 加入到黑名单即可。然后,每次使用 token 进行请求的话都会先判断这个 token 是否存在于黑名单中。
  • 修改密钥 (Secret) : 我们为每个用户都创建一个专属密钥,如果我们想让某个 token 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大,比如:1.如果服务是分布式的,则每次发出新的 token 时都必须在多台机器同步密钥。为此,你需要将必须将机密存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。2.如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
  • 保持令牌的有效期限短并经常轮换 :很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。

对于修改密码后 token 还有效问题的解决还是比较容易的,说一种我觉得比较好的方式:使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。

4.5 JWT

4.5.1 JWT 组成

JWT(JSON Web Token) 也是 token。

JWT 本质上就一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。

JWT 由 3 部分构成:

  1. Header : 描述 JWT 的元数据。定义了生成签名的算法以及 Token 的类型。
  2. Payload(负载): 用来存放实际需要传递的数据,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露密码。
  3. Signature(签名):将头部与载荷分别采用 base64 编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

在基于 Token 进行身份验证的的应用程序中,服务器通过 Payload、Header 和一个密钥(secret)创建令牌(Token)并将 Token 发送给客户端,客户端将 Token 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:Authorization: Bearer Token

jwt

4.5.2 安全性分析

从 JWT 生成的 token 组成上来看,要想避免 token 被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的 base64 编码,几乎是透明的,毫无安全性可言,那么最终守护 token 安全的重担就落在了加入的盐上面了,试想,如果生成 token 所用的盐与解析 token 时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析 token,就能用来伪造 token。 这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成 token 与校验 token 方所用的盐不一致的安全效果!

注意:加盐的意思就是让味道改变,也就是让通过加盐来提高 token 的复杂度,让 token 更加安全,这个盐你可以任意指定,全凭自己和项目需求。

6. OAuth

OAuth 2.0

7. SSO

单点登录全称 Single Sign On(简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录。

相比于单系统登录,SSO 需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,SSO 认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。

难以理解?看图举个例子:

sso

图上有三个参与者,客户端、业务系统、单点登录系统。

当客户端访问 A 系统的接口时,业务系统发现 serviceToken(也称服务器 token)不合法,就跳转到登录界面。

然后用户输入账户密码去访问登录接口,注意访问登录接口的时候携带了一个 callback 的参数,callback 参数为一个回调 url(sts 接口),这个 url 中还包含 followup 参数,followup 参数为登录之前的界面。

然后认证系统对账号密码进行判断,若账号密码正确的话,则生成 authToken(又称 passToken,用户 token),然后携带着这个 authToken 跳转到 callback 指定的 url。

然后业务系统对 authToken 进行检验,检验通过的话,则生成此系统对应 serviceToken,写入 cookie,然后重定向至 followup 中记录的登录前的页面。此页面 对 serviceToken 进行检验,若合法,则访问成功。

当使用系统 B的时候,直接用 authToken 去访问系统 B 的接口生成其系统对应的 serviceToken,即可实现一个系统登陆,多个系统可用。

介绍几个名词:

  • authToken:可以理解为用户 token,可以拿着这个 token 去生成免登陆的 serviceToken
  • serviceToken:即携带用户信息的 token,就是登陆的时候携带的 token
  • Callback:获取完 authToken 的回调,地址为一个接口,这个接口用来生成对应服务的 serviceToken
  • Followup:callback 中携带的地址,即为授权成功以后跳转到的地方
  • sts 接口:security token server,单点登录系统重定向至业务服务器的 sts 接口生成 serviceToken