Flask是目前十分流行的web框架,采用Python编程语言来实现相关功能。它被称为微框架(microframework),“微”并不是意味着把整个Web应用放入到一个Python文件,微框架中的“微”是指Flask旨在保持代码简洁且易于扩展,Flask框架的主要特征是核心构成比较简单,但具有很强的扩展性和兼容性,程序员可以使用Python语言快速实现一个网站或Web服务。
终端神器tmux命令提示
Tmux 就是会话与窗口的"解绑"工具,将它们彻底分离,提供了强大的终端复用以及会话保存功能。
Django+Vue 前后端分离项目的跨域请求问题
解决在先后端分离的项目中,由于跨端口导致的跨域请求问题...
Django静态资源配置
¶静态文件
静态文件的配置,一般需要使用到以下几个参数
1 | STATIC_URL |
¶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就是这样设计的:
此外,设置了该参数之后,在模板层通过引入 { % load staticfiles % }
[1]标签,可以在模板中使用 href="{ % static 'css/reset.css' % }"
[1:1] 构建静态资源路径的构建,这样当对 STATIC_URL
指定的静态文件的根名方法改变的时候,不需要修改模板中的访问路径,依然能够访问,效果和{ % url '' % }
[1:2]类似。
¶STATICFILES_DIRS
只在开发模式下有效,对于一些公共的静态文件,或者在项目中自建app的时候不想为每个app单独设置静态文件夹,为了方便管理,可以单独在项目的根目录下设置一个文件夹统一管理所有的静态文件(但是这个文件夹无法被开发模式下Django自带的静态文件代理服务定位到,所以必须使用STATICFILES_DIRS进行设置),设置如下:
1 | # 全局静态文件访问配置 |
这样在项目根目录下创建 集中管理的文件名
文件,在里面放置静态文件,此时静态文件的访问地址是 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文件夹下。
¶小结
- 无论是开发模式还是生产环境下,Django项目都依赖STATIC_URL=’/static/'的设置,因为这提供了静态文件访问的映射关系,有了他,Django才能够访问静态文件。
- STATICFILES_DIRS用于指定在项目根目录下统一管理的静态文件夹的名字,但只是在开发过程中有效以及使用,生产环境下,所有的静态文件都将被收集到统一的目录下(包括 STATICFILES_DIRS 的),交给第三方静态服务器调度提供。
- 若存在app下的static和STATICFILES_DIRS设置中同名的文件,默认访问的是STATICFILES_DIRS设置的静态文件下的,所以在app下的static中再设置一个app名的文件夹对静态文件管理,这样在访问时加上app名127.0.0.1:8000/static/app名,这样可以有效避免一些重名问题。
- 关于static目录的位置设置使用建议,若website中的每个app的独立性非常强的话,就在每个单独的app下设置static目录,这样app就会支持热插拔,独立性强。若app的独立性不强,app之间相互联系的话,建议使用集中管理static的方法。但是无论使用哪种方式,在项目部署的时候,都会进行静态文件的收集,由第三方静态服务器管理。
¶media文件
设置和static类似,因为在开发模式下(DEBUG需要设置为True),Django并不提供media文件的代理服务,所以即使在开发模式下,也需要手动设置medai文件的代理。
¶配置
需要同时设置MEDIA_URL和MEDIA_ROOT
1 | # 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 | # 在项目urls.py中设置url |
1 | from django.views.static import 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 | TEMPLATES = [ |
django中已经提供好了django.template.context_processors.media
只要添进去即可
我们看一下这个函数media的逻辑:
1 | def media(request): |
逻辑相当简单,这样我们也可以模仿使用类似的方式定义我们自己的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,即可自动拼接成图片相应的访问地址。
阿里云部署Django项目
使用 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服务器,这样可以达到很好的客户端响应。
¶完整的访问过程
- Nginx接收到浏览器的http请求,将包进行解析,分析url,如果是静态文件直接访问用户给Nginx配置静态文件目录,如果不是静态文件,是一个动态请求,Nginx会转发给uWSGI。
- uWSGI接到请求会进行处理成WSGI可以接受的形式,并发给WSGI。
- 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 | 在/etc/my.cnf中需要设置 |
数据库的迁移
1 | # 将本地的数据库(music_db)导出为sql文件(music_db.sql) |
¶nginx
1 | sudo yum install epel-release |
¶python环境
¶uWSGI
使用 pip install uwsgi
安装
在项目的根目录下,通过 uwsgi --http :8001 --module 项目名.wsgi
运行项目(使用的是项目下的wsgi.py文件,需要文件内包含一个WSGIapplication对象),以http的方式,让uwsgi启动我们的项目。
注意:端口需要在服务器的 安全策略组 中进行了开放
当输出以下内容时,说明项目启动成功
1 | ... |
¶配置
在Django项目根目录下创建conf文件夹,文件夹下创建uwsgi文件夹和nginx文件夹,分别用于存储uwsgi和nginx的配置文件。
¶nginx
配置nginx文件,后缀名为 .conf,如myproject_nginx.conf
1 | # the upstream component nginx needs to connect to |
使用nginx之后,项目中的静态文件就交给nginx代理了,而不使用Django自带的代理,得益与nginx底层的io多路复用,nginx的性能高。
注意区分:listen监听的这个端口是外部浏览器访问nginx的端口,而uwsgi_pass这个端口是nginx访问uwsgi的端口,所以设置需要和uwsgi.ini配置文件中的socket指定的路径一致。
¶uwsgi配置
配置uwsgi文件,后缀名为 .ini,如uwsgi.ini
1 | # mysite_uwsgi.ini file |
注意:以上的配置启动后,配合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 | ... |
¶启动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 | STATIC_URL = '/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文件的代理服务的,所以即使在开发阶段也是需要手动配置的)。
手动实现静态文件代理的方式有以下几种:
- 通过配置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}), - 通过第三方服务器,如使用Nginx来提供静态文件的代理服务(Nginx拥有更高的静态资源处理性能)
此时配置文件中只需要提供STATIC_URL
和MEDIA_URL
两个配置,如下:之后,Django所有静态资源的访问都将被转到Nginx,由Nginx提供。1
2STATIC_URL = '/static/'
MEDIA_URL = '/media/'
1 | # nginx下的静态文件代理配置 |
通过以上两种方式既可以完成静态文件的访问了,但是可能某些静态文件依然无法访问,这是因为在开发的时候,除了手动设置的用到的静态文件之外,还有一些安装的app也需要使用到一些静态文件,而这些静态文件一般是app自带的,安装在这些app目录下的static文件夹中,在开发环境下(DEBUG设置为True),Django会自动找到这些app的静态文件并提供相应的静态文件代理,生产环境下由于Django不再提供静态文件代理,我们所有的静态文件的访问路由都指向了项目更目录下的总的静态文件夹下,所以这些app自带的静态文件就无法访问(这也是Django在开发模式下提供自带的静态文件代理服务的原因以及优点),好在django为我们提供了静态文件的收集方案,如下:
1 | # 只需要在当前项目的根目录下,使用 python manage.py collectstatic 命令即可完成所有静态文件的收集 |
收集完成之后,所有的静态文件都可以正常访问了。
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
的作用有两个:
- 用于使用
python manage.py collectstatic
命令完成所有静态文件的收集 - 用于在 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之JWT用户验证
在之前的 Django REST framework的token原理和验证登录 中,使用了drf自带的Token认证类rest_framework.authentication.TokenAuthentication
实现了Token认证,但是drf自带的Token认证有以下几个问题:
- drf自带的token验证方式,token值是存储在生成的
authtoken_token
表中,是放在一台服务器上的,若采用分布式的话,还需要做数据的同步操作。 - 相比于 django_session 的设置,没有expire_date属性(过期时间),将是永久有效的,这样的话,一旦泄露将造成很大的安全问题。
- 和django_session存储同样的问题,随着用户的增多,token值会占用服务器大量空间,同时也会加大数据库的查询压力,导致性能下降。
为了解决以上的问题,将使用第三方的开源库djangorestframework-jwt完成这一功能。
¶JWT原理
参考文章:前后端分离之JWT用户认证
¶摘要
- HTTP协议的无状态存储,需要提供用户登录的验证功能。
- 前后端分离系统的传统解决方式是:前端登录,后端根据用户信息生成一个
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 | # python实现 |
完整的JWT构成:base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + Signature 签名
¶验证过程
由前端和后端共同完成
- 前端进行登录,提供账号和密码在后端进行验证,后端对其验证通过后生成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
完成验证,使用方法详见官方文档
¶安装后的配置过程
- 安装完成后,首先需要在REST_FRAMEWORK的
DEFAULT_AUTHENTICATION_CLASSES
中配置
1 | REST_FRAMEWORK = { |
和drf自带的token功能使用的rest_framework.authentication.TokenAuthentication
作用一致,是对用户post过来的token进行验证,验证完成后返回user。该设置只有在需要用户访问view,发送token进行验证的时候才需要设置,单纯的提供token值是不需要设置的(只需要完成步骤2)。
另外,这个值,最好不要在settings.py中进行设置,而只在需要使用的view中进行设置,因为有些页面属于公共资源,不需要验证token也可以进行访问。通过设置authentication_classes
参数进行配置:
1 | # views.py |
- 在urls.py中添加url
1 | from rest_framework_jwt.views import obtain_jwt_token |
此时访问http://xxx/jwt-auth
,使用post请求传递用户信息(用户名和密码),验证通过之后就会直接返回一个jwt,采用的返回方式是k/v形式的json对象:
1 | { |
当传递的用于验证用户的信息(username和password)错误的时候,返回以下JSON数据:
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": [
"无法使用提供的认证信息登录。"
]
} - 在http Headers中通过
¶和前端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 | # 源码位于 rest_framework_jwt/serializers.py |
JSONWebTokenAPIView 中获取到生成的 token,通过可选的方式(通过判断 api_settings.JWT_AUTH_COOKIE 的值决定是否通过 cookie 的方式进行传递 token 值,默认是 None,也就是不通过 cookie 进行传递值)
1 | # 源码位于 rest_framework_jwt/views.py |
vue前端获得返回的JWT:
1 | // 登录 |
本例中使用的是非cookie传递的方式,后端直接将JWT通过Response进行返回,然后在Vue中通过response.data读取属性的方式获取到JWT并存储在cookie中,这里的cookie只起到了数据存储的作用,vue中之后会使用 cookie.getCookie(‘token’) 的方式读取数据使用。
vue 传递 token 值给后端,用于身份验证:
根据 djangorestframework_jwt 文档要求,需要以 Authorization: JWT <your_token>
的形式进行传递,所以在前端设置 request拦截器,为 request 请求添加该信息
1 | // vue设置http request拦截器 |
通过这种方式向后端传递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 | # 位于 rest_framework/authentication.py |
¶可以定制的方法
jwt_response_payload_handler(token, user, request)
1 | def jwt_response_payload_handler(token, user=None, request=None): |
可以通过在settings.py中配置,用于定制返回的内容,默认只返回了token值。
1 | JWT_AUTH = { |
¶djangorestframework_jwt 配置
提供配置文件的方式,方便定制JWT的某些功能,除了上面的指定方法外,常用的配置还有以下几个:
1 | import datetime |
默认的 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 | def create(self, request, *args, **kwargs): |
¶登录状态和身份验证的关系
注意这里要区分 登录状态 和 身份验证的关系:
身份验证是前端传递相关数据,如用户信息,或者通过session、token等认证机制判断当前用户是谁,以及获得用户的信息。
登录状态是前端的表现,如用户通过身份认证后会得到相应的信息作为标记,前端通过判断这个标记,显示是否为登录状态。
¶session和token的区别
- token 和 session 其实都是为了身份验证,session 一般翻译为会话,而 token 更多的时候是翻译为令牌。
- session 服务器会保存一份,可能保存到缓存,文件,数据库,token 通过算法生成以及验证,后端不进行存储。
- 其实 token 与 session 的问题是一种时间与空间的博弈问题,session 是空间换时间,而 token 是时间换空间。两者的选择要看具体情况而定。虽然确实都是“客户端记录,每次访问携带”,但 token 很容易设计为自包含的,也就是说,后端不需要记录什么东西,每次一个无状态请求,每次解密验证,每次当场得出合法/非法的结论。这一切判断依据,除了固化在 CS 两端的一些逻辑之外,整个信息是自包含的。这才是真正的无状态。而 sessionid ,一般都是一段随机字符串,需要到后端去检索 id 的有效性。万一服务器重启导致内存里的 session 没了呢?万一 redis 服务器挂了呢?
Django REST framework的token原理和验证登录
¶drf的登陆验证功能
drf自带的登陆功能(drf Api Root中使用),通过定义路径访问 rest_framework.urls
中设置的路由进行访问
1 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), |
从子路径的实现
1 | url(r'^login/$', views.LoginView.as_view(template_name='rest_framework/login.html'), name='login'), |
可以发现,内部实现实际上是调用了Django的原生类 LoginView
和 LogoutView
。LoginView
中添加了 @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 和 用户身份 绑定的机制。
使用方式:
- 设置setting,设置认证方式,作用方式和Django中的中间件类似。
1 | REST_FRAMEWORK = { |
rest_framework.authentication.SessionAuthentication
内部的实现很简单,只是从request中取出了user,实际还是依赖django的django.contrib.sessions.middleware.SessionMiddleware和django.contrib.auth.middleware.AuthenticationMiddleware这两个中间件。
-
添加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
4from 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请求,需要传递用户名和密码进行验证)。
-
-
认证
需要手动在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 | from rest_framework.authentication import TokenAuthentication |
¶问题
- 目前,使用的token验证方式,token值是存储在生成的
authtoken_token
表中,是放在一台服务器上的,若采用分布式的话,还需要做数据的同步操作。 - 相比于 django_session 的设置,没有expire_date属性(过期时间),将是永久有效的,这样的话,一旦泄露将造成很大的安全问题。
- 和 django_session 存储同样的问题,随着用户的增多,token值会占用服务器大量空间,同时也会加大数据库的查询压力,导致性能下降。
解决措施:使用JWT用户认证
Django自定义用户认证系统
Django自带的认证系统通过调用 `django.contrib.auth.authenticate()` 方法实现,该方法是对外提供的可调用的统一接口,Django内部维护了一个认证列表,通过 settings.AUTHENTICATION_BACKENDS 配置进行设置,默认调用的是 `django.contrib.auth.backends.ModelBackend` 。
Redis如何有效避免内存耗尽
redis通过`maxmemory`配置选项来配置redis的存储数据所能使用的最大的内存限制(可以通过通过修改`redis.conf`或者在redis-cli中使用`config set`来进行配置),如果我们要配置的内存上限是100M的Redis缓存,那么我们可以在 redis.conf 配置 `maxmemory 100mb`。
SqlAlchemy 使用
SqlAlchemy 库用于与各种数据库交互,你可使用一种类似于 Python 类和语句的方式创建数据模型和查询。使用 SqlAlchemy 的首要原因是,它将你的代码从底层数据库及其相关的 SQL 特性中抽象出来。SqlAlchemy 使用功能强大的常见语句和类型,确保其 SQL 语句为美中数据库和供应商正确、高效地构建出来,而你无需考虑这些。