Python进/线程池使用

在 Python 中,进程/线程是个非常重要的概念,特别是 Python 还有 GIL(同一时刻只有一个线程在执行 Python bytecode)限制,使得 Python 线程并不那么好用。但 GIL 更多的是影响 CPU 密集型任务,实际业务场景更多的是 IO 密集型任务,多线程还是适用绝大多数场景。不过话又说回来,很多时候不太好判断是 IO 密集型多还是 CPU 密集型多,需要在多进程、多线程环境下分别验证。

但多线程和多进程手写起来还是有点差别,好在 multiprocessing.Pool 提供了统一的接口,可以无缝切换:

下面介绍下 Pool 类如何使用。

首先是构造函数:

class multiprocessing.Pool([processes[, initializer[, initargs[, maxtasksperchild]]]])

接收 processes initializer initargs maxtasksperchild 4个参数:

processes,就是池里有多少个进程,可以不传,默认 CPU 个数,可以按需多设置几倍

initializer、initargs,如果设置了参数,则会在每个进程初始化的时候调用 initializer(*initargs)。这个非常有用,比如可以在初始化的时候建立连接,连接重用。

maxtasksperchild,用于设置每个子进程执行多少个任务后重启。虽然很简单粗暴,但这是防止内存溢出、资源未释放等问题的常见手段,类似 PHP-FPM 的 pm.max_requests 参数。线程池无此参数。

这里有一个 hack 技巧,将初始化的数据库连接,绑定在了请求函数上,这样调用请求函数发送请求时,就不用重新建立连接,直接使用即可。

我们一般使用线程池有两个场景,一是关注执行结果,比如我们并行去 redis mysql 各个地方请求数据,然后整合这些数据,二是不关注执行结果,比如新开一个线程打印一个请求的审计日志,不阻塞主进程返回数据。这里主要是介绍下 apply 和 apply_async 的区别,其它的都类似。

apply(func[, args[, kwds]]),主要是传一个执行函数和参数,阻塞并得到返回结果。

apply_async(func[, args[, kwds[, callback]]]),也是主要传执行函数和参数,但返回的是一个 multiprocessing.pool.AsyncResult 对象,AsyncResult 主要有 get([timeout])、wait([timeout])、ready()、successful() 4 个方法,都很好理解,用的比较多的是 get 方法,给定超时时间内获取执行的结果,如果超时抛出 multiprocessing.TimeoutError 异常。如果 timeout 是 None,则一直就等待,行为就和 apply 一致了,实际上 apply 也是调用的 apply_async get:

apply_async 还有一个有用的参数 callback,相对于异步回调了,一般用于上述的场景二。下面是一些示例:

运行结果:

注意代码的最后一行,这里不仅仅是为了看回调结果,还因为回调是回调到主进程执行,如果主进(线)程已经退出了,那就处理不到回调了,实际使用需要注意运行环境。

最后,multiprocessing 模块的 pool 功能只是其中很小的一部分,但比较实用,后面有新的心得再介绍其它功能。

参考资料:

https://docs.python.org/2.7/library/multiprocessing.html#module-multiprocessing.pool

Python Requests Session set-cookie不生效的坑

我们知道 Python Requests库 中的 Session 模块有连接池和会话管理的功能,比如请求一个登录接口后,会自动处理 response 中的 set-cookie,下次再请求时会自动把 cookie 带上。但最近出现了一个诡异的事情,cookie 没有自动带上,导致请求 403。

一开始怀疑是登录接口错误了,没有 set-cookie,但抓包发现 response header 中有 set-cookie,打印请求的 response.cookies 也有需要的 cookie。又怀疑是 set-cookie 的格式不对或者其它问题,但用浏览器实际跑了下流程,发现系统一切正常,那基本就是 requests 库的问题了。

没办法,只能 debug 了,单步调试了几轮,基本了解了 requests 的处理方式,首先把请求参数转变为 Request 对象,然后对使用 prepare_request 对 Request 进行预处理,其中有一步 merge_cookies 的操作(还有各种其它处理),把传入的 cookies 和 self.cookies merge 到 RequestsCookieJar 对象上去,这一步也没啥问题,merged_cookies 变量也是对的。后续将预处理过的请求,通过内置的 http adapter 发送出去。http adapter 底层是通过 urllib3.poolmanager 获取到 urllib3.connectionpool 连接(这里是连接池的核心部分),再通过 conn.urlopen 实际发送请求。虽然跟踪了解到了整个请求逻辑,但最终发出的请求还是没有带上需要的 cookie。

