0%

Flask是目前十分流行的web框架,采用Python编程语言来实现相关功能。它被称为微框架(microframework),“微”并不是意味着把整个Web应用放入到一个Python文件,微框架中的“微”是指Flask旨在保持代码简洁且易于扩展,Flask框架的主要特征是核心构成比较简单,但具有很强的扩展性和兼容性,程序员可以使用Python语言快速实现一个网站或Web服务。

阅读全文 »

静态文件

静态文件的配置,一般需要使用到以下几个参数

1
2
3
STATIC_URL
STATIC_ROOT
STATICFILES_DIRS

STATIC_URL

STATIC_URL 是必须设置的,提供了静态文件访问的映射关系,如设置 STATIC_URL='/static/' 时(名字可以自定义,这个是访问所有的静态文件的根名),访问静态文件的路径就是 127.0.0.1:8000/static/静态文件名。在设置了该参数之后,Django实际上就有了访问静态文件的能力,所以说该参数是必须设置的。

在开发模式下(DEBUG设置为True),Django提供了静态文件代理的功能,会自动识别每个 app下静态文件(前提是,这些静态文件夹必须设置在每个app之下,且使用的名字必须是static,这是因为Django内部做了相应的处理,只有名字 static 才会被Django自带的静态文件代理服务器识别),提供访问。

设置app下的静态文件夹的时候,内部最好有一个用当前app名字命名的文件夹,并在该文件夹下放置相应的静态文件(这是因为不做区分,万一多个app之间有同名的静态文件,Django就会混淆),此时这些静态文件的访问地址就是: 127.0.0.1:8000/static/app命名的文件夹/相应的静态文件
例如,第三方插件xadmin这个app就是这样设计的:
第三方插件xadmin图片

此外,设置了该参数之后,在模板层通过引入 { % load staticfiles % } [1]标签,可以在模板中使用 href="{ % static 'css/reset.css' % }"[1:1] 构建静态资源路径的构建,这样当对 STATIC_URL 指定的静态文件的根名方法改变的时候,不需要修改模板中的访问路径,依然能够访问,效果和{ % url '' % }[1:2]类似。

STATICFILES_DIRS

只在开发模式下有效,对于一些公共的静态文件,或者在项目中自建app的时候不想为每个app单独设置静态文件夹,为了方便管理,可以单独在项目的根目录下设置一个文件夹统一管理所有的静态文件(但是这个文件夹无法被开发模式下Django自带的静态文件代理服务定位到,所以必须使用STATICFILES_DIRS进行设置),设置如下:

1
2
3
4
5
6
7
8
9
# 全局静态文件访问配置
STATICFILES_DIRS = [
os.path.join(BASE_DIR, '集中管理的文件名')
]
# 也可以设置多个静态文件夹
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'xxx/static') # 可以设置多个静态文件查找路径
]

这样在项目根目录下创建 集中管理的文件名文件,在里面放置静态文件,此时静态文件的访问地址是 127.0.0.1:8000/static/集中管理的文件名中的静态文件

STATICFILES_DIRS 的设置只是提供给开发模式下的Django自带静态文件代理服务定位静态文件用的。

STATIC_ROOT

在Django项目部署上线的时候,DEBUG需要设置为False,Django 不再提供静态文件代理功能,app自带的静态文件夹分布在各app下无法访问到,此时需使用 pyhton manage.py collectstatic 收集所有的静态文件,STATIC_ROOT就是用来设置所有的静态文件的聚合目录,如:

1
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

使用 python manage.py collectstatic 之后,django会把所有的static文件都复制到STATIC_ROOT文件夹下。

小结

  1. 无论是开发模式还是生产环境下,Django项目都依赖STATIC_URL=’/static/'的设置,因为这提供了静态文件访问的映射关系,有了他,Django才能够访问静态文件。
  2. STATICFILES_DIRS用于指定在项目根目录下统一管理的静态文件夹的名字,但只是在开发过程中有效以及使用,生产环境下,所有的静态文件都将被收集到统一的目录下(包括 STATICFILES_DIRS 的),交给第三方静态服务器调度提供。
  3. 若存在app下的static和STATICFILES_DIRS设置中同名的文件,默认访问的是STATICFILES_DIRS设置的静态文件下的,所以在app下的static中再设置一个app名的文件夹对静态文件管理,这样在访问时加上app名127.0.0.1:8000/static/app名,这样可以有效避免一些重名问题。
  4. 关于static目录的位置设置使用建议,若website中的每个app的独立性非常强的话,就在每个单独的app下设置static目录,这样app就会支持热插拔,独立性强。若app的独立性不强,app之间相互联系的话,建议使用集中管理static的方法。但是无论使用哪种方式,在项目部署的时候,都会进行静态文件的收集,由第三方静态服务器管理。

media文件

设置和static类似,因为在开发模式下(DEBUG需要设置为True),Django并不提供media文件的代理服务,所以即使在开发模式下,也需要手动设置medai文件的代理。

配置

需要同时设置MEDIA_URL和MEDIA_ROOT

1
2
3
# media文件的路径
MEDIA_URL = "/media/" # 可以改名
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # media为放置 media文件的文件夹名

解释:
设置MEDIA_ROOT后,Django做了一定的处理,能够使上传的文件传到MEDIA_ROOT指定的文件中,如models.py中指定的模型有些有 upload_to="org/%Y/%m",这样指定后,就会在media中创建 org/年/月 的文件夹存储文件。此时仅仅设置 MEDIA_ROOT 参数,就可以完成上传时自动放到设置的media目录下。但是此时的图片访问地址是当前所在页面的地址,加上存储的地址,为当前所在地址/org/%Y/%m/图片

若想访问到meida中的地址,还需要设置MEDIA_URL="/media/" ,设置之后访问media的路径就变成了127.0.0.1:8000/media/org/%Y/%m/图片。但是目前通过这个地址还是访问不了media(这点和static不同),Django没有对media的访问做处理,还需要我们手动对url的访问处理:

