修复漏洞的最佳时机便是开发的时候。
CSRF TOKEN是Django安全体系中的一项非常重要的安全措施。但是很多情况下,一些刚刚接触Django的同学会发现自己好不容易写出来的表单,在POST的时候报错了,经过一番查找发现是CSRF TOKEN的问题,然后按照网上的方法三下五除二将settings.py中的CSRF TOKEN配置全部移除了,代码正常跑起来了。熟不知这种操作将极大的影响网站的安全性,且提高了后期修补漏洞的成本;而在开发阶段消灭安全问题,是成本最低的时候。
关于CSRF TOKEN的相关内容,官方文档上有十分详细的说明,具体用法这里不再阐述了。这里推荐一种比较方便的用法,在开发的时候对开发人员的感知较小,不用特别去关心Token是否已经发送成功了。
在总的父模板页面中添加{% csrf_token %},并且在<script>部分进行如下设置:
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
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×tamp=3333&key1=aaa,根据字母序排序完成后为22221111aaa3333,加入分隔符(|)后为2222(|)1111(|)aaa(|)3333,然后将这个字符串计算sha1,生成sign值。用Python代码写的话比较简单:
data = {
"access_token": "123456",
"access_key": "654321",
"timestamp": int(time.time()),
"key1": "value1",
"key2": "value2"
}
sep = "(|)"
raw_params = sep.join(
[
str(x[1]) for x in sorted(data.items(), key=lambda item: item[0])
]
)
sign = hashlib.sha1(raw_params.encode()).hexdigest()
data["sign"] = sign
resp = requests.post(url, json=data)
result = resp.json()
在生产环境关闭DEBUG模式;
不要将settings.py添加到版本管理中,并且保护好SECRET_KEY;
设置好ALLOWED_HOSTS;
尽可能的使用Django提供的ORM,避免通过.raw()等方法直接执行SQL语句,如果无法避免,一定要将参数正确转义以避免出现SQL注入漏洞;
尽可能的禁用Django Admin,如果一定需要使用,请修改掉默认的Admin URL;
但是当我们部署多个项目的时候,这种安装依赖的方式很容易出现依赖冲突。举个简单的例子,我们现在有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正是为了解决这个问题而诞生的,它可以为每个项目创建一个单独的运行环境,从而避免依赖冲突的问题。
我们可以通过使用pyenv来管理多个Python版本的问题,进一步通过pyenv的插件pyenv-virtualenv来管理多Python版本、多虚拟环境的问题。
Django中已经内置了一个简单的WSGI实现来供我们通过上述方式启动Web服务,如果你只是想调试或者只提供服务给几个人用的小程序,那也不失为一种可选择的方案,虽然这种方案看上去并不是那么优雅。
如果你真的想将应用部署到实际的生产环境中,那么你还需要一个高性能的WSGI Server,而不是django提供的简单的WSGI Server。Gunicorn和uWSGI两种都是比较主流的WSGI Server,根据实际部署环境,从中选择一个就好。
不过我个人比较偏向于Gunicorn,虽然在众多的性能测试中uWSGI都占了上风,选择Gunicorn的理由是它与uWSGI相比十分简单,没有非常复杂的极少用到的功能,并且uWSGI中的一些功能已经逐步被Nginx所支持,且Gunicorn配置起来也较为简单方便。顺带一提,如果你在Windows上部署,你可能需要使用Apache+mod_wsgi。
需要Nginx来处理静态资源。如果你将Django的DEBUG模式设置为False,就会发现很多CSS以及JS等静态资源加载不到了,这是因为Django并不会主动去处理这些请求,这些都需要Nginx来帮忙处理;
通过Nginx来进行多个backend的负载均衡。如果你的服务部署在多台服务器上,或是进行了一主一备的部署,这些都可以通过在Nginx上进行简单的设置来实现;
直接将uWSGI或Gunicorn暴露出来存在一定的安全隐患,使用Nginx处理HTTP的问题会更加方便;
除此之外,还有一些理由就不在此列举了,还是上面的那句话,如果你的服务很简单,只有几个人访问,是不需要做这么复杂的设置的。
我们可以使用supervisor来守护Django进程,保证其稳定存活。但是有一点需要注意,小心不要出现Supervisord的远程命令执行漏洞,从而造成更大的事故。
通常来讲,如果想启动后台服务的话,celery是一个万能的选择,但是很多时候我们并不想引入这么重的依赖,就需要自己想办法来启动后台服务了。
一个简单的方法就是做成manage.py的command,通过./manage.py runcommand的方式来启动我们的后台服务,并且通过编写shell脚本控制服务的启动与停止,或者通过supervisor进行管理。
如果你想让后台进程随着Web服务同时启动和停止,那么放到wsgi.py中是个不错的选择,在wsgi.py中初始化相关的后台服务,并且启动。但是这样的做法不够灵活,当需要单独更新Web服务或后台服务时,需要将二者全部重启,而采用第一种方式的情况下可以单独更新其中的某个服务。