问题定位一度陷入僵局,只能再回顾上面的流程,cookie 肯定就是在 merged_cookies 和 conn.urlopen 之间没的,再仔细观察发现,conn.urlopen 请求参数里面压根没有 cookie 字段。

查阅资料发现,urllib3 的作者说,连接池只处理底层连接,cookie 跟踪等事情应该上层来做。大胆猜测,那 cookie 应该是放在 header 里了,往前捣看看 request.headers 是怎么变动的(此时里面的 Cookie 字段确实不正确)。

再走查代码发现 prepare_request 里面是调用了 PreparedRequest.prepare,其中有一步 prepare_cookies,主要是调用了 cookielib.CookieJar.add_cookie_header 最终将 cookie 放到了 self.headers['Cookie']。但里面有个 request.has_header("Cookie") 的判断,header 中没有 Cookie 字段才会放,不知道为什么这么考虑(最新版 3.8 还是这样),估计是 merge cookie 比较麻烦,但问题确实就出在这里,这次请求之前,Requests Session 已经直接通过 headers['Cookie'] 设置了 cookie。复现代码(修改自官方示例):

虽然找到了问题的原因,但又不好解决,总不能不让直接操作 headers['Cookie'] 吧,先不说无法限制使用者,而且之前的代码已经这样做了,改动量非常之大。不过好在,现在自己用的框架是在 Requests Session 上封装了一层,操作 header 都是调用的统一的 update_headers 方法:

对 headers['Cookie'] 的操作拦截一下,变成对 cookies 的操作:

这样即不用改之前的调用方代码,也防止了后人掉坑。

参考资料

https://requests.readthedocs.io/en/master/user/advanced/#session-objects

https://stackoverflow.com/questions/2422922/python-urllib3-and-how-to-handle-cookie-support

https://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.poolmanager

https://urllib3.readthedocs.io/en/latest/reference/index.html#urllib3.connectionpool.HTTPConnectionPool.urlopen

https://docs.python.org/2/library/cookie.html

Filebeat Log input核心配置详解

Filebeat运行过程

说到实时查看日志,最常用得莫过于 tail -f 命令,基于此可以自己实现一个简单的日志采集工具,https://github.com/iyaozhen/filebeat.py/blob/master/filebeat.py#L237

但这太简陋了,无法保证不丢不重。我们看看 Filebeat 是怎么实现的,官方说明:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html

简单来说 Filebeat 有两大部分,inputs 和 harvesters,inputs 负责找文件(类似 find 命令)和管理 harvesters,一个 harvester 则和一个文件一一对应,一行行读然后发送给 output(类似tail -f)。

当然还有很多细节问题,我们结合配置文件一一详解。

Log input配置详解

官方配置说明:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-log.html

先看一个基本例子(下面所述基于7.x版本,6.x版本也基本适用)

inputs 可以配置多块(block),就是相同类型的放一块,这个也很好理解。paths 可以配置多个文件,文件路径和文件名都支持通配。

ignore_older 和 scan_frequency

这就有两个细节问题了,一是路径下的历史文件可能很多,比如配置了按天分割,显然旧文件我们一般是不需要的。二是扫描频率如何控制,通配设置复杂的话频繁扫文件也是很大的开销。

问题一则是通过 ignore_older 参数解决,意思就是多久前的旧文件就不管了。比如设置为 1h,表示文件时间在 1h 之前的日志都不会被 input 模块搜集,直到有新日志产生。

问题二则是通过 scan_frequency 参数控制,表示多久扫描一次是否有新文件产生。比如设置 10s(默认),一个新文件产生 10s 后会被发现,或者一个旧文件(上面 ignore_older)新产生了一行日志 10s 才发现这个文件。有人说我需要实时性,是不是这个值设置的越小越好,其实是错误的,前面我们介绍了 Filebeat 架构,input 模块只是负责发现新文件,新文件是相对已经被 harvester 获取的文件,第一次发现之后就已经在被 harvester 一行行实时读取了,所以这里基本上只影响日志切分时的实时性(这种场景下的短暂延迟是可以接受的)。