1
2
3
4
# 在项目urls.py中设置url
from django.views.static import serve # 静态文件处理的view,该方法接受path参数
# 配置上传文件的访问url
url(r'^media/(?P<path>.*)$', serve, {"document_root": MEDIA_ROOT}),
1
2
3
4
5
6
7
8
9
from django.views.static import serve

def serve(request, path, document_root=None, show_indexes=False):
pass
'''
这个方法serve是一个专门用于处理静态文件的view,这里的用法就和我们定义一个view视图处理url请求一样,<path>中设置的参数path会传递给view处理函数,也就是这里的serve函数,
{"document_root": MEDIA_ROOT}这叫额外参数,为dict类型,额外参数只能被视图函数(这里的serve)读取
通过serve参数可见,设置的path和document_root都是serve()的参数
'''

此时就完成了media文件的访问。

问题

媒体文件都是通过models.py中模型通过设置 upload_to 字段将文件的路径存储在数据库中,文件存储在MEDIA_ROOT设置的路径之中,所以在使用的时候,数据库中只能读取到 upload_to 时的路径 /org/年/月/文件名,而这个路径是不完整的,想要访问到文件还需要添加MEDIA_URL设置的前缀。
在模板层中想要获得MEDIA_URL设置的前缀可以view视图进行传递,然后在模板层中使用 data-url="{ { MEDIA_URL } } { { org.image } }"[1:3],但是这样写的话,意味着要在每个views中传递MEDIA_URL参数,这是非常不好的。可以使用类似注入的方式,将MEDIA_URL注入到模板中,使其可以在模板中直接被调用。
Django自带了一些注入,如在模板中,直接可以使用{ % if request.user.is_authenticated % }[1:4]调用request.user设置直接使用user进行操作,这是因为在settings.py中的进行了如下的设置:

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
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')] # 该项目一定要配置
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request', # 注册了该条语句,能够使用request
'django.contrib.auth.context_processors.auth', # 能够直接使用user, 返回的也是request.user
'django.contrib.messages.context_processors.messages',
],
},
},
]


def auth(request):
"""
Return context variables required by apps that use Django's authentication
system.

If there is no 'user' attribute in the request, use AnonymousUser (from
django.contrib.auth).
"""
if hasattr(request, 'user'):
user = request.user
else:
from django.contrib.auth.models import AnonymousUser
user = AnonymousUser()

return {
'user': user,
'perms': PermWrapper(user),
}

django中已经提供好了django.template.context_processors.media只要添进去即可
我们看一下这个函数media的逻辑:

1
2
def media(request):
return {'MEDIA_URL': settings.MEDIA_URL}

逻辑相当简单,这样我们也可以模仿使用类似的方式定义我们自己的context_processors
之后再模板中使用图片的方式为:src="{ { MEDIA_URL } }{ { hot_course.image } }"[1:5]

若不不使用模板层的context_processors注入方法,还可以使用image model(models.ImageField)的自带属性方法url,他会自动拼接形成一个可以方法的url,如src="{ { hot_course.image.url } }"[1:6]。实际使用中也推荐使用该种方法,因为不需要做额外的操作,直接调用图片属性的url,即可自动拼接成图片相应的访问地址。


  1. Django模版语法中,此处的两个中括号之间以及中括号和百分号之间是没有空格的,由于hexo的限制,写在一起无法显示,故作此说明。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

使用 Nginx + uWSGI 部署,Nginx完成端口的转发,uWSGI负责启动 Django

Nginx、WSGI、uwsgi、uWSGI、django这几个关系

WSGI,全称 Web Server Gateway Interface,或者 Python Web Server Gateway Interface ,是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。描述的是Web服务器如何与Web应用间进行通信。它不是服务器、python模块、框架、API或者任何软件,只是一种描述web服务器(如nginx,uWSGI等服务器)如何与web应用程序(如用Django、Flask框架写的程序)通信的规范。实现了WSGI规范的服务器称为WSGI服务器,是专门用来部署WSGI程序(使用Python语言实现的web程序)的服务器,常见的生产级别的WSGI服务器有uWSGI、Gunicorn、Gevent、Waitress等,可以根据不同的使用场景进行选择。

uwsgi协议是一个uWSGI服务器自有的协议,它用于定义传输信息的类型(type of information),每一个uwsgi packet前4byte为传输信息类型描述,用于与nginx等代理服务器通信,它与WSGI相比是两样东西。

uWSGI是实现了uwsgi和WSGI两种协议的Web服务器。

为什么有了uWSGI为什么还需要nginx?因为nginx具备优秀的静态内容处理能力,然后将动态内容转发给uWSGI服务器,这样可以达到很好的客户端响应。

完整的访问过程

  1. Nginx接收到浏览器的http请求,将包进行解析,分析url,如果是静态文件直接访问用户给Nginx配置静态文件目录,如果不是静态文件,是一个动态请求,Nginx会转发给uWSGI。
  2. uWSGI接到请求会进行处理成WSGI可以接受的形式,并发给WSGI。
  3. WSGI根据请求调用应用程序的某个文件,某个文件的某个函数处理完会返回给WSGI,WSGI将返回值进行打包,打包成uWSGI能够接受的格式,uWSGI接受WSGI的发送请求,转发给Nginx,Nginx最终将返回值返回给浏览器。

为什么不用uWSGI作为唯一服务器

1.安全问题,程序不能直接被浏览器访问到,而是通过nginx,nginx只开放某个接口,uwsgi本身是内网接口,这样运维人员在nginx上加上安全性的限制,可以达到保护程序的作用。

2.负载均衡问题,一个uwsgi很可能不够用,即使开了多个work也是不行,毕竟一台机器的cpu和内存都是有限的,有了nginx做代理,一个nginx可以代理多台uwsgi完成uwsgi的负载均衡。

3.静态文件问题,用django或是uwsgi这种东西来负责静态文件的处理是很浪费的行为,而且他们本身对文件的处理也不如nginx好,所以整个静态文件的处理都直接由nginx完成,静态文件的访问完全不去经过uwsgi以及其后面的东西。

