名字都是用来唬人的。javascript
先解释两个名词,CSRF 和 JWT。html
CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,其中一个页面经过窃取另外一个页面的 cookie 来发送伪造的请求,由于 cookie 是随着请求自动发送到服务端的。前端
JWT (JSON Web Token),经过某种算法将两个 JSON 对象加密成一个字符串,该字符串能表明惟一用户。java
首先经过一个图来理解 CSRF 是什么现象。nginx
想要攻击成功,这三步缺一不可。web
第一,登陆受害者网站。若是受害者网站是基于 cookie 的用户验证机制,那么当用户登陆成功后,浏览器就会保存一份服务端的 SESSIONID。算法
第二,这时候在同一个浏览器打开攻击者网站,虽说它没法获取 SESSIONID 是什么(由于设置了 http only 的 cookie 是没法被 JavaScript 获取的),可是从浏览器向受害者网站发出的任何请求中,都会携带它的 cookie,不管是从哪一个网站发出。数据库
第三,利用这个原理,在攻击者网站发出一个请求,命令受害者网站进行一些敏感操做。因为此时发出的请求是处于 session 中的,因此只要该用户有权限,那么任何请求都会被执行。express
好比,打开优酷,并登陆。再打开攻击者网站,它里面有个 <img>
标签是这样的:npm
<img src="http://api.youku.com/follow/123" />
这个 api 只是个例子,具体的 url 和参数均可以经过浏览器的开发者工具(Network 功能)事先肯定。假如它的做用是让该登陆的用户关注由 123 肯定的一个节目或者用户,那么经过 CSRF 攻击,这个节目的关注量就会不断上升。
解释两点。第一,为何举这个例子,而不是银行这种和金钱有关的操做?很简单,由于它容易猜。对于攻击者来讲,没有什么是必定能成功的,好比 SQL 注入,攻击者他不知道某网站的数据库是怎么设计的,可是他通常会经过我的经验去尝试,好比不少网站把用户的主键设置为 user_id,或 sys_id 等。
银行的操做每每通过多重确认,好比图形验证码、手机验证码等,光靠 CSRF 完成一次攻击基本上是天方夜谭。但其余类型的网站每每不会刻意去防范这些问题。虽然金钱上的利益很可贵到,但 CSRF 能办到的事情仍是不少,好比利用别人发虚假微博、加好友等,这些都能对攻击者产生利益。
第二,如何确保用户打开优酷以后,又打开攻击者网站?作不到。不然任何人打开优酷以后,都会莫名其妙地去关注某个节目了。可是你要知道,这个攻击成本仅仅是一条 API 调用而已,它在哪里都能出现,你从任何地方下载一张图片,让你请求这个地址,看也不看就点肯定,请求不就发出去了吗?
对于如何防范 CSRF,通常有三种手段。
这个字段记录的是请求的来源。好比 http://www.example.com 上调用了百度的接口 http://api.map.baidu.com/service 那么在百度的服务端,就能够经过 Referer 判断这个请求是来自哪里。
在实际应用中,这些跟业务逻辑无关的操做每每会放在拦截器中(或者说过滤器,不一样技术使用的名词可能不一样)。意思是说,在进入到业务逻辑以前,就应该要根据 Referer 的值来决定这个请求能不能处理。
在 Java Servlet 中能够用 Filter(古老的技术);用 Spring 的话能够建拦截器;在 Express 中是叫中间件,经过 request.get('referer') 来取得这个值。每种技术它走的流程其实都同样。
但要注意的是,Referer 是浏览器设置的,在浏览器兼容性大不相同的时代中,若是存在某种浏览器容许用户修改这个值,那么 CSRF 漏洞依然存在。
讨论 GET 和 POST 两种请求,对于 GET,其实也没什么须要防范的。为何?由于 GET 在“约定”当中,被认为是查询操做,查询的意思就是,你查一次,查两次,无数次,结果都不会改变(用户获得的数据可能会变),这不会对数据库形成任何影响,因此不须要加其余额外的参数。
因此这里要提醒各位的是,尽可能听从这些约定,不要在 GET 请求中出现 /delete, /update, /edit 这种单词。把“写”操做放到 POST 中。
对于 POST,服务端在建立表单的时候能够加一个隐藏字段,也是经过某种加密算法获得的。在处理请求时,验证这个字段是否合法,若是合法就继续处理,不然就认为是恶意操做。
<form method="post" action="/delete"> <!-- 其余字段 --> <input type="hidden" name="csrftoken" value="由服务端生成"/> </form>
这个 html 片断由服务端生成,好比 JSP,PHP 等,对于 Node.js 的话能够是 Jade 。
这的确是一个很好的防范措施,再增长一些处理的话,还能防止表单重复提交。
但是对于一些新兴网站,不少都采用了“单页”的设计,或者退一步,不管是否是单页,它的 HTML 多是由 JavaScript 拼接而成,而且表单也都是异步提交。因此这个办法有它的应用场景,也有局限性。
思想是,将 token 放在请求头中,服务端能够像获取 Referer 同样获取这个请求头,不一样的是,这个 token 是由服务端生成的,因此攻击者他没办法猜。
这篇文章的另外一个重点——JWT——就是基于这个方式。抛开 JWT 不谈,它的工做原理是这样的:
解释一下这四个请求,类型都是 POST 。
经过 /login 接口,用户登陆,服务端传回一个 access_token,前端把它保存起来,能够是内存当中,若是你但愿用来模拟 session 的话。也能够保存到 localStorage 中,这样能够实现自动登陆。
调用 /delete 接口,参数是某样商品的 id。仔细看,在这个请求中,多了一个名为 Authoriaztion 的 header,它的值是以前从服务端传回来的 access_token,在前面加了一个“Bearer”(这是和服务端的约定,约定就是说,说好了加就一块儿加,不加就都不加……)
调用 /logout 接口,一样把 access_token 加在 header 中传过去。成功以后,服务端和前端都会把这个 token 置为失效,或直接删除。
再调用 /delete 接口,因为此时已经没有 access_token 了,因此服务端判断该请求没权限,返回 401 。
各位有没有发现,从头到尾,整个过程没有涉及 cookie,因此 CSRF 是不可能发生的!
若是不关心 JWT,那文章彻底能够结束了,由于看到这里,除了章节标题提到的内容以外,各位还能够引伸出几点:第一,在设计 API 时多斟酌一下;第二,利用 token 作单点登陆;第三,cookie 和 token 这两种用户验证机制的不一样。
而 JWT,其实就是对新增的 HTTP Header 的约定。就好比 GET 请求中的参数,约定了用 & 分隔,可是用别的能够吗?固然能够,你用 逗号 或者 分号 也行啊,服务端再规定一个转义的规则就好了。只不过,约定是为了让全部人更规范地作事情,若是按照约定行事的话,那从一个工具换到另外一个工具,本身须要改的代码就不多。这里就不深刻谈了。
这个网站 对 JWT 的术语和内容有最官方的说明。
JWT 的每一个部分都是字符串,由 点 分隔,因此它的格式是这样的:
XXX1.XXX2.XXX3
整个字符串是 URL-safe 的,因此能够直接用在 GET 请求的参数中。
它是一个 JSON 对象,表示这个整个字符串的类型和加密算法,好比
{ "typ":"JWT", "alg":"HS256" }
通过 base64url 加密以后变成
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
它也是一个 JSON 对象,能惟一表示一个用户,好比
{ "iss": "123", "exp": 1441593850 }
通过 base64url 加密以后变成
eyJpc3MiOiIxMjMiLCJleHAiOjE0NDE1OTM4NTB9
在官网有详细的属性说明,尽可能使用里面提到的 Registered Claim Names,这样能够提升阅读性。这里的 iss 表示 issuer,就是发起请求的人,它的值是跟业务相关的,因此由你的应用去决定。exp 表示 expiration time,即何时过时,注意,这个值是秒数,而不是毫秒数,因此是在整型范围内的。
这个签名的计算跟第一部分中的 alg 属性有关,假如是 HS256,那么服务端须要保存一个私钥,好比 secret 。而后,把第一部分和第二部分生成的两个字符串用 点 链接以后,再结合私钥,用 HS256 加密能够获得以下字符串
AOtbon6CebgO4WO9iJ4r6ASUl1pACYUetSIww-GQ72w
如今就集齐三个部分了,用 .
链接,获得完整的 token 。
对于服务端来讲,已经存在各类库去支持 JWT 了,推荐几个以下:
平台 | 库 |
---|---|
Java | maven com.auth0 / java-jwt / 0.4 |
PHP | composer require lcobucci/jwt |
Ruby | gem install jwt |
.NET | Install-Package System.IdentityModel.Tokens.Jwt |
Node.js | npm install jsonwebtoken |
若是以前有 Node.js 和 Express 的学习经历的话,那对下面的代码应该很容易理解。
var express = require('express'), jwt = require('jsonwebtoken'); var router = express.Router(), PRIVATE_KEY = 'secret'; router.post('/login', function(req, res, next) { // 生成 JWT var token = jwt.sign({ iss: '123' }, PRIVATE_KEY, { expiresInMinutes: 60 }); // 将 JWT 返回给前端 res.send({ access_token: token }); }); router.post('/delete', function(req, res, next) { var auth = req.get('Authorization'), token = null; // 判断请求头中是否有 Authoriaztion 字段,为了缩短代码就减小了别的验证 if (auth) { token = /Bearer (.+)/.exec(auth)[1]; res.send(jwt.decode(token)); } else { res.sendStatus(401); } });
关于 jsonwebtoken 的使用能够看它的手册。
例子中定义了两个 API。
/login,会返回一个 JWT 字符串。其中包含了一个用户 id,和存活时间,这个时间会被转换成 exp 和 iat (issue at, 发起请求的时间),二者之差就是存活时间。
/delete,验证请求头中是否有 Authorization 字段,而且是否合法,若是是的话就处理请求,不然返回 401 。
注意一下,服务端期待的 Authoriaztion 请求头是这样的格式:
Authorization: Bearer XXX1.XXX2.XXX3
这个跟 JWT 无关,是 OAuth 2.0 的一种格式。由于 Authorization 这个字段也是约定的,它由 token 的类型和值组成,类型除了上文提到的 Bearer,还有 Basic、MAC 等。
前端的工做分两方面,一是存储 jwt,二是在全部的请求头中增长 Authoriaztion 。
若是是重构已有的代码,第二个工做可能有点难度,除非旧代码中的表单都是异步提交,而且请求的方法是本身包装过的,由于只有这样才有机会去修改请求头。
在若干星期以前的这篇文章中,写了怎么在 Angular 中拦截请求。如今就以 Backbone 为例。
// 先保存原始的 sync 方法 var sync = Backbone.sync; Backbone.sync = function (method, model, options) { var token = window.localStorage.getItem('jwt'); // 若是存在 token,就把它加到请求头中 if (token) { options.headers.Authorization = 'Bearer ' + token; } // 调用原始的 sync 方法 sync(method, model, options); };
在跨域的应用场景中,须要服务端作一些额外的设置,这些设置是加在响应头上的。
Access-Control-Allow-Origin: * Access-Control-Allow-Headers: Authorization
第一个表示容许来自任何域名的请求。第二个表示容许一些自定义的请求头,由于 Authoriaztion 是自定义的,因此必须加上这个配置,若是各位使用了其余的请求头,请一样加上。
若是服务端用了 nginx,那么这些配置能够写在 nginx.conf 文件中。若是是在代码中配置,那么不管是 Java,仍是 Node.js,都有 response.setHeader 方法。
我对 Web 安全方面的了解还不太深,因此没有太多经验可谈。安全性是一个在日常不太受重视的领域,由于完成一个项目的优先级历来都是:功能 > 颜值 > 性能, 安全 。至少得保证用户在使用过程当中不会出错,而后再作得酷炫或清新一点,性能和安全只有在知足了前两项,或者迫在眉睫的时候才去考虑。当服务器承受不了那么高的负载了,才会去增长更多的服务器,但业务功能从一开始就不能少。
但是这样作有错吗?并无吧。在特定的场景,作特定的处理,或许是性价比最高的决策了。
这篇文章中反复提到的一个词是“约定”,它貌似和“具体状况具体分析”这个观点矛盾了,额……。
约定是人与人之间的共识,好比说 GET 请求,那么对方的第一反应就是查询,当有人破坏约定,用 GET 请求去作删除操做时,就会让别人很难理解(当有一大堆人这么作的时候,就不难理解了吧……)。或者当咱们提到 JWT 的时候,那它就应该是由三个部分组成,若是有人仅仅是按照本身的算法来生成一个 token,一样能够惟一标识用户,那他必须得像共事的人解释,这个算法的安全性、使用方法等。
另外一方面,若是真心以为按照“约定”办事不必,太麻烦,而且能够接受“耍小聪明”的后果的话,那就按本身的想法去作吧(真的再也不考虑一下了吗)。
为何 HTML5 新增了那么多语义化的标签,是由于一切都在朝着更规范的方向走。