close_* 和 clean_*

那么被 harvester 获取的文件就一直拿着不放吗?文件重命名或者被删除后怎么办呢?这里重点介绍这两组配置。 Continue Reading...

现代化的PHP-写好注释

注释

有个段子:程序员最恨写注释和不写注释的人。个人认为注释主要作用是方便别人或者自己以后能快速理解这段代码逻辑和提升开发效率,这样要说的是如何使用注释来提升开发效率。

众所周知,PHP 是弱类型语言,变量的类型是可以变的,但在实际业务中,一个变量或者函数的返回值往往是确定的,确定的类型能提升我们的编码效率,也能把一些低级的错误(弱类型不代表无类型)扼杀在摇篮里。

PHP 主要通过类型声明来说明变量的类型,PHP7 开始允许更多的函数参数类型限定。比如下面这个例子,声明入参是 int 类型,但使用了 array 函数来处理,PhpStorm 能给出实时的提醒。

调用函数参数错误的话也有提示:

但 PHP 会有隐式类型转换,如上面传入浮点数的时候并没有提示错误,因为这也是合理的。不过 PHP 可以开启严格模式来检查此类错误。

PHP 内置的类型声明基本上够用了,但一般使用注释来实现更加丰富的功能。PHPDoc 是注释 PHP 代码的非正式标准,它有许多不同的标记可以使用。PhpStorm 能根据函数/类方法自动生成基本的标记:

基本标记 @param@return 也很好理解,参数和返回值,作用和 PHP 自带的类型声明差不多,主要用来限制调用方传参和使用返回值。除了这两个标记之外,还有很多其它比较好用的标记。

Continue Reading...

GitHub 研发链 travis-ci 和 codecov 介绍

经常混迹于 GitHub 的话就会发现很多项目都有一些徽章,出现最多的就数这两个了:

这些徽章都是可以点击的,第一个点进去是 https://travis-ci.org/,travis-ci 是一个 CI(Continuous integration,持续集成) 平台,主要提供集群编译、单测、集成测试的环境。.org 的服务对公有仓库免费,.com 面向私人、团队、公司的项目提供商业支持(收费)。使用起来非常简单,使用 Github 帐号登录进去,就能看见开始界面:

核心就是.travis.yml的文件配置,一开始可以根据自己的语言选择初始的配置文件:https://docs.travis-ci.com/user/customizing-the-build#Specifying-Runtime-Versions。我这边的项目使用的是 Python,初始配置是:

官方注释可以说非常详细,我就不画蛇添足做说明了,根据自己代码实际情况做调整即可:

配置好之后再 push 代码,就能在 travis-ci 页面上看见项目的构建、测试结果了:

点击图标就能生成我们需要的 MD 语法的代码了,粘贴进 README.MD 中就能显示了。

第二个徽章是 codecov.io(单测覆盖率统计平台),接入过程也很简单,也是不同语言选择不同的配置文件,codecov 可以无缝衔接 travis-ci,只需要在原来的配置文件上稍作修改即可,核心就是生成单测的结果文件。修改后的 .travis.yml

在原来的基础上多了 codecov nose 插件的下载和运行单测时多了--with-coverage等参数。最后还是再次 push 代码就能看见单测覆盖率报告了:

codecov 还有自己的配置文件:codecov.yml。用来实现一些定制化需求,比如说我需要排除一些模块,不进入覆盖率统计:

徽章生成代码的话在setting中可以找到:

除了上面介绍的这两个徽章,还可以通过 shields.io 平台生成一些其它的徽章,甚至可以自定义,比如 https://img.shields.io/badge/Python-2.7-brightgreen.svg 就可以生成一个表示 Python 版本的徽章,别人能看懂就行。

当然徽章不是重点,不是越多的越牛逼,重要的是规范整个研发流程。上面说的两个其实都是属于 CI(持续集成)中最具代表性的两个环节,算是入门了。关于 CI 是一个很大的话题了,这个有机会以后再说一下。