工具安装(以CentOS7为例)

mysql

安装完成之后,需要使用进行如下配置

1
2
3
4
在/etc/my.cnf中需要设置
在 [mysqld]:
下面加一行
bind-address = 0.0.0.0

数据库的迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 将本地的数据库(music_db)导出为sql文件(music_db.sql)
mysqldump -uroot -p db_name > db_name.sql

# 用scp命令将sql文件发送到服务器
scp music_db.sql 远程登录账号@服务器ip:服务器目录/db_name.sql

# 在服务器上登录mysql
mysql -uroot -p

# 在服务器的mysql中新建同名数据库(music_db),然后退出mysql
create database new_db_name charset=utf8;

# 退出
exit;

# 将通过scp命令传输过来的db_name.sql文件导入到,刚刚建立的同名数据库中
mysql -uroot -p new_db_name < db_name.sql

nginx

1
2
sudo yum install epel-release
sudo yum install nginx

python环境

uWSGI

使用 pip install uwsgi 安装
在项目的根目录下,通过 uwsgi --http :8001 --module 项目名.wsgi 运行项目(使用的是项目下的wsgi.py文件,需要文件内包含一个WSGIapplication对象),以http的方式,让uwsgi启动我们的项目。

注意:端口需要在服务器的 安全策略组 中进行了开放

当输出以下内容时,说明项目启动成功

1
2
3
...
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 24712, cores: 1)

配置

在Django项目根目录下创建conf文件夹,文件夹下创建uwsgi文件夹和nginx文件夹,分别用于存储uwsgi和nginx的配置文件。

nginx

配置nginx文件,后缀名为 .conf,如myproject_nginx.conf

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
# the upstream component nginx needs to connect to
upstream django {
# server unix:///path/to/your/mysite/mysite.sock; # for a file socket
server 127.0.0.1:8001; # for a web port socket (we'll use this first)
# 设置启动Django的端口,可以任意命名合法的端口号
}
# configuration of the server

server {
# the port your site will be served on
listen 80;
# the domain name it will serve for
server_name 云服务器公网IP或者域名 ; # substitute your machine's IP address or FQDN
# 配置服务器的IP地址
charset utf-8;

# max upload size
client_max_body_size 75M; # adjust to taste

# Django media
# 配置使用nginx来代理静态文件,而不使用Django自带的代理,得益与nginx底层的io多路复用,nginx的性能高
location /media {
alias /root/projects/MxShop/media; # 指向django的media目录
}

location /static {
alias /root/projects/MxShop/static; # 指向django的static目录
}

# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass django;
include uwsgi_params; # the uwsgi_params file you installed
}
}

使用nginx之后,项目中的静态文件就交给nginx代理了,而不使用Django自带的代理,得益与nginx底层的io多路复用,nginx的性能高。

注意区分:listen监听的这个端口是外部浏览器访问nginx的端口,而uwsgi_pass这个端口是nginx访问uwsgi的端口,所以设置需要和uwsgi.ini配置文件中的socket指定的路径一致。

uwsgi配置

配置uwsgi文件,后缀名为 .ini,如uwsgi.ini

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
# mysite_uwsgi.ini file
[uwsgi]

# Django-related settings
# the base directory (full path) 项目的地址
chdir = /root/MyProject
# Django's wsgi file 项目中的wsgi.py文件的位置,相对chdir的设置
module = MyProject.wsgi
# the virtualenv (full path)

# process-related settings
# master
master = true
# maximum number of worker processes
processes = 10
# the socket (use the full path to be safe )
socket = 127.0.0.1:8001 # 和nginx中的server配置相对应
# ... with appropriate permissions - may be needed
# chmod-socket = 664
# clear environment on exit
vacuum = true
# 虚拟环境的路径 ,当前的python环境文件
virtualenv = /root/.virtualenvs/myproject

pidfile= uwsgi.pid # 生成包含当前uwsgi主进程的文件,用户管理uwsgi服务
# logto = /tmp/mylog.log # 日志文件的存储地址

注意:以上的配置启动后,配合nginx的设置进行使用,直接通过url无法访问。

参数解释

  • socket:socket文件,也可以是地址+端口,用于与某些第三方路由器(例如nginx)一起使用的http选项,如果直接通过http直接访问uWSGI,需要使用http,如http = 0.0.0.0:8001
  • master:是否启动主进程来管理其他进程;
  • chdir:项目的根目录;
  • module:wsgi文件相对路径;
  • vacuum:服务结束后时候删除对应的socket和pid文件;
  • pidfile:指定pid文件,设置该属性之后,uWSGI通过xxx.ini启动后会在相同目录下生成一个xxx.pid的文件,里面只有一行内容是uWSGI的主进程的进程号。

基本命令

启动
uwsgi --ini xxx.ini
重启
uwsgi --reload xxx.pid 需要设置pidfile属性
停止
uwsgi --stop xxx.pid 需要设置pidfile属性
kill -9 uwsgi 此方法会杀死所有的uWSGI服务,在启动了多个uWSGI服务时,谨慎使用

启动Django项目

先启动uWSGI,之后再启动Nginx

启动uWSGI

进入虚拟环境,进入uwsgi配置文件所在的目录,通过 uwsgi --ini uwsgi.ini 命令,使用uwsgi启动Django项目,出现以下输出,表示启动成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
uWSGI running as root, you can use --uid/--gid/--chroot options
*** WARNING: you are running uWSGI as root !!! (use the --uid flag) ***
spawned uWSGI master process (pid: 25690)
spawned uWSGI worker 1 (pid: 25693, cores: 1)
spawned uWSGI worker 2 (pid: 25694, cores: 1)
spawned uWSGI worker 3 (pid: 25695, cores: 1)
spawned uWSGI worker 4 (pid: 25696, cores: 1)
spawned uWSGI worker 5 (pid: 25697, cores: 1)
spawned uWSGI worker 6 (pid: 25698, cores: 1)
spawned uWSGI worker 7 (pid: 25699, cores: 1)
spawned uWSGI worker 8 (pid: 25700, cores: 1)
spawned uWSGI worker 9 (pid: 25701, cores: 1)
spawned uWSGI worker 10 (pid: 25702, cores: 1)

