关于JWT(Json Web Token)是一种较新的用户认证方式,官网在这里 ,网上有篇中文解释写的很好,点此跳转 。
用户认证(Authentication)和用户授权(Authorization)是两个不同的概念,认证解决的是“有没有”的问题,而授权解决的是“能不能”的问题。
一般用到JWT认证的情况大多都是配合REST框架使用,比如我大Django的Django-REST-framework框架,就已经有了现成的三方库django-rest-framework-jwt 。不过这个库默认只支持基于Header
传递信息,所以改成基于Cookie方式还需要我们来手动处理一下。
关于安装,直接使用pip安装即可,在settings.py
中,先来修改django-restframework的基本配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES' : [ 'rest_framework.permissions.IsAuthenticated' , ], 'DEFAULT_AUTHENTICATION_CLASSES' : ( 'app.myauth.CookieAuthentication' , ), 'PAGE_SIZE' : 20 , } JWT_AUTH = { 'JWT_EXPIRATION_DELTA' : datetime.timedelta(seconds=300 ), 'JWT_AUTH_HEADER_PREFIX' : 'ABC' , }
至于JWT_AUTH
更多的配置见官网,比如使用什么加密算法等等。
接下来编写认证代码,这里逻辑很简单,app/myauth.py
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from rest_framework_jwt.settings import api_settingsfrom rest_framework_jwt.authentication import BaseJSONWebTokenAuthenticationclass CookieAuthentication (BaseJSONWebTokenAuthentication) : """自定义JWT认证,从cookie中获取认证信息""" def get_jwt_value (self, request) : return request.COOKIES.get(api_settings.JWT_AUTH_HEADER_PREFIX.upper(), '' ) def authenticate_header (self, request) : """ 注意这里: 返回一个字符串作为`WWW-Authenticate`的值,http响应头中有`WWW-Authenticate`才会返回401. 否则返回403. """ return '{0} realm="{1}"' .format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
然后编写我们自己的获取JWT-Token的View,编辑app/views.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 from datetime import datetimefrom rest_framework import statusfrom rest_framework.response import Responsefrom rest_framework_jwt.settings import api_settingsfrom rest_framework_jwt.views import ObtainJSONWebTokenclass CookieJSONWebToken (ObtainJSONWebToken) : """ 接受post请求生成JWT-Token并设置cookie """ def post (self, request, *args, **kwargs) : serializer = self.get_serializer(data=request.data) if serializer.is_valid(): user = serializer.object.get('user' ) or request.user token = serializer.object.get('token' ) response_data = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER( token, user, request) res = Response(response_data) res.set_cookie(api_settings.JWT_AUTH_HEADER_PREFIX.upper(), value=response_data[ 'token' ], httponly=True , expires=datetime.now() + api_settings.JWT_EXPIRATION_DELTA) return res return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
这里注意设定httponly属性,防止造成安全漏洞。
最后,修改urls.py
添加我们的view:
1 2 3 4 5 6 7 8 from app import views as baseviewurlpatterns = [ ... url(r'^api-token-auth/' , baseview.CookieJSONWebToken.as_view()), ... ]
设定完成后,就可以通过向api-token-auth
这个URL提交用户名密码来获取JWT-Token了,并且在浏览器使用ajax请求获取那些登录后的数据会自动带上cookie进行认证。至于如何解决跨域,可以使用django-cors-headers 。
这里多说一句,使用这种方式登录时候你会发现在头部即便有Set-Cookie
,但ajax请求成功获取token后并不会自动设置cookie,因为浏览器把这个头忽略了。这里就涉及到另一个HTTP头Access-Control-Allow-Credentials
,只有这个设置成true
时候浏览器才会处理ajax返回来的Cookie信息。相关概念可以参考这里 。
这里我用axios
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 axios({ method: 'post' , url: 'http://127.0.0.1:9090/api-token-auth/' , data: { username: 'username' , password: 'password' }, withCredentials: true , }) .then(function (response ) { console .log(response); }) .catch(function (error ) { console .log(error); });
这里注意,设置withCredentials
为true
,此时发送请求,你会在浏览器控制台中看到已拦截跨源请求:同源策略禁止读取位于 http://127.0.0.1:9090/api-token-auth/ 的远程资源。(原因:CORS 头中的 'Access-Control-Allow-Credentials' 预期为 'true')
解决这个问题,首先安装上面的django-cors-headers
,然后修改settings.py
添加下面的配置即可:
1 2 3 4 5 6 CORS_ORIGIN_WHITELIST = ( 'localhost:8080' , ) CORS_ALLOW_CREDENTIALS = True
这里需要注意的,如果CORS_ALLOW_CREDENTIALS
设置为True
,那么Access-Control-Allow-Origin
则不能为通配符*
了,所以使用报名单的方式。
最后需要注意的,不要在JWT-Token中存储任何敏感信息!