参考资料:

开源项目徽章集锦,https://segmentfault.com/a/1190000004278253

使用 Python nose 组织 HTTP 接口测试

现在前端 Web、移动端 APP 开发基本上是面向接口编程,后端各种 HTTP 接口已经提供好了,大家只要实现逻辑交互和展示就行。那么问题来了,这么多接口如何方便的进行测试呢?根据个人经验,我认为需要解决三个问题:1. HTTP 接口多种多样,测试程序如何统一?2. 测试程序如何规范组织?3. 接口间有上下文依赖,如何解决?

HTTP 接口多种多样,测试程序如何统一?

后端接口可能来自各个系统,GET/POST 协议的、遵循 RESTful 规范的,使用 session 认证、token 认证的,各式各样的接口都存在。但无论怎么变都无法脱离 HTTP 协议。因为组内的技术栈是 Python,这就很容易想到使用 Python 的 requests 库。首先我们使用requests.Session()会话对象,来进行会话保持和 Header、Cookie 等处理。这里我们可以简单封装一个 HttpSession 类,把一些常用操作和设置封装一下:

通过HttpSession()来获取一个requests session对象,后续的 HTTP 请求都通过它发起。requests 提供 get()、post() 等各种方法调用,但这会让测试代码显得不统一,这里我们直接调用底层的 request 方法,该方法几乎提供了发起一个 HTTP 请求需要的所有参数,非常强大。

这样每一个 HTTP 接口的测试都可以通过准备参数发起请求断言结果三板斧来解决。

测试程序如何规范组织?

前面我们已经解决了如何快捷的发起一个 HTTP 请求的问题,这让我们几行代码就可以测试一个接口的返回值。但你会发现新的问题又来了,各种脚本散乱在一堆,返回值解析各种中间变量,各种配置硬编码在代码中,测试结果全靠 print,这时 nose 框架就派上用场了。nose 是 Python 最流行的一个单测框架,提供测试 case 设置标签、测试 case 查找、强大的断言函数,格式化/可视化报告输出等功能。这样就可以像写单测一样写 HTTP 接口测试了。我们可以把一类接口的测试放在一个类里面,甚至可以把基础变量的定义放在基础测试类里面,其它测试类继承这个基类。

HttpTest基类只做了两个工作:1. 创建http会话对象;2. 读取配置文件到类变量config中。配置文件是一个很好的编程实践,这让我们测试程序和数据能够分离,可以通过调用不同的配置文件来测试不同环境的接口,让我们测试程序不光能做线下测试,还能做线上回归和监控,切换成本非常低。

我这里选择 yaml 语法来组织配置信息,yaml 语法风格类似 json,自带基础数据结构,但更加易于书写和阅读,非常适合做配置文件。通过-env=xxx指定配置文件路径(或者 nose.cfg 配置文件中指定),使用 ruamel.yaml 来解析 yaml,转换为 Python 中能直接操作的数据结构。

这一切都放在setUpClass方法中,在具体测试 case 运行之前就已经准备好这些工作了。 Continue Reading...

使用karma+mocha进行前端测试驱动开发(一)

之前有接触过一些使用 casperJs 做前端自动化测试和页面元素监控,效果还不错,这个后续有时间再介绍一下。但对前端代码层面的测试不是很了解,今天下班比较早,闲来无事,就看了下最近听说的 karma+mocha 前端测试框架,发现还是很有意思,收(刚)获(刚)颇(入)多(门)。目前来看 karma+mocha 非常适合做 TDD(测试驱动开发)。