启动Nginx

通过以下命令,将nginx的配置文件放置到 /etc/nginx/conf.d/ 目录下
sudo ln -s 你的目录/Mxonline/conf/nginx/uc_nginx.conf /etc/nginx/conf.d/

  • conf.d 文件夹下可以放置多个启动配置文件,每个项目对应一个。
  • 推荐使用软连接的方式,在conf.d目录下创建配置文件的软链接,这样在项目中修改,会直接同步到配置文件中,比较方便。

最好修改一下 /etc/nginx/nginx.conf中的权限配置,将第一行的user nginx;修改为user root;这样可以避免在后期nginx访问某些文件的时候出现权限问题,当然也可以选择给需要访问的文件给nginx权限。

一切配置完成之后,通过 sudo systemctl start nginx 即可启动nginx服务,此时没有异常则为启动正常(可以通过 sudo systemctl status nginx 查看nginx服务的状态),之后即可用浏览器访问 http://nginx配置文件中server_name指定的地址

后续问题

静态文件的访问

Django中的静态文件访问设置需要分两种情况进行讨论(更加settings.py中DEBUG配置的状态分),即 开发测试场景下 和 生产环境中。

开发测试场景下

DEBUG设置为True。
此场景下Django自带的服务器提供静态文件的代理服务,此时只需要设置 STATIC_URL 以及 STATICFILES_DIRS 即可完成静态文件的访问,并且此时Django自带的服务器提供静态文件的代理服务还会自动代理项目中使用的外部app自带的静态文件(且不需要额外的设置)。

1
2
3
4
5
STATIC_URL = '/static/'
# 全局静态文件配置,开发模式下用于一些公共静态文件的路径提供,因为不是在app下单独设置,开发模式下Django自带的静态文件代理无法定位到这些文件,需要使用STATICFILES_DIRS进行设置。
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'), # 指向项目下的static文件夹
]

注意:STATICFILES_DIRS 和 STATIC_ROOT 的配置不能相同,否则提示 (staticfiles.E002) The STATICFILES_DIRS setting should not contain the STATIC_ROOT setting.。一般情况下,在生产环境中所有的静态资源都会收集到一起放到统一的文件夹下,交由第三方服务代理,此时STATICFILES_DIRS也就不需要使用了。

生产环境下

DEBUG设置为False(此时注意:必须设置ALLOWED_HOSTS参数,用于设置允许访问当前网站的ip地址)
在Django部署上线之后,为了不对外暴露内部程序细节(测试开发过程中,DEBUG设置为True,异常会显示在浏览器),此时需要设置DEBUG为False,而一旦这样设置之后,Django将不再提供静态服务代理的功能,需要手动配置来完成。配置过程和media的配置相同(因为Django是不提供media文件的代理服务的,所以即使在开发阶段也是需要手动配置的)。
手动实现静态文件代理的方式有以下几种:

  1. 通过配置url进行访问(Django只推荐在开发过程中使用,生产环境下最好使用第2种)
    配置的过程和media的配置过程一样,配置内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # settings.py中的配置
    STATIC_URL = '/static/'
    STATIC_ROOT = os.path.join(BASE_DIR, 'static') # 指向项目下的static文件夹
    # media文件的路径
    MEDIA_URL = "/media/"
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 指向项目下的media文件夹


    # urls.py中的路由设置
    from MyProject.settings import STATIC_ROOT
    from MyProject.settings import STATIC_ROOT
    from django.views.static import serve # 用于提供静态文件的视图和功能。这些仅在开发过程中使用,不应在生产环境中使用。

    url(r'^media/(?P<path>.*)$', serve, {"document_root": MEDIA_ROOT}),
    url(r'^static/(?P<path>.*)$', serve, {"document_root": STATIC_ROOT}),
  2. 通过第三方服务器,如使用Nginx来提供静态文件的代理服务(Nginx拥有更高的静态资源处理性能)
    此时配置文件中只需要提供 STATIC_URLMEDIA_URL 两个配置,如下:
    1
    2
    STATIC_URL = '/static/'
    MEDIA_URL = '/media/'
    之后,Django所有静态资源的访问都将被转到Nginx,由Nginx提供。
1
2
3
4
5
6
7
8
# nginx下的静态文件代理配置
location /media {
alias /root/projects/MxShop/media; # 指向django项目下的media文件夹
}

location /static {
alias /root/projects/MxShop/static; # 指向django项目下的static文件夹
}

通过以上两种方式既可以完成静态文件的访问了,但是可能某些静态文件依然无法访问,这是因为在开发的时候,除了手动设置的用到的静态文件之外,还有一些安装的app也需要使用到一些静态文件,而这些静态文件一般是app自带的,安装在这些app目录下的static文件夹中,在开发环境下(DEBUG设置为True),Django会自动找到这些app的静态文件并提供相应的静态文件代理,生产环境下由于Django不再提供静态文件代理,我们所有的静态文件的访问路由都指向了项目更目录下的总的静态文件夹下,所以这些app自带的静态文件就无法访问(这也是Django在开发模式下提供自带的静态文件代理服务的原因以及优点),好在django为我们提供了静态文件的收集方案,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 只需要在当前项目的根目录下,使用 python manage.py collectstatic 命令即可完成所有静态文件的收集
# 注意,该条指令依赖STATIC_ROOT = os.path.join(BASE_DIR, 'static')的设置
$ python manage.py collectstatic

You have requested to collect static files at the destination
location as specified in your settings:

/root/projects/MyProject/static

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes

646 static files copied to '/root/projects/MyProject/static'.

收集完成之后,所有的静态文件都可以正常访问了。

STATIC_ROOT = os.path.join(BASE_DIR, 'static')的作用有两个:

  1. 用于使用 python manage.py collectstatic 命令完成所有静态文件的收集
  2. 用于在 DEBUG 设置为 False(说明此时是生产模式下,不打印异常栈)时,想由django实现静态文件的访问路由 url(r'^static/(?P<path>.*)$', serve, {"document_root": STATIC_ROOT})

