CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,个中一个页面通过盗取另一个页面的 cookie 来发送假造的要求,由于 cookie 是随着要求自动发送到做事真个。

JWT (JSON Web Token),通过某种算法将两个 JSON 工具加密成一个字符串,该字符串能代表唯一用户。

CSRF 的产生

首先通过一个图来理解 CSRF 是什么征象。

jsp把token放在请求头中若何经由过程JWT防御CSRF JavaScript

想要攻击成功,这三步缺一不可。

第一,登录受害者网站。
如果受害者网站是基于 cookie 的用户验证机制,那么当用户登录成功后,浏览器就会保存一份做事真个 SESSIONID。

第二,这时候在同一个浏览器打开攻击者网站,虽然说它无法获取 SESSIONID 是什么(由于设置了 http only 的 cookie 是无法被 JavaScript 获取的),但是从浏览器向受害者网站发出的任何要求中,都会携带它的 cookie,无论是从哪个网站发出。

第三,利用这个事理,在攻击者网站发出一个要求,命令受害者网站进行一些敏感操作。
由于此时发出的要求是处于 session 中的,以是只要该用户有权限,那么任何要求都会被实行。

比如,打开优酷,并登录。
再打开攻击者网站,它里面有个 <img> 标签是这样的:

<img src=\"大众http://api.youku.com/follow/123\"大众 />

这个 api 只是个例子,详细的 url 和参数都可以通过浏览器的开拓者工具(Network 功能)事先确定。
如果它的浸染是让该登录的用户关注由 123 确定的一个节目或者用户,那么通过 CSRF 攻击,这个节目的关注量就会不断上升。

阐明两点。
第一,为什么举这个例子,而不是银行这种和金钱有关的操作?很大略,由于它随意马虎猜。
对付攻击者来说,没有什么是一定能成功的,比如 SQL 注入,攻击者他不知道某网站的数据库是怎么设计的,但是他一样平常会通过个人履历去考试测验,比如很多网站把用户的主键设置为 user_id,或 sys_id 等。

银行的操作每每经由多重确认,比如图形验证码、手机验证码等,光靠 CSRF 完成一次攻击基本上是天方夜谭。
但其他类型的网站每每不会刻意去戒备这些问题。
虽然金钱上的利益很难得到,但 CSRF 能办到的事情还是很多,比如利用别人发虚假微博、加好友等,这些都能对攻击者产生利益。

第二,如何确保用户打开优酷之后,又打开攻击者网站?做不到。
否则任何人打开优酷之后,都会莫名其妙地去关注某个节目了。
但是你要知道,这个攻击本钱仅仅是一条 API 调用而已,它在哪里都能涌现,你从任何地方下载一张图片,让你要求这个地址,看也不看就点确定,要求不就发出去了吗?

CSRF 的防御

对付如何戒备 CSRF,一样平常有三种手段。

判断要求头中的 Referer

这个字段记录的是要求的来源。
比如 http://www.example.com 上调用了百度的接口 http://api.map.baidu.com/service 那么在百度的做事端,就可以通过 Referer 判断这个要求是来自哪里。

在实际运用中,这些跟业务逻辑无关的操作每每会放在拦截器中(或者说过滤器,不同技能利用的名词可能不同)。
意思是说,在进入到业务逻辑之前,就该当要根据 Referer 的值来决定这个要求能不能处理。

在 Java Servlet 中可以用 Filter(古老的技能);用 Spring 的话可以建拦截器;在 Express 中是叫中间件,通过 request.get('referer') 来取得这个值。
每种技能它走的流程实在都一样。

但要把稳的是,Referer 是浏览器设置的,在浏览器兼容性大不相同的时期中,如果存在某种浏览器许可用户修正这个值,那么 CSRF 漏洞依然存在。

在要求参数中加入 csrf token

谈论 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 拼接而成,并且表单也都是异步提交。
以是这个办法有它的运用处景,也有局限性。

新增 HTTP Header

思想是,将 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 的约定

如果不关心 JWT,那文章完备可以结束了,由于看到这里,除了章节标题提到的内容之外,各位还可以引申出几点:第一,在设计 API 时多推敲一下;第二,利用 token 做单点登录;第三,cookie 和 token 这两种用户验证机制的不同。

而 JWT,实在便是对新增的 HTTP Header 的约定。
就比如 GET 要求中的参数,约定了用 & 分隔,但是用别的可以吗?当然可以,你用 逗号 或者 分号 也行啊,做事端再规定一个转义的规则就行了。
只不过,约定是为了让所有人更规范地干工作,如果按照约定行事的话,那从一个工具换到另一个工具,自己须要改的代码就很少。
这里就不深入谈了。

三个组成部分

这个网站 对 JWT 的术语和内容有最官方的解释。

JWT 的每个部分都是字符串,由 点 分隔,以是它的格式是这样的:

XXX1.XXX2.XXX3

全体字符串是 URL-safe 的,以是可以直接用在 GET 要求的参数中。

第一部分 JWT Header

它是一个 JSON 工具,表示这个全体字符串的类型和加密算法,比如

{ \公众typ\"大众:\公众JWT\"大众, \"大众alg\"大众:\公众HS256\"大众}

经由 base64url 加密之后变成

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9

第二部分 JWT Claims Set

它也是一个 JSON 工具,能唯一表示一个用户,比如

{ \"大众iss\"大众: \"大众123\"大众, \公众exp\公众: 1441593850}

经由 base64url 加密之后变成

eyJpc3MiOiIxMjMiLCJleHAiOjE0NDE1OTM4NTB9

在官网有详细的属性解释,只管即便利用里面提到的 Registered Claim Names,这样可以提高阅读性。
这里的 iss 表示 issuer,便是发起要求的人,它的值是跟业务干系的,以是由你的运用去决定。
exp 表示 expiration time,即什么时候过期,把稳,这个值是秒数,而不是毫秒数,所以是在整型范围内的。

第三部分 JWS Signature

这个署名的打算跟第一部分中的 alg 属性有关,如果是 HS256,那么做事端须要保存一个私钥,比如 secret 。
然后,把第一部分和第二部分天生的两个字符串用 点 连接之后,再结合私钥,用 HS256 加密可以得到如下字符串

AOtbon6CebgO4WO9iJ4r6ASUl1pACYUetSIww-GQ72w

现在就集齐三个部分了,用 . 连接,得到完全的 token 。

例子 1/2:以 Express 作为做事端

对付做事端来说,已经存在各种库去支持 JWT 了,推举几个如下:

平台库Javamaven com.auth0 / java-jwt / 0.4PHPcomposer require lcobucci/jwtRubygem install jwt.NETInstall-Package System.IdentityModel.Tokens.JwtNode.jsnpm 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 等。

例子 2/2:以 Backbone 作为前端

前真个事情分两方面,一是存储 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 新增了那么多语义化的标签,是由于统统都在朝着更规范的方向走。