测试驱动开发(英语:Test-driven development,缩写为 TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。

TDD 提倡写代码之前先把单测写好,然后通过单测来验证代码是否正确。这就需要有一个单测框架(即 mocha),但这还不够,也不能每写完一次就运行一下单测代码,这也效率太低了吧,所以还需要一个框架(即 karma)来做这个事情。

这里先介绍一些 JS 单测框架 mocha(https://github.com/mochajs/mochatj 大神的作品,奈何大神觉得 Node 没搞头去搞 go 了)。安装还是比较简单,这里就不多说了。提醒一下,如果是在 Windows 下安装 Node 的话可能需要设置一下环境变量

实例:一个加法模块src/add.js的代码:

各个语言写单测都差不多,就是对预期结果的断言。上面代码对应的测试代码tests/add.test.js

测试脚本里面应该包括一个或多个describe块,每个describe块应该包括一个或多个it块。

describe块称为“测试套件”(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称(“加法函数的测试”),第二个参数是一个实际执行的函数。

it块称为“测试用例”(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称(“1 加 1 应该等于 2”),第二个参数是一个实际执行的函数。

断言库有很多种,mocha 并不限制使用哪一种。上面代码引入的断言库是 chai,并且指定使用它的assert断言风格。因为这更接近其它语言的风格。

运行一下测试代码:

filehelper_1472143702070_9

大概流程就是这样,详细的可以查看官方文档。随便说一下,WebStorm 和 PhpStorm 原生支持 mocha,设置一下即可右键运行时直接调用 mocha 来运行当前 JS 单测文件:

20160826005459

20160826005634

20160826005739

单测已经写好了,怎么能让测试驱动我开发呢?这时候就需要 karma(https://github.com/karma-runner/karma)登场了。如 github 主页上所说,Karma 不是一个测试框架,也不是一个断言库,仅仅启动一个 http server,并通过你熟知的测试框架生成运行测试的HTML。Karma 支持的测试框架非常多,如前文介绍,我们这里选择的是 mocha 测试框架和 chai 断言库。

首先安装 karma,然后使用karma init my.conf.js命令生成配置文件。生成文件的过程是交互式的,很友好,注意选择测试框架是 mocha。选错了也没关系,配置文件之后可以手动编辑,大概长这样:

比较重要的几个参数:

  • frameworks:我们这里需要 mocha 和 chai。框架必须预先通过 npm install karma-xxx --save-dev 命令安装好,支持的框架详见:https://www.npmjs.com/browse/keyword/karma-adapter
  • files:可以配置通配符把源代码和测试代码加载进来。
  • browsers:需要启动的浏览器
  • autoWatch:观察文件是否变动,如有变动则重新运行单测。

最后使用命令karma start my.conf.js运行即可。但这里我一直失败,遇到如下这些错误:

这是因为上文的代码使用的是node的require()语法,karma是在浏览器端运行的,当然不支持了。如果你的源代码使用的是 AMD、CMD 加载器,测试代码也要使用 requirejs 等框架加载源代码模块。这里我们将源代码和测试代码稍微改造一下就行了:

成功运行:

20160826014133

karma 可以启动不同的浏览器,所以还可以做兼容性测试。

我修改一下源代码,karma 能自动监听到文件变动,然后显示单测结果,这样我就能实时知道代码是否有问题。

karma

当然这只是 karma 的一个功能,还有很多实用功能等待后续学习。

参考资料:

测试框架 Mocha 实例教程,http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html

[JavaScript] Test Suit 4 - Test Runner - Karma,http://veck.logdown.com/posts/248538-javascript-test-suit-4-test-runner-karma

典型的用户注册页面短信验证逻辑漏洞

用户注册是一个网站基本的功能,做过 web 开发的基本上都写过这个功能,但很多漏洞都出在这个页面上面。这里先不讨论密码加密存储,传输安全等问题,就说说注册页面的功能逻辑。

2016-05-22_000624

如上图的新浪微博注册页面,一般的注册页面都需要填写账号、密码等信息,为了防止恶意注册,都还有一个验证码,这几乎是一个注册页面的标配了。现在有些注册页面会多一个手机验证,防止恶意注册的同时也变相完成了实名制的红头文件要求,但有时候问题就出在这里。

2016-05-18_020524

上图这个注册页面,中规中矩,看似没什么问题,填写一些信息后点击下一步:

2016-05-18_020915

很常见的操作,POST 请求将信息带到下一个页面(校验了图形验证码),跳过来的同时已经给手机发了短信验证码,到目前为止没什么问题,用户体验还不错。不过发现页面上还有一个重新发送的按钮,点击一下发现直接调用 /sendRegMsg 接口发送了短信,接口只接收一个手机号参数,无其它参数。这里就是第一个漏洞:短信发送接口无权限验证,可被恶意调用造成经济损失,还可被利用于“短信炸弹”,而且这家厂商本身就是提供短信验证码服务的,估计“短信炸弹”效果更改。要解决这个漏洞也很简单,加个验证码就行了,但这里可能又会有一个坑(后面细说)。实际短信验证码为 4 位,提交企业名称等信息时也没有图形验证码校验:

2016-05-18_021422

那就好办了,直接跑个小脚本暴力提交(最多9999次)即可:

2016-05-18_024823

这里就是第二个漏洞了,任意手机号注册(一大波薅羊毛党将要来袭):

2016-05-18_024610

这两个漏洞都是很好修复的,影响也不大,但若安全意识薄弱,逻辑没有理清楚会造成修复不彻底。比如在短信重新发送操作那里加了一个图形验证码,发送短信和提交时都会做校验,但若你调用一次短信发送接口后图形验证码没有重置,我就可以拿着这一个正确的图形验证码一直调用发送短信接口(重放攻击)也能达到恶意调用和“短信炸弹”的目的,提交信息时也可以用这一个的正确图形验证码暴力提交。还有的为了限制短信发送频率,点击发送短信后将发送按钮设置为 disabled="disabled",然后设置一个定时器 60s 后恢复,这完全也是自己骗自己。

一种比较好的做法是每发一次短信图形验证码都重置,短信验证码填写失败一定次数后重置校验 session,后面任何输入都返回错误。

其实这个漏洞很鸡肋也很低级,但目前这种逻辑漏洞已经成为主要的问题,看似低级,但很难避免,往往影响还很大。而且我们可以看到图形验证码在整个流程中扮演了很重要的角色,但你的图形验证码真的安全吗?答案是否定的,百度贴吧和 12306 “变态”的验证码都是几天内就被破解了,甚至还有人工打码平台,可谓是道高一尺魔高一丈。安全,还是仍重道远呀。

推荐阅读:

黑产揭秘:“打码平台”那点事儿 http://www.freebuf.com/articles/others-articles/104536.html

参考资料:

无(不要问为什么知道这类漏洞)

kanqiu_26461760_2144148040__20160126154003

配置Nginx,开启HTTP/2

HTTP/2(超文本传输协议第2版,最初命名为 HTTP 2.0),是在 Google 之前提出的 SPDY 协议的基础上演变而来,相对 HTTP1.1 增加了连接复用、头部压缩、服务端 push 等特性。与 HTTP1.1 完全语义兼容,几乎可以无缝升级。目前主流浏览器都已经支持 HTTP/2 了(IE 自 IE 11 开始支持)。

开源版本的 Nginx 从 1.9.5 版开始支持 HTTP/2,其实配置也很简单,升级 Nginx 到最新版本,然后把之前 HTTPS 配置中的 spdy 改成 http2 就行了(listen 443 ssl http2 default_server),但改了之后发现并没有生效,直接退回到了 HTTP1.1。nginx -V 查看编译参数也带有 --with-http_v2_module,应该没什么问题才对。Google 了一把发现是 OpenSSL 版本的问题,我使用的是 ubuntu 14.04 下的 ppa:nginx/stable 源,这个源上的 Nginx 是在 OpenSSL 1.0.1f 上编译而成的,而 OpenSSL 的这个版本不支持 ALPN,所以无法开启 HTTP/2。

Note that accepting HTTP/2 connections over TLS requires the “Application-Layer Protocol Negotiation” (ALPN) TLS extension support, which is available only since OpenSSL version 1.0.2. Using the “Next Protocol Negotiation” (NPN) TLS extension for this purpose (available since OpenSSL version 1.0.1) is not guaranteed.

知道原因就好办了,先升级本地的 OpenSSL到最新版:

然后替换 Nginx 源为 ppa:ondrej/nginx 重新安装 Nginx:

现在再打开网站,发现 HTTP/2 已经开启成功了。

2016-05-21_233215

当然如果你是 ubuntu 16.04,就没有那么麻烦了,直接安装 Nginx 配置一下就行:https://www.digitalocean.com/community/tutorials/how-to-set-up-nginx-with-http-2-support-on-ubuntu-16-04

推荐阅读:

Dropbox 迁移到 HTTP/2 的经验,满满都是干货:https://blogs.dropbox.com/tech/2016/05/enabling-http2-for-dropbox-web-services-experiences-and-observations/

检查网站是否支持 SPDY 或者 HTTP/2 的 Chrome 扩展:HTTP/2 and SPDY indicator

参考资料:

https://www.nginx.com/blog/nginx-1-9-5/

http://serverfault.com/questions/732474/nginx-configured-with-http2-doesnt-deliver-http-2

http://nginx.org/en/docs/http/ngx_http_v2_module.html

为什么我们应该尽快支持 ALPN?https://imququ.com/post/enable-alpn-asap.html

Nginx HTTPS 配置实践

前言

博客使用 HTTPS 已经有一段时间了,最近把当时的配置过程梳理了一遍,希望能对有需要的同学有所帮助。

概念介绍

最近 HTTPS 是越来越流行了,前一阵子的百度,最近的淘宝、知乎,都已经开启了全站 HTTPS 的时代。HTTPS 即 Hypertext Transfer Protocol Secure,简单理解就是在 HTTP 协议上加了一层加密。具体的细节以后弄透彻了再写写。

无利不起早,那么 HTTPS 的好处是什么呢?我个人觉得有以下几点:

  1. 数据传输加密,防止信息被窃取。使用 HTTP 协议时,用户的密码、银行账户、隐私信息等都是在网络上明文传输,很容易被中间人截取。
  2. 防欺诈。当你使用 EV 级别证书(https://www.wosign.com/EVSSL/index.htm)时,浏览器网址显示和一般 SSL 证书不一样。可以对比下:https://www.paypal.com/signin/ 和 https://www.baidu.com/。使用此类证书的网址,主流的浏览器都会在地址栏显示企业的名称(支持中文),增加了网站信任度,进一步防止用户被钓鱼网站欺骗,当然证书的价格也更贵一些。
  3. 防止劫持。这点可以说是国内各大公司下决心支持 HTTPS 的主要原因了。一旦被劫持,用户访问速度会变慢,看到的内容会被篡改,比如商家在百度投放的广告被替换成竞品的广告,淘宝的商品被带上小尾巴(返利链接),甚至直接跳转到x东。这些不光是损害用户的利益,更是直接影响公司收入。
  4. 使用新的技术。比如 SPDY/HTTP2(头部压缩、连接复用、Server Push等) 的基础都是 HTTPS,新协议对移动端 APP 性能提升帮助很大:《双11手淘前端技术: H5性能最佳实践》

当然 HTTPS 不是银弹,不是用了就保险了,安全是一个整体,这是典型的水桶场景,往往一个系统被攻破是在其薄弱的环节。

证书申请

证书申请的大概流程是,首先在自己的服务器使用 openssl 命令(尽量使用高版本的 OpenSSL,除了能规避漏洞,还能使用更高级的密钥加密/交换算法,提升安全性和性能)生成 csr 和 key 文件,命令如下:

如命令参数所示,私钥 key RSA 的加密强度是 2048 位(rsa:2048)。CSR 文件使用了更安全的 sha256(SHA-2)摘要算法(SHA-1算法在2016年将不被证书厂商和现代浏览器支持)。

-subj 参数指定证书申请者的信息,如果不直接使用此参数,也会有交互式的命令提示填写这些参数。此部分具体参数释义:

Country Name (2 letter code): 输入国家地区代码,例如中国的 CN

State or Province Name (full name): 地区省份

Locality Name (eg, city): 城市名称

Organization Name (eg, company): 公司名称

Organizational Unit Name (eg, section): 部门名称

Common Name (e.g. server FQDN or YOUR name): 申请证书域名,如果是泛域名证书,则应该填写 *.example.com

Email Address: 电子邮箱

命令还会提示输入challenge password,这个不是加密证书,而是相当于一个二次认证密码,如果厂商证书支持challenge password,而且你也设置了的话,后续证书操作需要验证这个密码,提高安全性。

生成的 CSR(Certificate Signing Request,即证书签名请求,不是证书)文件包含了证书的主要信息和公钥,同时生成了私钥 key 文件(注意保存)。然后把 CSR 文件提交给 CA 厂商(不用提交 key 文件),一般提交的时候会让你选择服务器类型,本文指选的 Nginx。中间还会有一个域名所有者验证过程,一般几分钟后(根据证书类型不同,时间不一样)就会发一份邮件给你。

邮件附件一般会带有厂商用 CA 私钥签好的证书文件 example.com.cert.pem(包含 域名、CA 信息,key 对应的公钥,CA 的签名)。同时会给你 CA 的证书链 example.com.cert.ca.pem,如果 CA 证书是多个文件(通常包括 1-2 个中间证书和 1 个根证书),那么需要按照证书信任链由下游到上游,把文件合并成 example.com.cert.ca.pem(cat COMODOECCDomainValidationSecureServerCA.crt COMODOECCAddTrustCA.crt AddTrustExternalCARoot.crt > example.com.cert.ca.pem)。不用把根证书放进 CA 证书链文件里面,因为浏览器和操作系统都内置了根证书,也只认自己内置的根证书。

不过现在更流行的是使用 acme.sh 自动申请和续签 Let’s Encrypt 证书,即免费又好用。acme.sh 提供的方式有很多种,大家按需配置即可,我是 DNSPod + Nginx,仅供参考:

证书处理

后面的步骤各个 HTTP Server 就有点不一样了:

tomcat 则比较特别,证书编码格式需要的不是 pem 而是 JKS,得转换一下。

到这里为止出现了好几个文件,可能有点迷糊,这里总结一下

文件名
释义
example.com.csr
Certificate Signing Request,证书签名请求,不是证书
example.com.key.pem
证书私钥(key),不能泄露(即使是对 CA 厂商),和CSR文件成对存在
example.com.cert.pem
自己的域名证书(pem格式),包含 key 对应的公钥
example.com.cert.ca.pem
CA厂商证书链(pem格式)
example.com.cert.full.pem
合并了自己证书和CA证书的完整证书链(pem格式)
example.com.key.cert.pem
合并了key和域名证书的文件(pem格式)
example.com.cert.pfx
pfx/p12格式的完整证书链(包含了key)
example.com.cert.jks
jks格式(JAVA)的完整证书链(包含了key)

对于这几种文件的区别,可以参考:https://www.cnblogs.com/yjmyzz/p/openssl-tutorial.htmlhttps://blog.freessl.cn/ssl-cert-format-introduce/。实际不用那么麻烦,一般证书厂商会提供不同 webserver 的配置文件,如果没有则需要动手一下,或者使用证书厂商提供的转换工具

TLS握手简述

借由这几个证书文件,简单介绍一下 TLS 的握手过程(RSA 密钥交换方式):

客户端
服务端
ClientHello
明文发送握手请求(上报支持的能力)
生成一个随机数r1
ServerHello
明文返回域名证书(example.com.cert.full.pem)
还会返回一个随机数r2
这里服务端也可以要求客户端提供证书进行校验(流程会有所不同,省略)
对证书进行验证,前面有介绍,证书中包含 CA 信息和签名信息,逐级向上验证即可
ClientKeyExchange
验证通过后从域名证书中拿出公钥,然后使用公钥加密PreMasterSecret(随机生成),发送给服务端(密文)
服务端使用私钥(example.com.key.pem)解密得出PreMasterSecret
客户端使用r1、r2、PreMasterSecret生成session key(会话密钥)
服务端也用同样的方式生成session key,生成算法相同(PRF
使用session key进行对称加密

可以看出核心就是 PreMasterSecret 是密文,其它都是明文。如果中间人想要截取就只能替换证书中的公钥(用自己私钥解密),因为证书本身也有完整性签名,就只能替换整个证书,但证书又是一级级到 Root CA 校验,所以 TLS 握手的结果是安全的。

证书逐级验证的过程(来源:https://security.stackexchange.com/questions/56389/ssl-certificate-framework-101-how-does-the-browser-actually-verify-the-validity

才疏学浅,介绍的非常简陋,建议阅读 https://halfrost.com/https_tls1-2_handshake/ 及系列文章

配置详解

下面正式开始 Nginx 的配置详解。 Continue Reading...