CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,个中一个页面通过盗取另一个页面的 cookie 来发送假造的要求,由于 cookie 是随着要求自动发送到做事真个。
JWT (JSON Web Token),通过某种算法将两个 JSON 工具加密成一个字符串,该字符串能代表唯一用户。
CSRF 的产生首先通过一个图来理解 CSRF 是什么征象。
想要攻击成功,这三步缺一不可。
第一,登录受害者网站。如果受害者网站是基于 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,那文章完备可以结束了,由于看到这里,除了章节标题提到的内容之外,各位还可以引申出几点:第一,在设计 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 了,推举几个如下:
如果之前有 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 新增了那么多语义化的标签,是由于统统都在朝着更规范的方向走。