身份验证的两种方式

身份验证(Authentication)

要想区分来自不同用户的请求的话,服务端需要根据客户端请求确认其用户身份,即身份验证。

在人机交互中,身份验证意味着要求用户登录才能访问某些信息。而为了确认用户身份,用户必须提供只有用户和服务器知道的信息(即身份验证因子),比如用户名 / 密码。

Web 环境下,常见的身份验证方案分为两类:

  • 基于 Cookie / Session 的验证
  • 基于 Token 的验证

用户名 / 密码属于知识因子,另外还有占有因子和遗传因子:

  • 知识因子:用户登录时必须知道的东西都是知识因子,比如用户名、密码等
  • 占有因子:用户登录时必须具备的东西,比如密码令牌、ID 卡等
  • 遗传因子:个人的生物特征,比如指纹、虹膜、人脸等

Authentication(验证)与 Authorization(授权)不同,前者验证身份,后者验证权限。

定义

为了解决 HTTP 无状态的问题,Lou Montulli1994 年的时候,推出了 Cookie

Cookie 指的就是在 Cookie 是服务器端发送给客户端的一段特殊信息,这些信息以文本的方式存放在客户端,客户端每次向服务器端发送请求时都会带上这些特殊信息,Cookie 仅仅是浏览器实现的一种数据存储功能。

有了 Cookie 之后,服务器端就能够获取到客户端传递过来的信息了,如果需要对信息进行验证,还需要通过 Session

客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个便是 Session 对象。

为了解决 Session + Cookie 机制暴露出的诸多问题,我们可以使用 Token 的登录方式。

Token 是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。

由于 HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户,进而也就无法判断用户的登录状态

怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。

当客户端请求服务器,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器颁发一个 Cookie。Cookie 实际上是一小段的特殊信息,客户端浏览器会把这些信息以文本的形式存放在客户端。当浏览器再请求该网站时,浏览器把请求的网址连同该 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。服务器还可以根据需要修改 Cookie 的内容。

Cookie 的保存时间,可以自己在程序中设置。如果没有设置保存时间,默认一关闭浏览器,cookie 就自动消失

注意:Cookie 功能需要浏览器的支持。如果浏览器不支持 Cookie(如大部分手机中的浏览器)或者把 Cookie 禁用了,Cookie 功能就会失效。不同的浏览器采用不同的方式保存 Cookie。IE 浏览器会以文本文件形式保存,一个文本文件保存一个 Cookie。

Cookie 具有不可跨域名性。根据 Cookie 规范,浏览器访问 Google 只会携带 Google 的 Cookie,而不会携带 Baidu 的 Cookie。浏览器判断一个网站是否能操作另一个网站 Cookie 的依据是域名

Session

Session 的工作原理

Session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。

如果说 Cookie 机制是通过检查客户身上的 “通行证” 来确定客户身份的话,那么 Session 机制就是通过检查服务器上的 “客户明细表” 来确认客户身份。

session 也是类似的道理,服务器要知道当前发请求给自己的是谁。为了做这种区分,服务器就要给每个客户端分配不同的 “身份标识”,然后客户端每次向服务器发请求的时候,都带上这个 “身份标识”,服务器就知道这个请求来自于谁了。对于浏览器客户端,大家都默认采用 cookie 的方式,保存这个 “身份标识”。这也就是 Cookie + Session 登录。

服务器使用 session 把用户的信息临时保存在了服务器上,用户离开网站后 session 会被销毁。这种用户信息存储方式相对 cookie 来说更安全

可是 session 有一个缺陷:如果 web 服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候 session 会丢失。

注意:Session 的使用比 Cookie 方便,但是过多的 Session 存储在服务器内存中,会对服务器造成压力。

  • cookie 数据存放在客户的浏览器上,session 数据放在服务器上。
  • cookie 不是很安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE 欺骗,考虑到安全应当使用 session。
  • session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面,应当使用 COOKIE。
  • 单个 cookie 在客户端的限制是 3K,就是说一个站点在客户端存放的 COOKIE 不能超过 3K。

Cookie 和 Session 的方案虽然分别属于客户端和服务端,但是服务端的 session 的实现对客户端的 cookie 有依赖关系的,上面讲到服务端执行 session 机制时候会生成 session 的 id 值,这个 id 值会发送给客户端,客户端每次请求都会把这个 id 值放到 http 请求的头部发送给服务端,而这个 id 值在客户端会保存下来,保存的容器就是 cookie,因此当我们完全禁掉浏览器的 cookie 的时候,服务端的 session 也会不能正常使用。

Cookie+Session实现流程
Cookie+Session 实现流程

用户首次登录时:

  • 1. 用户访问 a.com/pageA,并输入密码登陆。
  • 2. 服务器验证密码无误后,会创建 SessionId,并将它保存起来。
  • 3. 服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。
  • 4. 浏览器会根据 Set-Cookie 中的信息,自动将 SessionID 存储至 Cookie 中。

注意:服务器端的 SessionId 可能存放在很多地方,例如:内存、文件、数据库等。

Cookie+Session后续访问
Cookie+Session 后续访问

第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:

  • 1. 用户访问 a.com/pageB 页面时,会自动带上第一次登录时写入的 Cookie。
  • 2. 服务器端对比 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。
  • 3. 如果一致,则身份验证成功。