异常

Server Error (500)

出现这种错误,一般是服务器内部,也就是代码出现错误,此时需要设置 DEBUG = True,开启调试模式,打印异常栈才能发现错误的地方。
像我就因为项目中使用了redis服务,但是因为 DEBUG 设置为 False,所以不知道错误在什么地方。

小结

  • 部署上线时,一定要设置DEBUG设置为False,防止错误栈对外输出,造成安全隐患。
  • Django开发模式下提供的静态文件代理服务为我们的开发提供了便利,不用考虑安装的额外app的静态文件的访问问题。
  • DEBUG设置为False后,Django不再提供静态文件代理,需要手动配置。
  • media文件不管在什么环境都需要手动设置,因为Django不提供高media文件的代理服务。
  • Ngingx拥有更高的静态资源处理性能,应该使用这种方式完成静态资源的获取。
  • 一定要使用collectstatic完成所有静态资源的收集,保证所有静态文件的正常访问。此外,collectstatic命令的实现离不开STATIC_ROOT的设置。

参考

uWSGI 文档:https://uwsgi-docs-zh.readthedocs.io/zh_CN/latest/WSGIquickstart.html

在之前的 Django REST framework的token原理和验证登录 中,使用了drf自带的Token认证类rest_framework.authentication.TokenAuthentication 实现了Token认证,但是drf自带的Token认证有以下几个问题:

  1. drf自带的token验证方式,token值是存储在生成的 authtoken_token 表中,是放在一台服务器上的,若采用分布式的话,还需要做数据的同步操作。
  2. 相比于 django_session 的设置,没有expire_date属性(过期时间),将是永久有效的,这样的话,一旦泄露将造成很大的安全问题。
  3. 和django_session存储同样的问题,随着用户的增多,token值会占用服务器大量空间,同时也会加大数据库的查询压力,导致性能下降。

为了解决以上的问题,将使用第三方的开源库djangorestframework-jwt完成这一功能。

JWT原理

参考文章:前后端分离之JWT用户认证

摘要

  1. HTTP协议的无状态存储,需要提供用户登录的验证功能。
  2. 前后端分离系统的传统解决方式是:前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

  • 简洁(Compact)
    可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
  • 自包含(Self-contained)
    负载中包含了所有用户所需要的信息,避免了多次查询数据库

组成

JWT由三部分组成

  • HEAD 头部: 包含了两部分,token 类型和采用的加密算法
  • Payload 负载: 我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。默认情况下负载中的内容使用的是 base64 编码方式,是可逆的,所以任何人都可以解读该部分的内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露
  • Signature 签名: 保证内容没有被篡改。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥(存放在服务端),然后使用 header 中指定的签名算法(HS256)进行签名。生成的方式:
1
2
# python实现 
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) # secret密钥保存在服务器端,且不对外公布

完整的JWT构成:base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + Signature 签名

验证过程

由前端和后端共同完成
jwt认证过程

  • 前端进行登录,提供账号和密码在后端进行验证,后端对其验证通过后生成JWT,再发送给前端,前端保存在本地(一般是存储在本地的cookie中(推荐使用该种方式,只是使用cookie的存储机制),或者是保存在localStorage或sessionStorage上,退出登录时由前端删除保存的JWT即可)
  • 前端在跳转到登录页或者请求API的时候,会发送JWT,到达后端时被过滤器拦截进行JWT的验证(在Django可以交给中间件完成),验证通过之后请求被传送给view,未通过,返回错误信息(可以交给Django中间件中的process_exception函数处理),跳转到登录页面。(前面的所有的Django中间件的功能都将由即将使用到的 rest_framework_jwt.authentication.JSONWebTokenAuthentication 来完成)

JWT的优势

  • 完全使用算法进行验证,加密解密取得用户ID(相较于sessionid,后者还多了向服务器请求对应session_data的步骤),通过用户ID查询到用户信息,这意味着不需要存储token表,也不需要Django之中的session表,会减少服务器的存储压力
  • 适用于单点登录

JWT详细解读:https://www.cnblogs.com/DeadBoy/p/11481146.html

Django REST framework实现JWT的验证登录功能

使用第三方库djangorestframework-jwt完成验证,使用方法详见官方文档

安装后的配置过程

  1. 安装完成后,首先需要在REST_FRAMEWORK的DEFAULT_AUTHENTICATION_CLASSES 中配置
1
2
3
4
5
6
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
...
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],
}

和drf自带的token功能使用的rest_framework.authentication.TokenAuthentication作用一致,是对用户post过来的token进行验证,验证完成后返回user。该设置只有在需要用户访问view,发送token进行验证的时候才需要设置,单纯的提供token值是不需要设置的(只需要完成步骤2)。
另外,这个值,最好不要在settings.py中进行设置,而只在需要使用的view中进行设置,因为有些页面属于公共资源,不需要验证token也可以进行访问。通过设置authentication_classes参数进行配置:

1
2
# views.py
authentication_classes = (JSONWebTokenAuthentication,)
  1. 在urls.py中添加url
1
2
3
4
5
6
from rest_framework_jwt.views import obtain_jwt_token

url(r'^api-token-auth/', obtain_jwt_token) # jwt的认证接口,是一个post请求。可以自定义url地址,如jwt-auth
# 这里注意区分url(r'^api-token-auth/', views.obtain_auth_token),这是drf自带的认证模式
# 自定义的jwt接口
url(r'^jwt-auth/', obtain_jwt_token)

此时访问http://xxx/jwt-auth,使用post请求传递用户信息(用户名和密码),验证通过之后就会直接返回一个jwt,采用的返回方式是k/v形式的json对象:

1
2
3
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3NjMyODgzLCJlbWFpbCI6IjEyMzRAcXEuY29tIn0.YIcAYZC3aY0AsoCyRDR78-1D-zdIGN3jHZHqgBWtjxo"
}

