simplejwt token前后端验证详解
simplejwt token前后端验证详解
前后端分离场景:
django
djangorestframework
djangorestframework-simplejwt
element-ui
范例说明:
后端部分代码:
serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer # Custom Token ObtainPairSerializer class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): ''' 自定义jwt认证返回值 ''' def validate(self, attrs): data = super().validate(attrs) refresh = self.get_token(self.user) data['refresh'] = str(refresh) data['access'] = str(refresh.access_token) data['user_id'] = self.user.id data['user_name'] = self.user.username return data
views.py
from backend.serializers import * from rest_framework_simplejwt.views import TokenObtainPairView # custom token view class CustomTokenObtainPairView(TokenObtainPairView): serializer_class = CustomTokenObtainPairSerializer
urls.py
from django.contrib import admin from django.views.generic import TemplateView from django.urls import path, include from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView from rest_framework.routers import DefaultRouter from backend import views router = DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'groups', views.GroupViewSet) # 使用自动url路由连接我们的API # 另外,我们还包括支持浏览器浏览API的登录URL app_name = 'backend' urlpatterns = [ path('', TemplateView.as_view(template_name='index.html'), name='index'), path('admin/', admin.site.urls), # django 管理员视图 path('alterpassword/', views.AlterPassword.as_view()), path('search/', views.GlobalSearchAPIView.as_view(), name="search"), path('api/<version>/', include(router.urls)), # 自定义rest api path('api-token-auth/', views.CustomTokenObtainPairView.as_view(), name='custom_token_obtain_pair'), # rest_framework_simplejwt 生成 path('api-token-refresh/', TokenRefreshView.as_view(), name='token_refresh'), # rest_framework_simplejwt 刷新 path('api-token-verify/', TokenVerifyView.as_view(), name='token_verify'), # rest_framework_simplejwt 验证 path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), # rest framework api 可视化 ]
前端部分代码:
axios-filter.js(拦截器)
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' // 读取cookie中的csrftoken axios.defaults.xsrfCookieName = 'csrftoken' // axios header设置X-CSRFTOKEN axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN' // 设置Content-Type axios.defaults.headers = { 'Content-Type': 'application/json;charset=UTF-8' } // 设置超时时间 axios.defaults.timeout = 50000 // request拦截器 axios.interceptors.request.use( config => { // sessionStorage 如果存在jwt token if (sessionStorage.token) { // 则JWT token认证格式,写入header config.headers.Authorization = 'JWt ' + sessionStorage.token // console.log(sessionStorage.token) } return config }, error => { // console.log(error) return Promise.reject(error) } ) // reponse自定义拦截器 start function customAxiosResponseInterceptor () { const interceptor = axios.interceptors.response.use( response => response, error => { // console.log(error) var config = error.config if (error.response.status === 401) { axios.interceptors.response.eject(interceptor) let data = { 'refresh': sessionStorage.getItem('refresh') } return axios.post('/api-token-refresh/', data).then(response => { sessionStorage.setItem('token', response.data.access) console.log('refresh access token success') // 重新发送请求 return axios(config) }).catch(error => { Message({ showClose: true, message: '登录超时,请重新登录!!!', type: 'error' }) redirectLoginWithQuery() console.log(error) return Promise.reject(error) }).finally(customAxiosResponseInterceptor) } else { Message({ showClose: true, message: error.response.data, type: 'error' }) return Promise.reject(error) } } ) } customAxiosResponseInterceptor() // reponse自定义拦截器 end // login with redirect function redirectLoginWithQuery () { router.push({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) } export default axios
login.vue
<template> <el-form @keyup.enter.native="submitForm('loginForm')" :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="55px" class="login-container" > <h3 class="title">系统登录</h3> <el-form-item label="用户名" prop="name"> <el-input type="text" v-model="loginForm.name" autocomplete="off" placeholder="用户名"></el-input> </el-form-item> <el-form-item label="密码" prop="pass"> <label slot="label">密 码</label> <el-input type="password" v-model="loginForm.pass" autocomplete="off" placeholder="密码"></el-input> </el-form-item> <el-checkbox v-model="checked" class="remember">记住密码</el-checkbox> <el-form-item> <el-button ref="button_login" v-loding="loading" type="primary" @click="submitForm('loginForm')" >登录</el-button> <el-button @click="resetForm('loginForm')">重置</el-button> </el-form-item> </el-form> </template> <script> export default { data () { var validateName = (rule, value, callback) => { if (!value) { callback(new Error('请输入用户名')) } else { callback() } } var validatePass = (rule, value, callback) => { if (!value) { callback(new Error('请输入密码')) } else { callback() } } return { checked: false, loginForm: { name: '', pass: '' }, rules: { name: [ { validator: validateName, trigger: 'blur' } ], pass: [ { validator: validatePass, trigger: 'blur' } ] }, loading: false } }, methods: { submitForm (formName) { this.$refs[formName].validate((valid) => { if (valid) { var that = this this.$refs.button_login.loading = true this.$axios.request( // 发送axios请求 { url: '/api-token-auth/', // 请求路径 method: 'POST', // 请求方式 data: { // 要发送的数据 username: this.loginForm.name, password: this.loginForm.pass }, responseType: 'json' // 期望返回的类型是json格式 } ).then(response => { // 把返回的结果交给回调函数处理 console.log(response) sessionStorage.setItem('token', response.data.access) sessionStorage.setItem('refresh', response.data.refresh) sessionStorage.setItem('user_id', response.data.user_id) sessionStorage.setItem('user_name', response.data.user_name) this.$message.success('登录成功') let redirect = decodeURIComponent(this.$route.query.redirect || '/') that.$router.push({ path: redirect }) // console.log(sessionStorage) }).catch(error => { this.$message.error(error) console.log(error) this.$refs.button_login.loading = false }) } else { console.log('请输入合法用户名和密码') this.$message.error('请输入合法用户名和密码') return false } }) }, resetForm (formName) { this.$refs[formName].resetFields() }, keyupEnter () { const that = this if (window.event.keyCode === 13) { that.submitForm('loginForm') } } }, mounted () { window.addEventListener('keyup', this.keyupEnter, false) }, beforeDestroy () { window.removeEventListener('keyup', this.keyupEnter, false) } } </script> <style lang="scss" scoped> .login-container { -webkit-border-radius: 5px; border-radius: 5px; -moz-border-radius: 5px; background-clip: padding-box; margin: 180px auto; width: 350px; padding: 35px 35px 15px 35px; background: #fff; border: 1px solid #eaeaea; box-shadow: 0 0 25px #cac6c6; .title { margin: 0px auto 40px auto; text-align: center; color: #505458; } .remember { margin: 0px 0px 35px 0px; } } </style>
token相关接口使用说明
方法 功能说明 特点 TokenObtainPairView 用户第一次登录,返回access和refresh的两个token access的token应该设置较短有效时间,refresh的token应该设置较长的合理有效时间 TokenRefreshView 传递有效时间内的refresh-token,接口返回有效时间的新access-token refresh-token过期返回的状态码与access-token过期返回的状态码都是401(vue拦截器对此处理需要一点小技巧,请参考axios-filter.js) TokenVerifyView 验证提交的token,返回当前token状态信息 settings.py中simplejwt相关设置参数
SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1), 'REFRESH_TOKEN_LIFETIME': timedelta(minutes=2), 'ROTATE_REFRESH_TOKENS': False, 'BLACKLIST_AFTER_ROTATION': True, 'UPDATE_LAST_LOGIN': False, 'ALGORITHM': 'HS256', 'SIGNING_KEY': SECRET_KEY, 'VERIFYING_KEY': None, 'AUDIENCE': None, 'ISSUER': None, 'AUTH_HEADER_TYPES': ('JWt',), 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', 'USER_ID_FIELD': 'id', 'USER_ID_CLAIM': 'user_id', 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 'TOKEN_TYPE_CLAIM': 'token_type', 'JTI_CLAIM': 'jti', 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), }
完整参数请参考:simplejwt完整参数项
- 实际应用中:
verify_jwt_token调用的频率可能比较少,
如果对安全性没太大要求,可以只使用access的token,不启用refresh的token,access的token超时后,跳转登录页即可。
但当用户成功验证而应用程序不更新cookie时,这个时候就存在会话固定漏洞,攻击者可利用此漏洞发起会话劫持
,
所以建议设置——ACCESS_TOKEN_LIFETIME
为较短时间,设置REFRESH_TOKEN_LIFETIME
为合适的较长时间,
即——access为较短的有效期,refresh设置为合理的较长有效期,
前端不断携带refresh token刷新接口,获取新的access token来进行业务交互。 简述一下djangorestframework-jwt和djangorestframework-simplejwt的区别
关于djangorestframework-jwt的模块,在使用obtain_jwt_token方法时,只会返回一个token,且该token只能在固定有效期时间内调用refresh_jwt_token,获取新的token
(新的token有效期等于前面所有token有效期减去已消耗的时间,也就是t1+t2+t3+..........=t1(如果没有刷新token)=JWT_EXPIRATION_DELTA;Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain of token stores the time that the original token (obtained with username/password credentials), as orig_iat. You can only keep refreshing tokens up to JWT_REFRESH_EXPIRATION_DELTA.)
- 而如果使用djangorestframework-simplejwt模块,在使用TokenObtainPairView方法时,会直接返回两个(access和refresh)token,且设置不同有效期,携带refresh token访问TokenRefreshView,才能返回新的固定有效期的access token
参考链接:
https://jpadilla.github.io/django-rest-framework-jwt
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
https://simpleisbetterthancomplex.com/tutorial/2018/12/19/how-to-use-jwt-authentication-with-django-rest-framework.html
https://www.remoteinning.com/blog/how-to-use-jwt-authentication-with-django-rest-framework
https://stackoverflow.com/questions/51646853/automating-access-token-refreshing-via-interceptors-in-axios
评论已关闭