虽然我们使用 Cookie + Session 的方式完成了登录验证,但仍然存在一些问题:

  • 由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId,这样会导致服务器压力过大。 - 如果服务器端是一个集群,为了同步登陆状态,需要将 SessionId 同步到每一台机器上,无形中增加了服务器端的维护成本。
  • 由于 SessionId 存放在 Cookie 中,所以无法避免 CSRF 攻击 (跨站请求伪造)。

基于 token 的登陆认证

在大多数使用 Web API 的互联网公司中,tokens 是多用户下处理认证的最佳方式。

以下几点特性会让你在程序中使用基于 Token 的身份验证

  • 无状态、可扩展
  • 支持移动设备
  • 跨程序调用
  • 安全

Session 方案中用户身份信息(以 Session 记录形式)存储在服务端。而 Token 方案中(以 Token 形式)存储在客户端,服务端仅验证 Token 合法性。这种区别在单点登录(SSO,Single Sign On)的场景最为明显:

  • 基于 Session 的 SSO:考虑如何同步 Session 和共享 Cookie。比如登录成功后把响应 Cookie 的 domain 设置为通配兄弟应用域名的形式,并且所有应用都从身份验证服务同步 Session。
  • 基于 Token 的 SSO:考虑如何共享 Token。比如进入兄弟应用时通过 URL 带上 Token。

Token 的起源(对以上的总结)

在介绍基于 Token 的身份验证的原理与优势之前,不妨先看看之前的认证都是怎么做的。

  • 基于服务器的验证

我们都是知道 HTTP 协议是无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份。

在这之前,程序都是通过在服务端存储的登录信息来辨别请求的。这种方式一般都是通过存储 Session 来完成。

  • 基于服务器验证方式暴露的一些问题

    1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。

    2. 可扩展性:在服务端的内存中使用 Seesion 存储登录信息,伴随而来的是可扩展性问题。

    3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用 Ajax 抓取另一个域的资源,就可以会出现禁止请求的情况。

    4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。

在这些问题中,可扩展性是最突出的。因此我们有必要去寻求一种更有行之有效的方法。

基于 Token 的验证原理

基于 Token 的身份验证是无状态的,不将用户信息存在服务器中。这种概念解决了在服务端存储信息时的许多问题。NoSession 意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。

Token 验证实现流程:

基于Token的用户身份验证过程
基于 Token 的用户身份验证过程

把这个过程分为两步,步骤图及解析如下:

Token验证实现流程
Token 验证实现流程

用户首次登陆时:

  • 1. 用户输入用户名和密码,并发送请求,访问 a.com/PageA。
  • 2. 服务器端程序验证,账号密码无误,创建 Token。
  • 3. 服务器端程序返回一个带签名的 token 给客户端,客户端储存 token。

后续页面访问时:

Token验证后续访问
Token 验证后续访问

  • 4. 客户端每次访问 a.com/PageB 都携带第一次登录时获取的 Token 到服务器端。
  • 5. 服务端验证 token,校验成功则返回请求数据,校验失败则返回错误码。

Tokens 的优势 / 特点

  • 无状态、可扩展

在客户端存储的 Tokens 是无状态的,并且能够被扩展。基于这种无状态和不存储 Session 信息,所以不会对服务器端造成压力,负载均衡器能够将用户信息从一个服务器传到其他服务器上,即使是服务器集群,也不需要增加维护成本。

  • 安全性

请求中发送 Token 而不是发送 Cookie,能够防止 CSRF(跨站请求伪造)。即在客户端使用 Cookie 存储 Tooken,Cookie 也仅仅是一个存储机制而不是用于认证。不将信息存储在 Seesion 中,让我们少了对 Session 的操作。Token 也可以存放在前端任何地方,可以不用保存在 Cookie 中,提升了页面的安全性。

Token 是有时效的,一段时间之后用户需要重新验证。

  • 可扩展性

Token 能够创建与其他程序共享权限的程序。

  • 多平台跨域

前端在项目中使用 Token

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  • 1. 前端使用用户名跟密码请求首次登录。
  • 2. 后服务端收到请求,去验证用户名与密码是否正确。
  • 3. 验证成功后,服务端会根据用户 id、用户名、定义好的秘钥、过期时间生成一个 Token,再把这个 Token 发送给前端。
  • 4. 前端收到 返回的 Token ,把它存储起来,比如放在 Cookie 里或者 Local Storage 里。
1
2
3
4
5
6
export interface User {
token: string;
userInfo: UserInfo | any;
companyInfo: CompanyInfo | any;
resources?: string[];
}
1
2
3
4
5
6
7
8
9
10
11
save(key: string, value: any, storageType ?: StorageType) {
return this.storageService.put(
{
pool: key,
key: 'chris-app',
storageType: StorageType.localStorage
},
value
);
}
this.storageService.save(CACHE_USER_KEY, user);
  • 5. 前端每次路由跳转,判断 localStroage 有无 token ,没有则跳转到登录页。有则请求获取用户信息,改变登录状态。

  • 6. 前端每次向服务端请求资源的时候需要在请求头里携带服务端签发的 Token。

1
2
HttpInterceptor =>
headers = headers.set('token', this.authService.getToken());
  • 7. 服务端收到请求,然后去验证前端请求里面带着的 Token。没有或者 token 过期,返回 401。如果验证成功,就向前端返回请求的数据。

  • 8. 前端得到 401 状态码,重定向到登录页面。

1
2
HttpInterceptor =>
401: '用户登陆状态失效,请重新登陆。'