当传递的用于验证用户的信息(username和password)错误的时候,返回以下JSON数据:

1
2
3
4
5
{
"non_field_errors": [
"无法使用提供的认证信息登录。"
]
}
  1. 验证方式(注意和drf自带的token验证方式的不同书写)
    • 在http Headers中通过 Authorization: JWT <your_token>格式传递获得的JWT
    • 打断点,请求设置了 authentication_classes = (JSONWebTokenAuthentication,) 的view,查看request属性,可以发现用户存储在request中的user属性中,JWT值存储在auth属性中
    • JWT过期之后,返回的JSON格式数据
    1
    2
    3
    4
    5
    {
    "non_field_errors": [
    "无法使用提供的认证信息登录。"
    ]
    }

和前端vue登录功能的集成

直接将url(r'^api-token-auth/', obtain_jwt_token)的url地址改为url(r'^login/', obtain_jwt_token),变成登陆接口,前端向后端传递用户名和密码请求登陆,后端接受到请求后验证,验证通过返回JWT,前端保存,之后的每次请求,发送该JWT,验证用户,完成登录。
详细过程:
后端验证用户信息返回JWT:
url 请求的 view 是 rest_framework_jwt 中的 ObtainJSONWebToken,该 view 继承自 JSONWebTokenAPIView,使用的序列化类是 JSONWebTokenSerializer,该序列化类中通过重写 validate() 方法,进行用户的身份校验,验证通过后进行 token 的构建,并和用户名组合为字典存放到 _validated_data 参数中。

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
# 源码位于 rest_framework_jwt/serializers.py
# JSONWebTokenSerializer中的validate()方法
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}

if all(credentials.values()):
# 调用django的用户认证方法进行验证
user = authenticate(**credentials)

if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)

# 用户认证通过后进行token的构造,以下两个方法也是token构造的主要方法
payload = jwt_payload_handler(user)
return {
# 将token和用户对象存储到一起,通过validate()最终返回给 _validated_data
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)

JSONWebTokenAPIView 中获取到生成的 token,通过可选的方式(通过判断 api_settings.JWT_AUTH_COOKIE 的值决定是否通过 cookie 的方式进行传递 token 值,默认是 None,也就是不通过 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
# 源码位于 rest_framework_jwt/views.py
class JSONWebTokenAPIView(APIView):
...
def post(self, request, *args, **kwargs):
# 调用 JSONWebTokenSerializer 进行数据的序列化
serializer = self.get_serializer(data=request.data) # request为drf重写,内部通过data属性保存传递过来的值

if serializer.is_valid(): # 进行验证(validata中进行了用户验证),验证通过之后,返回生成的token值和user属性
# serializer.object属性为rest_framework_jwt设置的属性方法,之际返回的是self.validated_data,也就是_validated_data中的值
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
# jwt_response_payload_handler方法用于自定义响应的内容,默认的只是返回{token:'token值'}
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
# 判断JWT_AUTH_COOKIE是否设置,默认为None,表示不使用cookie的方式,设置的时候为cookie中存储的名称
if api_settings.JWT_AUTH_COOKIE:
# 设置过期时间,需要使用到api_settings.JWT_EXPIRATION_DELTA的配置,默认为300s
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
# 设置cookie, 使用WT_AUTH_COOKIE设置的名称,token,过期时间
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True) # 如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击,窃取cookie内容,这样就增加了cookie的安全性
return response

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

vue前端获得返回的JWT:

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
// 登录
export const login = params => {
return axios.post(`${host}/login/`, params)
}
// 登录调用的方法
login({
// vue传递 用户名和密码
username:this.userName,
password:this.parseWord
}).then((response)=> {
console.log(response);
// 验证通过之后,本地存储返回的信息
// 当前示例的返回的信息中由于使用的是djangorestframework_jwt默认的jwt_response_payload_handler(token, user, request),该方法中只返回了token,所以name不通过后端获得,由前端获取
cookie.setCookie('name',this.userName,7);
// 设置token,同时设置过期时间,这是因为djangorestframework_jwt使用非cookie形式传递token时,没有传递过期时间值,此时需要设置cookie的过期时间,
// 但是要注意,传递的token中在编码的时候,是有过期时间这个属性的,用于后端在接收到该token进行过期验证的,和这里的cookie的过期时间无关,token的构造参见 jwt_payload_handler(user) 的实现,由该方法定义。
// 此处定义的cookie的过期时间是用在前端浏览器中设置cookie保存的时间,JWT也有单独设置的过期时间用于保证JWT的时效性,这是在生成token的时候就定义并存储好的,
// 另外,此处cookie过期时间最好和JWT设置的过期时间保持一直,应小于等于JWT的值。
cookie.setCookie('token',response.data.token,7)
// 存储在store,更新store数据
that.$store.dispatch('setInfo');
// 获得返回值,说明用户验证通过,此时跳转到首页
this.$router.push({ name: 'index'})
})
.catch( function (error) {
// 异常处理
...
});

本例中使用的是非cookie传递的方式,后端直接将JWT通过Response进行返回,然后在Vue中通过response.data读取属性的方式获取到JWT并存储在cookie中,这里的cookie只起到了数据存储的作用,vue中之后会使用 cookie.getCookie(‘token’) 的方式读取数据使用。

vue 传递 token 值给后端,用于身份验证:
根据 djangorestframework_jwt 文档要求,需要以 Authorization: JWT <your_token> 的形式进行传递,所以在前端设置 request拦截器,为 request 请求添加该信息

1
2
3
4
5
6
7
8
9
10
11
// vue设置http request拦截器
axios.interceptors.request.use(
config => {
if (store.state.userInfo.token) { // 判断是否存在token,如果存在的话,则每个http header设置Authorization,添加token值
config.headers.Authorization = `JWT ${store.state.userInfo.token}`;
}
return config;
},
err => {
return Promise.reject(err);
});

