欢迎访问我的博客专题
源码可访问 Github 查看
JSON Web Token(JWT)用户认证原理
JSON Web Token Authentication
JSON Web Token是一个相当新的标准,可以用于基于令牌的身份验证。与内置的TokenAuthentication
方案不同,JWT身份验证不需要使用数据库来验证token
。一个用于JWT身份验证的包是djangorestframework-simplejwt,它提供了一些特性以及一个可扩展的token黑名单应用程序。
可以访问 前后端分离之JWT用户认证 了解其原理。
Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。
JWT安装配置
由于视频中使用的django-rest-framework-jwt
不支持新版的Django和Django Restful Framework,现使用django-rest-framework-simplejwt
,2019年4月23日支持的版本如下
- Python (3.5, 3.6, 3.7)
- Django (1.11, 2.0, 2.1, 2.2)
- Django REST Framework (3.5, 3.6, 3.7, 3.8, 3.9)
安装配置djangorestframework_simplejwt
首先安装包
pip install djangorestframework_simplejwt
配置jwt认证类
然后,必须将django项目配置为使用该库。在 settings.py, 添加rest_framework_simplejwt.authentication.JWTAuthentication
到认证类中。
# DRF配置
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# 'PAGE_SIZE': 5,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', # 上面两个用于DRF基本验证
# 'rest_framework.authentication.TokenAuthentication', # TokenAuthentication,取消全局token,放在视图中进行
'rest_framework_simplejwt.authentication.JWTAuthentication', # djangorestframework_simplejwt JWT认证
)
}
添加jwt认证URL
此外,在主 url .py 文件(或任何其他url配置)中,包括 Simple JWT
的TokenObtainPairView
和TokenRefreshView
视图的路由:
from rest_framework_simplejwt import views as simplejwt_views # 引入simplejwt
urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')), # drf 认证url
path('api-token-auth/', views.obtain_auth_token), # drf token获取的url
path('api/token/', simplejwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'), # simplejwt认证接口
path('api/token/refresh/', simplejwt_views.TokenRefreshView.as_view(), name='token_refresh'), # simplejwt认证接口
path('ckeditor/', include('ckeditor_uploader.urls')), # 配置富文本编辑器url
path('', include(router.urls)), # API url现在由路由器自动确定。
# DRF文档
path('docs/', include_docs_urls(title='DRF文档')),
]
JWT使用
验证获取jwt access
验证jwt接口是否生效,请求 http://127.0.0.1:8000/api/token/
可以得到请求的结果
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU1NjA4MzUyNiwianRpIjoiYTFjMzA4NWJkYzM0NGVlYTlhNDhlMmYyYTZkMTc3OWUiLCJ1c2VyX2lkIjoxfQ.xWFW0xTwsO47HOASaPxsT8Tich8BwP1SJZJjBBU36jg",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTU1OTk3NDI2LCJqdGkiOiI1YmVlYzEzM2ZiMzM0MDY5OWQ1NzJiMjFhNGU3MzIzZSIsInVzZXJfaWQiOjF9.tKqJRlrb_DQsuw31xtwYW0i1fTc-7ZhGUqLk1ZiHyI0"
}
以点号.
做分隔,前两部分(Header、Payload)都是使用 Base64 进行编码,即前端可以解开知道里面的信息。后一部分(Signature )需要使用编码后的 Header 和 Payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
三个部分通过.连接在一起就是我们的 JWT 了。
可以使用返回的access
token来验证受保护视图的身份验证。格式为:Authorization: Bearer [access对应的值]
当这个短暂的access
token过期时,可以使用较长时间的refresh
token获得另一个access
token。
页面上测试使用这个access
请求,同样在list
函数中打上断点。进入调试状态。
jwt access过期
如果中途等待时间过长,那么access
就会过期,这时候访问 http://127.0.0.1:8000/goods/ ,就会出现401错误
使用refresh获取新的access
这时候可以使用refresh
来获取新的access
,http://127.0.0.1:8000/api/token/refresh/ 需要将refresh
的值传入后POST提交得到新的access
{
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTU1OTk4NTY5LCJqdGkiOiI2ODA0Y2Y1ZGJhZWQ0MGFiYjlkNDlmZjY0MjMxZWRhMSIsInVzZXJfaWQiOjF9.is3sU4yEs69ARFlTKnCEXNVXByfxvibSPRZdHSW8EJI"
}
使用jwt access获取认证用户
获取新的access
后再次添加认证请求 http://127.0.0.1:8000/goods/
可以在后端得到登录用户user
的信息
可以访问 JWT配置 查看自定义配置,比如可以自定义过期时间等。
自定义jwt配置
设置过期时间为7天,在 settings.py 中添加
# JWT自定义配置
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=7), # 配置过期时间
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
}
Vue登录和JWT接口调试
查看Vue登录逻辑
在 src/api/api.js 中,登录的接口是
//登录
export const login = params => {
return axios.post(`${local_host}/login/`, params)
};
而现在DRF登录url是api/token/
,有两种修改,一是修改Vue中的登录接口,二是修改Django的URL。这采用修改后台的方式,修改主 urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')), # drf 认证url
path('api-token-auth/', views.obtain_auth_token), # drf token获取的url
# path('api/token/', simplejwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'), # simplejwt认证接口
path('login/', simplejwt_views.TokenObtainPairView.as_view(), name='token_obtain_pair'), # 登录一般是login
path('api/token/refresh/', simplejwt_views.TokenRefreshView.as_view(), name='token_refresh'), # simplejwt认证接口
path('ckeditor/', include('ckeditor_uploader.urls')), # 配置富文本编辑器url
path('', include(router.urls)), # API url现在由路由器自动确定。
# DRF文档
path('docs/', include_docs_urls(title='DRF文档')),
]
访问前端 http://127.0.0.1:8080/#/app/login 输入用户名、密码点击登录
登陆后就可以在页面看到登录用户
src/views/login/login.vue 中有登录的逻辑
login({
username: this.userName, //当前用户名
password: this.parseWord
}).then((response) => {
//console.log(response);
//本地存储用户信息
console.log('用户登录信息:');
console.log(response.data);
cookie.setCookie('name', this.userName, 7); // 设置过期时间7天
// cookie.setCookie('token', response.data.token, 7);
cookie.setCookie('token', response.data.access, 7);
//存储在store
// 更新store数据
that.$store.dispatch('setInfo');
//跳转到首页页面
this.$router.push({name: 'index'})
})
当用户登录将可以获得response.data
的信息
然后将用户名保存到cookie的name
中,access
保存在cookie的token
中。
通过Vue的浏览器插件可以看到,当用户没有登录时name
值为空
点击登录,输入帐密登录后,显示当前的name
和token
的值
that.$store.dispatch('setInfo');
通过这个向Vuex发起一个setInfo
,(按住Ctrl点击setInfo
),可以跳转到 src/store/action.js
export const setInfo = makeAction(types.SET_INFO);
按住Ctrl点击SET_INFO
可以跳转到 src/store/mutation-types.js
export const SET_INFO = 'SET_INFO';
在 src/store/mutations.js 有下面内容
[types.SET_INFO](state) {
state.userInfo = {
name: cookie.getCookie('name'),
token: cookie.getCookie('token')
}
// console.log(state.userInfo);
},
mutations就会将name
和token
取出来,放在组件中,所以组件中的内容就会实时做一个更新。
增加用户名和手机号登录功能
修改 users/views.py 增加自定义后台认证类
from django.shortcuts import render
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.contrib.auth.backends import ModelBackend
User = get_user_model()
class CustomBackend(ModelBackend):
"""
自定义用户登录,可以使用用户名和手机登录,重写authenticate方法
"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(mobile=username))
if user.check_password(password):
return user
except Exception as e:
return None
配置到 settings.py ,添加以下内容
AUTHENTICATION_BACKENDS = ('users.views.CustomBackend',) # 指定认证后台
将CustomBackend
打上断点
通过请求 http://127.0.0.1:8000/login/ 测试是否能进入自定义认证后台中
这样就可以在后端看到请求的用户信息了,说明这个认证后台是正确的。