基于 Token 的身份验证,很多大型网站也都在用,比如 Facebook,Twitter,Google+,Github 等等,比起传统的身份验证方法,Token 扩展性更强,也更安全点,非常适合用在 Web 应用或者移动应用上
传统身份验证的方法
HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下
解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端
上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session
基于 Token 的身份验证方法
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
优点
支持跨域跨站点访问:
Cookie 是不允许垮域访问的,可以通过设置顶级域名的方式实现部分跨域,但是跨站点的访问仍然不支持,
如果使用 Token 机制,就可以通过 HTTP 头传输用户认证信息,从而更好的实现跨域跨站点
去耦:
不需要绑定到一个特定的身份验证方案。Token 可以在任何地方生成,只要在你的 API 被调用的时候,你可以进行 Token 生成调用即可
更适用于移动应用:
当客户端是原生应用时,Cookie 是不被支持的,虽然目前 Webview 的方式可以解决 Cookie 问题,但是显然采用 Token 认证机制会简单得多
安全性更强:
XSS 攻击的原理是,攻击者插入一段可执行的 JavaScript 脚本,该脚本会读出用户浏览器的 cookies 并将它传输给攻击者,攻击者得到用户的 Cookies 后,即可冒充用户。但是要防范 XSS 也很简单,在写入 cookies 时,将 HttpOnly 设置为 true,客户端 JavaScript 就无法读取该 cookies 的值,就可以有效防范 XSS 攻击。因为 Tokens 也是储存在本地的 session storage 或者是客户端的 cookies 中,也是会受到 XSS 攻击
相比 XSS,CSRF 的危害性更大,因为大多数 Web 框架都已经内置了 XSS 防范机制(例如在 Ruby on Rails 中,用户的输入在输出的时候都会做转义操作,攻击者插入的脚本就无法执行),对于大部分开发者而言,甚至连 CSRF 都不知道是什么玩意,更别提防范了。CSRF 目前并不是每个 Web 框架都有防范机制,因此开发者更应该留意 CSRF
因为不再依赖于 Cookie,所以就不需要考虑对 CSRF(跨站请求伪造)的防范
即使存放在 Cookie,也只是把 cookie 当作一个储存机制,而不是一种验证机制。比如说,这个 cookie 不会被 Web 框架用于用户验证,所以没有 CSRF 攻击的危险
标准化易扩展:
可以采用标准化的 JSON Web Token (JWT),对以后系统接入 Node 等纯前端开发更便捷
相比 Session 一致性提高性能:
- 相比服务端保存 Session 一致性信息,并查询用户登录状态,一般来说 Token 的验证过程(包含加密和解密),性能开销会更小
- 基于 Token 的身份验证是无状态的,不用将用户信息存在服务器或 Session 中
- 相比原始的 Cookie + Session 方式,更适合分布式系统的用户认证,绕开了传统的分布式 Session 一致性等问题
JWT
实施 Token 验证的方法挺多的,还有一些标准方法,比如 JWT,读作:jot,表示:JSON Web Tokens 。JWT 标准的 Token 有三个部分:
- header
- payload
- signature
中间用点分隔开,并且都会使用 Base64 编码,所以真正的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
header 部分主要是两部分内容,一个是 Token 的类型,另一个是使用的算法,比如下面类型就是 JWT,使用的算法是 HS256
{
"typ": "JWT",
"alg": "HS256"
}
上面的内容要用 Base64 的形式编码一下,所以就变成这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:
- iss:Issuer,发行者
- sub:Subject,主题
- aud:Audience,观众
- exp:Expiration time,过期时间
- nbf:Not before
- iat:Issued at,发行时间
- jti:JWT ID
比如下面这个 Payload ,用到了 iss 发行人,还有 exp 过期时间。另外还有两个自定义的字段,一个是 name ,还有一个是 admin
{
"iss": "ninghao.net",
"exp": "1438955445",
"name": "wanghao",
"admin": true
}
使用 Base64 编码以后就变成了这个样子:
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
不应该在 Payload 里面加入任何敏感的数据,如用户密码等信息,因为 Payload 并没有做加密,只是一个 Base64 的编码,攻击者拿到 token 以后就可以得到用户敏感信息
Signature
JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64 编码的 header.payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
HMACSHA256(encodedString, 'secret');
处理完成以后看起来像这样:
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源
JWT 认证的实现
常规的 token 保存在 sessionStorage 或者 localStorage 中,每次请求时将 token 加在 http 请求的 Header 中,下面是典型的 token 认证方式:
- 客户端登录时通过账号和密码到服务端进行认证,认证通过后,服务端通过持有的密钥生成 Token,Token 中一般包含失效时长和用户唯一标识,如用户 ID,服务端返回 Token 给客户端
- 客户端保存服务端返回的 Token
- 客户端进行业务请求时在 Head 的 Authorization 字段里面放置 Token
- 服务端对请求的 Token 进行校验,如果 Token 不是存放在 Cookie 中,需要解决用户主动注销,但设置的过期时间并未过期问题;用户注销时可以把还在失效内的 Token 储存在 Redis 等缓存中,验证时查找 Token 是否存在,如果 Token 在 Redis 中存在,则说明用户已注销;如果 Token 不存在,则校验通过
- 服务端可以通过从 Token 取得的用户唯一标识进行相关权限的校验,并把此用户标识赋予到请求参数中,业务可通过此用户标识进行业务处理
还有一种方式是把 token 保存在 Cookie 中,这时就不需要在服务端保存 token 的值,用户注销时直接清除 Cookie 就可以,这种方式不需要在服务端储存 token 的值,认证过程如下:
如何防范 MITM(Man-In-The-Middle)Attacks
所谓 MITM 攻击,就是在客户端和服务器端的交互过程被监听,比如像可以上网的咖啡馆的 WIFI 被监听或者被黑的代理服务器等;
针对这类攻击的办法使用 HTTPS,包括针对分布式应用,在服务间传输像 cookie 这类敏感信息时也采用 HTTPS;所以云计算在本质上是不安全的