通过这种方式向后端传递JWT,
后端通过设置 authentication_classes = (JSONWebTokenAuthentication,) 进行JWT认证
JSONWebTokenAuthentication 继承自 BaseJSONWebTokenAuthentication,该父类中提供了 authenticate() 方法用于验证用户,内部通过 get_jwt_value(request) 方法获得前端传递过来的token,内部通过调用 jwt_decode_handler(jwt_value) 反解 token 获得其中的 payload(该操作与之前的 jwt_encode_handler(payload) 相对),再通过 authenticate_credentials(payload) 获得user。

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
# 位于 rest_framework/authentication.py
# JSONWebTokenAuthentication 中的 get_jwt_value 调用该方法获得请求头中的 Authorization 属性
def get_authorization_header(request):
auth = request.META.get('HTTP_AUTHORIZATION', b'') # 获得 Authorization 中的值
if isinstance(auth, str):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth

# 位于 rest_framework_jwt/authentication.py
# JSONWebTokenAuthentication 中的 get_jwt_value
def get_jwt_value(self, request):
auth = get_authorization_header(request).split() # 通过split得到列表 ['JWT', 'token值']
auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

if not auth: # 如果auth中没有值,则可能是通过cookie传递,查看 JWT_AUTH_COOKIE 是否设置
if api_settings.JWT_AUTH_COOKIE:
# 如果通过验证,则说明在之前是通过cookie的方式将JWT传递给前端的,那么这里也就一定会通过cookie返回了token,则之后的请求是一定会带上该cookie的,此时就可以直接获取并返回,不需要再进行下面的验证了。
return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
return None

# 验证token 的前缀,这里使用的默认的 JWT
if smart_text(auth[0].lower()) != auth_header_prefix:
return None
# auth中获得值是 ['JWT', 'token值'] 形式,长度一定是为2的
if len(auth) == 1:
msg = _('Invalid Authorization header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid Authorization header. Credentials string '
'should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
return auth[1] # 返回token值

# JSONWebTokenAuthentication 从父类 BaseJSONWebTokenAuthentication 中继承过来的验证方法 autheiticate(self, request)
def authenticate(self, request):
jwt_value = self.get_jwt_value(request) # 获得前端传递过来的token值
if jwt_value is None:
return None

try:
# 调用jwt_decode_handler(jwt_value)反解token获得其中的payload
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
msg = _('Signature has expired.')
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Error decoding signature.')
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()

# 使用反解得到的payload获得存储的用户
user = self.authenticate_credentials(payload)

return (user, jwt_value)

可以定制的方法

jwt_response_payload_handler(token, user, request)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def jwt_response_payload_handler(token, user=None, request=None):
"""
可以自定义响应的内容,用于返回,可以定制要返回的数据,用于前端的获取
默认只返回了token值
Example:

def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token,
'user': UserSerializer(user, context={'request': request}).data
}

"""
return {
'token': token
}

可以通过在settings.py中配置,用于定制返回的内容,默认只返回了token值。

1
2
3
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': '指定处理函数',
}

djangorestframework_jwt 配置

提供配置文件的方式,方便定制JWT的某些功能,除了上面的指定方法外,常用的配置还有以下几个:

1
2
3
4
5
6
import datetime

JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 设置JWT的过期时间
'JWT_AUTH_HEADER_PREFIX': 'JWT', # 指定接受token的前缀,常用的有Token和JWT两种
}

默认的 JWT_AUTH_HEADER_PREFIX 就是设置的 JWT 前缀,该设置在后端传递给前端token时并不其作用,而是在前端传递给后端的时候用到,用于在后端读取前端设置在 headers.Authorization 中的 Authorization: JWT 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b 时,验证前缀是否为settings.py中设置的前缀,所以该参数的设置只在后台验证的时候才会进行。

token功能的项目代码内嵌

