分类
技术

简析HTTP认证技术

引言

本文简要探讨了在设计 Web 认证方案尤其是 REST API 时所涉及的技术以及常见问题的解决方案,重点关注 CSRF 和 XSS 攻击的原理和应对措施。最后附带介绍了 CORS 协议的基本内容。

Web 认证

网站为了验证用户的合法性,需要用户输入用户名密码以登录到系统中,用户名和密码的传输要在 SSL 的保护下进行,这个自不必多说。最主要的问题是,用户只会输入一次密码,不可能要求用户向系统发出的每一个请求都附上密码,也不能将密码保存在本地,因为这会造成密码丢失的风险。那么如何在后续的会话中既能持续地识别用户,又能保证用户的安全信息不被泄露呢?

常见的 Web 服务器是这样做的:在成功验证完用户的密码后,将该用户的身份信息用服务器才有的密钥进行加密,然后在给客户端返回时,使用 Set-Cookie 头将加密过的信息设置到 Cookie 中。以后每一次发送请求时,浏览器会自动附上 Cookie 中的信息,服务器即可解密 Cookie 获取到用户身份信息。

由此可见,只要得到了 Cookie 中的用户身份信息,即相当于拥有了用户的授权,因此对 Cookie 的安全保护变得十分重要。

虽然服务器可以为 Cookie 设置过期时间,但仅有这样是远远不够的,比如下面的这个例子:

  1. 用户丘某使用浏览器登录了某家银行的网站(www.bank.com),银行服务器返回他的身份信息并保存在 Cookie 中
  2. 丘某没有退出银行网站,又去访问了陈某开发的一个网站
  3. 陈某的这个网站是一个恶意网站,其在页面中加入了一个不可见的图片:<img src=”www.bank.com/transfer/1000/to/chen” />,用户在加载这个图片时,实际访问的是银行的网站,并附带了在该银行的 Cookie 信息
  4. 用户不知情的情况下将钱转给了别人,可能过好久才回发现

这种攻击方式,叫做跨站请求伪造(CSRF)。有人可能会问,浏览器的同源策略难道不会禁止这种行为吗?首先,同源机制并不是所有请求都保护,对于像图片这样的静态资源是不受保护的;其次,即便有同源机制,也需要服务端的配合,关于跨域资源共享的内容,后面会讲到。这里先来谈谈应对 CSRF 的常见措施:

  1. 服务端检查请求头部的 Origin 和 Referer,确保请求来自本站点
  2. 服务端每次给客户端返回数据时,附带一个随机的 token,并要求客户端下次提交表单时一并提交这个 token,很多 Web 框架会自动做这件事,对开发者透明

对用户来说,不浏览不安全的网站、在公共网络登录后离开时清除 Cookie 也可降低受攻击的风险

说完了 CRSF,再简单聊一下另一种常见的攻击方式:跨站脚本攻击(XSS)。黑客为实现这种攻击,首先在某个可以发布内容的网站(如博客)上发布一篇文章,其中包含一段可执行的 js 脚本,如果网站对其提交的内容不做校验的话,当某个其它的用户访问黑客所发布的这篇文章时,便会执行这段代码。这段代可能会读取用户的 Cookie 并发送到另一个事先准备好的网站,从而实现 Cookie 的窃取。

应对 XSS 攻击,除了要对用户的输入进行安全校验,还可以将 Cookie 设置为 HttpOnly 以禁止通过脚本读取,但这无法防止黑客读取用户 Local Storage 中的内容。此外,网站所使用的第三方库也要做安全上的防范

REST 认证

REST 认证和普通的 Web 认证有很多不同:

  1. 用户不一定处在浏览器环境中,无法使用 Cookie
  2. 如果是分布式或多服务环境,用户的验证信息需要在多方传递,无法集中管理
  3. 可能需要支持 CORS

目前常用的解决方案是在用户登录后,服务器为用户生成一个 token,里面编码了用户的身份信息,此后用户的每个请求都要附上这个 token 以实现验证。提交token 时,应将 token 放在头部的 Authorization 字段中,有开发者将 access_token 放在 URL 后面,这样不仅不符合规范,还会使得用户 token 出现在书签、历史、分享链接、服务器日志等地方,很容易泄露。关于 token 的生成,常用的算法是 JWT(Json Web Token),这是一个很灵活的标准,支持加密设置加密算法,也不限编码内容,但值得注意的是,JWT 并不真正加密数据,而是只做了签名,即服务器可以保证这个 token 的合法性以及不可篡改性,但其他人可以看到 token 中的内容,所以不可在 JWT 中放置私密信息。

除了 JWT,还有其它验证的方式,在安全性更高的环境中,可以事先给用户发布一个 key,之后会话时,每个请求体都要用该 key 进行签名。因为 key 并不需要在网络上传输,所以降低了丢失的可能性。微信开发平台的第三方平台,结合了两种加密方式,首先要用 key 解密微信推送来的认证信息,再拿着这个信息去获取 access_token,之后的请求只附带 access_token 进行验证,access_token 会很快过期,需要用 key 和 refresh_token 重新获取,所以 access_token 泄露带来的损失是有限的。

不过,通常环境下,还是使用 token 的方式较多,我们接下来只探讨这个。

JWT 中可以编码自定义信息,可以很方便地实现用户会话管理,但也会带来一些安全性问题,比如将账户余额放在 JWT 中,则会带来请求重放的风险,用户余额 20 块,他拿着这个包含“20块”信息的 token,可以一直重复消费,如果服务器不做校验的话。此外,虽然可以在 token 中编码时间戳以实现过期,但是很难实现如黑名单、退出登录等功能,所以我强烈建议只在 token 中编码用户标识,而对于 token 的合法性以及业务逻辑的校验,都要放在服务端进行。

如果客户端是浏览器,还要考虑 token 的存储问题,选择 Cookie 或 Local Storage,会分别面临 CSRF 和 XSS 的安全风险,实施时要有相应的对策

最后,聊一聊跨域资源共享(CORS),这个话题虽然不涉及认证,但也和安全有很大关系,且在目前流行的前后分离架构中会经常遇到。

CORS 的本质目的是提供跨域资源共享,但为了避免安全隐患,做出了许多限制。首先,一些请求被归为“简单请求”,这类请求的方法只能为 GET、HEAD 或 POST,且可用的头部也是有限的,如果是 POST 方法,头部的 Content-Type 也有限制,具体的约束可以查阅这篇文章(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)。对于简单请求,浏览器会直接发送给跨域网站,但是会对返回的数据做检查,确保响应数据中的 Access-Control-Allow-Origin 包含源站点,否则会出现网络日志中请求成功发送了,返回结果也有了,但浏览器还是报跨域,也不会把返回数据提供给调用处。

遇到简单请求以外的其它请求,浏览器会首先发送一个方法为 OPTIONS 的称为“Preflighted request” 的请求,其中明确了将要发送的请求的方法,包含哪些头部等。服务器需要对该方法做出响应,通过指定 Access-Control-Allow-Method、Access-Control-Allow-Origin、Access-Control-Allow-Header 来表明是否支持该请求,如果支持,浏览器才会发送真正的请求,否则报跨域错误。需要注意的是,服务器对真实请求的响应中也要包含 Access-Control-Allow-* 等头部,如果同意了 Preflighted request 但没有同意真实的请求,浏览器还是会丢弃返回的数据。