Author:[email protected]

Date:20180321




PART 1. 安全第一


修复漏洞的最佳时机便是开发的时候。

1.1 CSRF TOKEN

CSRF TOKEN是Django安全体系中的一项非常重要的安全措施。但是很多情况下,一些刚刚接触Django的同学会发现自己好不容易写出来的表单,在POST的时候报错了,经过一番查找发现是CSRF TOKEN的问题,然后按照网上的方法三下五除二将settings.py中的CSRF TOKEN配置全部移除了,代码正常跑起来了。熟不知这种操作将极大的影响网站的安全性,且提高了后期修补漏洞的成本;而在开发阶段消灭安全问题,是成本最低的时候。

关于CSRF TOKEN的相关内容,官方文档上有十分详细的说明,具体用法这里不再阐述了。这里推荐一种比较方便的用法,在开发的时候对开发人员的感知较小,不用特别去关心Token是否已经发送成功了。

在总的父模板页面中添加{% csrf_token %},并且在<script>部分进行如下设置:

这样所有通过JQuery中的的AJAX发起的非GET|HEAD|OPTIONS|TRACE请求都会自动包含CSRF TOKEN(获取CSRF TOKEN的部分代码未列出),如果使用的是其他HTTP库或Fetch,进行对应的设置即可。


1.2 API安全设计