有这样一种功能需求,我们在系统的注册功能中,当我们注册之后应该直接完成登录,显示登录状态,可以发现这部分的前端处理逻辑和登录功能的是类似,在跳转页面的时候也需要向后端传递token以完成身份的验证,但是djangorestframework_jwt只提供了login的url,并没有注册的,此时就需要我们手动在注册完成之后,手动构造符合djangorestframework_jwt要求的token并返回。
通过以上代码的分析,token的构造主要依赖两个方法,jwt_payload_handler(user)jwt_encode_handler(payload), 所以我们可以重写注册过程中需要使用到的 mixins.CreateModelMixin 中的create()方法,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def create(self, request, *args, **kwargs):
"""
为了完成配合前端实现的 注册完成后即自动登录的要求
重载create方法,完成token的返回
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)

# 重载perform_create方法,使其返回创建的实例对象,这里获取到该返回值
user = self.perform_create(serializer)
re_dict = serializer.data

# 分析jwt源码,调用源码组件,完成token的构建
payload = jwt_payload_handler(user)
# 获取返回的对象serializer.data,并添加token字段,由前端获取到
re_dict["token"] = jwt_encode_handler(payload)
re_dict["name"] = user.name if user.name else user.username
headers = self.get_success_headers(serializer.data)

# 返回修改后的值
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

登录状态和身份验证的关系

注意这里要区分 登录状态 和 身份验证的关系:
身份验证是前端传递相关数据,如用户信息,或者通过session、token等认证机制判断当前用户是谁,以及获得用户的信息。
登录状态是前端的表现,如用户通过身份认证后会得到相应的信息作为标记,前端通过判断这个标记,显示是否为登录状态。

session和token的区别

  • token 和 session 其实都是为了身份验证,session 一般翻译为会话,而 token 更多的时候是翻译为令牌。
  • session 服务器会保存一份,可能保存到缓存,文件,数据库,token 通过算法生成以及验证,后端不进行存储。
  • 其实 token 与 session 的问题是一种时间与空间的博弈问题,session 是空间换时间,而 token 是时间换空间。两者的选择要看具体情况而定。虽然确实都是“客户端记录,每次访问携带”,但 token 很容易设计为自包含的,也就是说,后端不需要记录什么东西,每次一个无状态请求,每次解密验证,每次当场得出合法/非法的结论。这一切判断依据,除了固化在 CS 两端的一些逻辑之外,整个信息是自包含的。这才是真正的无状态。而 sessionid ,一般都是一段随机字符串,需要到后端去检索 id 的有效性。万一服务器重启导致内存里的 session 没了呢?万一 redis 服务器挂了呢?

drf的登陆验证功能

drf自带的登陆功能(drf Api Root中使用),通过定义路径访问 rest_framework.urls 中设置的路由进行访问

1
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),

从子路径的实现

1
2
url(r'^login/$', views.LoginView.as_view(template_name='rest_framework/login.html'), name='login'),
url(r'^logout/$', views.LogoutView.as_view(), name='logout'),

可以发现,内部实现实际上是调用了Django的原生类 LoginViewLogoutViewLoginView 中添加了 @method_decorator(csrf_protect) 装饰器,所以在drf的API Root页面使用了csrf_token 标签,该标签会在渲染模版的时候替换为 <input type="hidden" name="csrfmiddlewaretoken" value="服务器随机生成的token值"> ,配合 django.middleware.csrf.CsrfViewMiddleware 中间件防止csrf(跨站请求伪造),但是目前是一个前后端分离的系统,就不需要做csrf的验证(因为前后端分离的系统中,已经产生了跨站的问题,app和网站肯定不是一个站点,已经是跨站了),所以在做drf开发的时候,不需要关系csrf验证的问题。
综上,直接调用 LoginView 进行登录认证是不适合在前后端分离的系统中使用的。

前后端分离的验证方式

drf中的Authentication提供了一种 request 和 用户身份 绑定的机制。
使用方式:

  1. 设置setting,设置认证方式,作用方式和Django中的中间件类似。
1
2
3
4
5
6
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [ # 设置权限验证的类,默认为以下两个类
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}

rest_framework.authentication.SessionAuthentication 内部的实现很简单,只是从request中取出了user,实际还是依赖django的django.contrib.sessions.middleware.SessionMiddleware和django.contrib.auth.middleware.AuthenticationMiddleware这两个中间件。

  1. 添加drf自带的Token认证类 rest_framework.authentication.TokenAuthentication,使用该类时,需要在 INSTALLED_APPS 中进行注册'rest_framework.authtoken',因为需要使用数据表,生成authtoken_token表(所以在INSTALLED_APPS注册完之后,还需要使用 migrate 命令生成数据表),该表还用三个字段(key、created、user_id),通过 user_id 自动和用户表进行关联,保证一个用户对应着一个token值。
    在创建用户的时候,不会自动生成token的key值,需要手动调用方法进行生成,手动生成的方式有几种:

    • 调用提供的 Token 类生成

      1
      2
      3
      4
      from rest_framework.authtoken.models import Token

      token = Token.objects.create(user=...)
      print(token.key)
    • 使用官方提供的url,url(r'^api-token-auth/', views.obtain_auth_token),发送post请求,会返回相应的token,若没有则生成后返回,并自动和相应的用户进行关联(需要注意的该url请求是post请求,需要传递用户名和密码进行验证)。

  2. 认证
    需要手动在header中传递 Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b 。通过DEFAULT_AUTHENTICATION_CLASSES 的作用之后,就会获取到用户,然后将用户存储在request中的user属性中,token值存储在auth属性中。

token的实际应用

问题:有这样的使用场景,若访问一个公共数据页面,一般的,这种公共页面即使是在用户未登录(没有传递token),或者用户传递的token产生错误的情况下也是可以访问的,该如何解决呢?

可以通过前端,也可以在后端解决。

后端的方式:
其实可以不需要采用在 settings.py 中设置全局的 DEFAULT_AUTHENTICATION_CLASSES 的方式,而是在需要进行token验证的view中使用 authentication_classes 属性指定是否需要使用token验证(该属性在基础类APIView中设置了,authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES)。

drf的配置,多数都可以选择是在settings.py中配置还是在view中进行配置。settings.py中的是全局的配置,view中的设置是局部的,同时设置时,view中的设置优先于全局的设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
from rest_framework.authentication import TokenAuthentication

class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
商品列表页, 分页, 搜索, 过滤, 排序
"""
...
# 注意是classes,可接受多个值,所以采用元组的数据格式,另外,当元组只有一个数据的时候,需要使用逗号
# view中的局部设置会覆盖全局的设置
authentication_classes = (TokenAuthentication, )
permission_classes = (IsAuthenticated,) # 只有设置了IsAuthenticated,才会进行身份验证
...

问题

  1. 目前,使用的token验证方式,token值是存储在生成的 authtoken_token 表中,是放在一台服务器上的,若采用分布式的话,还需要做数据的同步操作。
  2. 相比于 django_session 的设置,没有expire_date属性(过期时间),将是永久有效的,这样的话,一旦泄露将造成很大的安全问题。
  3. 和 django_session 存储同样的问题,随着用户的增多,token值会占用服务器大量空间,同时也会加大数据库的查询压力,导致性能下降。

解决措施:使用JWT用户认证

Django自带的认证系统通过调用 `django.contrib.auth.authenticate()` 方法实现,该方法是对外提供的可调用的统一接口,Django内部维护了一个认证列表,通过 settings.AUTHENTICATION_BACKENDS 配置进行设置,默认调用的是 `django.contrib.auth.backends.ModelBackend` 。

阅读全文 »

redis通过`maxmemory`配置选项来配置redis的存储数据所能使用的最大的内存限制(可以通过通过修改`redis.conf`或者在redis-cli中使用`config set`来进行配置),如果我们要配置的内存上限是100M的Redis缓存,那么我们可以在 redis.conf 配置 `maxmemory 100mb`。

阅读全文 »

SqlAlchemy 库用于与各种数据库交互,你可使用一种类似于 Python 类和语句的方式创建数据模型和查询。使用 SqlAlchemy 的首要原因是,它将你的代码从底层数据库及其相关的 SQL 特性中抽象出来。SqlAlchemy 使用功能强大的常见语句和类型,确保其 SQL 语句为美中数据库和供应商正确、高效地构建出来,而你无需考虑这些。

阅读全文 »