某些情况下,我们的Web服务还会对外提供一些API服务,供其他系统调用,对于这些API接口,CSRF是一定要关闭的,不然根本无法正常使用。除此之外,我们还应当做一些其他的安全措施,防止API接口被滥用。除了正常的传递参数外,我们应当保证这些参数不被中间人篡改,并且只允许有权限的人调用对应的接口,为此我们需要额外引入几个传递的参数:timestamp, sign, access_key, access_token。

  • access_key和access_token,这是一对参数。access_key相当于标识调用方是谁,access_token则是相当于调用方的秘钥,access_token内容不应该能被简单的预测到,而access_key可以为了方便记忆选择较为简单的字符串。只有当两个参数匹配后,才认为本次API的调用是合法的。

  • timestamp时间戳,根据自己的业务情况选择精确到秒或是毫秒。添加时间戳主要是为了防止本次调用被重放攻击,服务端应当校验客户端传递的时间戳是否在一个时间范围内,超时的时间戳都应当被认为是非法的请求。但是时间戳在重放的时候还是有被篡改的风险,所以就需要引入下一个参数sign来保证参数的真实性。

  • sign是所有参数的签名值,是所有参数值参与hash计算产生的,参数每变动一点,sign都需要重新生成,借此保证参数的真实性。常用的一套算法是:根据参数key的字母序,将参数value进行排序,并且使用特定的分隔符连接起所有的参数,然后进行hash计算,并将sign参数一同传递给服务端。举个例子,现有参数ak=2222&at=1111&timestamp=3333&key1=aaa,根据字母序排序完成后为22221111aaa3333,加入分隔符(|)后为2222(|)1111(|)aaa(|)3333,然后将这个字符串计算sha1,生成sign值。用Python代码写的话比较简单:


    1.3 一些杂项

    除了上面两个比较重要的点之外,还有一些地方需要额外关注一下。

    • 在生产环境关闭DEBUG模式;

    • 不要将settings.py添加到版本管理中,并且保护好SECRET_KEY;

    • 设置好ALLOWED_HOSTS;

    • 尽可能的使用Django提供的ORM,避免通过.raw()等方法直接执行SQL语句,如果无法避免,一定要将参数正确转义以避免出现SQL注入漏洞;

    • 尽可能的禁用Django Admin,如果一定需要使用,请修改掉默认的Admin URL;



    PART 2. 如何部署


    从git上拉下代码,使用pip安装项目依赖,通过manage.py运行服务,这一切看起来很美好,可是你真的打算在生产环境中这样做吗?

    2.1 隔离环境

    一般情况下,我们的服务器上都只会有一个Python环境,部署的时候一般都是通过pip来安装项目所需要的依赖,这些包都会被安装到全局的site-packages目录中。

    但是当我们部署多个项目的时候,这种安装依赖的方式很容易出现依赖冲突。举个简单的例子,我们现在有Project-A和Project-B两个项目,A和B都依赖第三方包third-A的不同版本,当我们通过pip install -r requirements-a.txt的时候,依赖third-A被安装到了全局的Python环境中,当我们再次安装pip install -r requirements-b.txt的时候,也会再次安装third-A,这个时候,如果两个项目依赖的版本不一致,譬如A项目需要1.0版本,而B项目需要2.0版本,就会产生依赖冲突,进而导致安装依赖失败。

    那么如何解决这个问题呢?我们很容易想到的就是,如果我们有多个互不相干的隔离环境,每个项目部署在一个独立的环境中,那么这个问题就迎刃而解了。virtualenv正是为了解决这个问题而诞生的,它可以为每个项目创建一个单独的运行环境,从而避免依赖冲突的问题。


    2.2 版本管理

    前面我们知道了如何创建隔离环境并且在不同的环境中部署不同的项目,但是这里有个问题,所有环境使用的Python版本是一样的。如果恰好你需要部署多个不同版本的Python项目,例如Python2.7(我知道这个版本即将不维护了,但是我在这里举个例子)、Python3.6和Jython项目,一个一个安装就显得有些复杂,甚至编译安装的时候一不小心少了某个编译参数而需要重新编译,都在某种程度上加大了部署工作量。

    我们可以通过使用pyenv来管理多个Python版本的问题,进一步通过pyenv的插件pyenv-virtualenv来管理多Python版本、多虚拟环境的问题。


    2.3 网关接口

    当我们解决了各种环境的问题后,是时候来考虑如何将项目跑起来了,如果你想的是python manage.py runserver 0.0.0.0:80,那就有点太过于简单了。

    Django中已经内置了一个简单的WSGI实现来供我们通过上述方式启动Web服务,如果你只是想调试或者只提供服务给几个人用的小程序,那也不失为一种可选择的方案,虽然这种方案看上去并不是那么优雅。

    如果你真的想将应用部署到实际的生产环境中,那么你还需要一个高性能的WSGI Server,而不是django提供的简单的WSGI Server。Gunicorn和uWSGI两种都是比较主流的WSGI Server,根据实际部署环境,从中选择一个就好。

    不过我个人比较偏向于Gunicorn,虽然在众多的性能测试中uWSGI都占了上风,选择Gunicorn的理由是它与uWSGI相比十分简单,没有非常复杂的极少用到的功能,并且uWSGI中的一些功能已经逐步被Nginx所支持,且Gunicorn配置起来也较为简单方便。顺带一提,如果你在Windows上部署,你可能需要使用Apache+mod_wsgi。


    2.4 反向代理

    当我们的WSGI Server启动就绪后,就要考虑一下反向代理的问题了,之所以前面再挡一层Nginx进行反向代理,有以下几点原因:

    • 需要Nginx来处理静态资源。如果你将Django的DEBUG模式设置为False,就会发现很多CSS以及JS等静态资源加载不到了,这是因为Django并不会主动去处理这些请求,这些都需要Nginx来帮忙处理;

    • 通过Nginx来进行多个backend的负载均衡。如果你的服务部署在多台服务器上,或是进行了一主一备的部署,这些都可以通过在Nginx上进行简单的设置来实现;

    • 直接将uWSGI或Gunicorn暴露出来存在一定的安全隐患,使用Nginx处理HTTP的问题会更加方便;

    • 除此之外,还有一些理由就不在此列举了,还是上面的那句话,如果你的服务很简单,只有几个人访问,是不需要做这么复杂的设置的。


    2.5 进程守护

    到现在为止,我们的服务已经部署成功了,并且可以开始提供正常的服务了。但是我们少考虑了一点,如果我们的Django不幸因为一些未知的原因退出了,那么我们的Web服务就会变成502了。为了保证服务的稳定性,我们需要对Django进程进行守护,当其出现未知问题而导致异常退出的时候,需要自动将需要的进程拉起来。

    我们可以使用supervisor来守护Django进程,保证其稳定存活。但是有一点需要注意,小心不要出现Supervisord的远程命令执行漏洞,从而造成更大的事故。



    PART 3. 后台服务

    通常来讲,如果想启动后台服务的话,celery是一个万能的选择,但是很多时候我们并不想引入这么重的依赖,就需要自己想办法来启动后台服务了。

    一个简单的方法就是做成manage.py的command,通过./manage.py runcommand的方式来启动我们的后台服务,并且通过编写shell脚本控制服务的启动与停止,或者通过supervisor进行管理。

    如果你想让后台进程随着Web服务同时启动和停止,那么放到wsgi.py中是个不错的选择,在wsgi.py中初始化相关的后台服务,并且启动。但是这样的做法不够灵活,当需要单独更新Web服务或后台服务时,需要将二者全部重启,而采用第一种方式的情况下可以单独更新其中的某个服务。