解决了跨域资源共享CORS,自定义认证方式,自定义登陆返回及自定义错误返回,返回中包含token过期时间。不更改源代码,第一次优雅实现,值得记录。
一,Django Rest Framework中的跨域
1,安装django-cors-headers
pip install django-cors-headers
2,settings.py文件配置更新
...
INSTALLED_APPS = [
...
'rest_framework',
'corsheaders',
]
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
...
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = [
"https://example.com",
"https://sub.example.com",
"http://localhost:8080",
"http://localhost:8000",
"http://127.0.0.1:8000"
]
二,Django Rest Framework中的jwt认证
1,安装djangorestframework-jwt
pip install djangorestframework-jwt
2,settings.py文件配置更新
...
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
# 自定义obtain_jwt_token登录参数验证
AUTHENTICATION_BACKENDS = (
'custom_jwt.views.CustomJwtBackend',
)
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=365),
'JWT_AUTH_HEADER_PREFIX': 'JWT',
'JWT_ALLOW_REFRESH': True,
}
...
- custom_jwt目录名如果要分隔,一定是下划线,短横杠符号不识别,切记!!!*
3,urls.py文档示例
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
# from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token
from custom_jwt import views as jwt_views
# Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'email', 'is_staff']
# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = [
path('', include(router.urls)),
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')),
path('jwt_auth/', jwt_views.obtain_jwt_token),
path('refresh_jwt_auth/', jwt_views.refresh_jwt_token),
path('verify_jwt_auth/', jwt_views.verity_jwt_token),
]
obtain_jwt_token, refresh_jwt_token这些函数都是自定义的,实现了自定义的登陆成功和失败的返回。这是不改源码的关键思路,切记切记!!!
4,custom_jwt目录下的views.py内容
from datetime import datetime
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q
from rest_framework import status
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.views import JSONWebTokenAPIView
from rest_framework_jwt.views import ObtainJSONWebToken
from rest_framework_jwt.views import RefreshJSONWebToken
from rest_framework_jwt.views import VerifyJSONWebToken
User = get_user_model()
class CustomJwtBackend(ModelBackend):
"""
自定义用户验证,定义完之后还需要在settings中进行配置
"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(email=username))
# django里面的password是加密的,前端传过来的password是明文,
# 调用check_password就会对明文进行加密,比较两者是否相同
if user.check_password(password):
return user
except Exception as e:
return None
# 不改rest_framework_jwt源码的情况下,自定义登陆后成功和错误的返回,最优雅
def jwt_response_payload_handler(token, user=None, expiration=None):
"""
自定义jwt认证成功返回数据
"""
data = {
'token': token,
'expireAt': expiration,
'user_id': user.id,
'user': user.username,
'is_superuser': user.is_superuser,
'permissions': 'admin',
'roles': ['admin']
}
return {'code': 0, 'data': data}
def jwt_response_payload_error_handler(serializer, requst=None):
"""
自定义jwt认证错误返回数据
"""
data = {
'message': "用户名或者密码错误",
'status': 400,
'detail': serializer.errors,
}
return {'code': -1, 'data': data}
# jwt的返回,由JSONWebTokenAPIView,自定义它的调用和返回即可
class CustomWebTokenAPIView(JSONWebTokenAPIView):
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')
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response_data = jwt_response_payload_handler(token, user, expiration)
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expiration=expiration,
httponly=True)
return response
error_data = jwt_response_payload_error_handler(serializer, request)
return Response(error_data, status=status.HTTP_200_OK)
class CustomObtainJSONWebToken(ObtainJSONWebToken, CustomWebTokenAPIView):
pass
class CustomRefreshJSONWebToken(RefreshJSONWebToken, CustomWebTokenAPIView):
pass
class CustomVerifyJSONWebToken(VerifyJSONWebToken, CustomWebTokenAPIView):
pass
obtain_jwt_token = CustomObtainJSONWebToken.as_view()
refresh_jwt_token = CustomRefreshJSONWebToken.as_view()
verity_jwt_token = CustomVerifyJSONWebToken.as_view()
用户认证时,新增了邮件方式认证,jwt_response_payload_handler默认第3个参数是request,我在返回里没有用上,使用token过期时间来代替换,使前端能获取到后端token的过期时间。这个正确和错误的返回json,是按vue-antd-admin项目的要求返回的。
三,curl测试用户及邮件认证,正确和错误的返回
1,用户错误
curl -X POST -d "username=sky" -d "password=passwor" http://localhost:8000/jwt_auth/
{"code":-1,"data":{"message":"用户名或者密码错误","status":400,"detail":{"non_field_errors":["Unable to log in with provided credentials."]}}}
2,用户正常
curl -X POST -d "username=sky" -d "password=password" http://localhost:8000/jwt_auth/
{"code":0,"data":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6InNreSIsImV4cCI6MTYzNjc5MjE0NSwiZW1haWwiOiJza3lAZGVtby5jb20iLCJvcmlnX2lhdCI6MTYwNTI1NjE0NX0.-6R-oya1XTWSwht-6Wi2bcER__44H_9psK-2x1Ni1D4","expireAt":"2021-11-13T08:29:05.448980","user_id":2,"user":"sky","is_superuser":false,"permissions":"admin","roles":["admin"]}}
3,邮箱错误
curl -X POST -d "[email protected]" -d "password=passwor" http://localhost:8000/jwt_auth/
{"code":-1,"data":{"message":"用户名或者密码错误","status":400,"detail":{"non_field_errors":["Unable to log in with provided credentials."]}}}
4,邮箱正确
curl -X POST -d "[email protected]" -d "password=password" http://localhost:8000/jwt_auth/
{"code":0,"data":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6InNreSIsImV4cCI6MTYzNjc5MjA5NiwiZW1haWwiOiJza3lAZGVtby5jb20iLCJvcmlnX2lhdCI6MTYwNTI1NjA5Nn0.kZNZA4w2U5H5yb7G27KOu97KkISeooR_6wWd0V5xpFQ","expireAt":"2021-11-13T08:28:16.798464","user_id":2,"user":"sky","is_superuser":false,"permissions":"admin","roles":["admin"]}}
四,vue-antd-admin登陆后的小更改
好像vue-antd-admin在登陆错误时,信息提示和loading状态不友好,我所更改一下。
Login.vue
onSubmit (e) {
e.preventDefault()
this.form.validateFields((err) => {
if (!err) {
this.logging = true
const name = this.form.getFieldValue('name')
const password = this.form.getFieldValue('password')
// 这里仍然是name,但在传递给后台的service那里,要更新成username参数
login(name, password).then(this.afterLogin)
}
})
},
afterLogin(res) {
this.logging = false
const loginRes = res.data
if (loginRes.code >= 0) {
const {user, permissions, roles} = loginRes.data
const expireAt = new Date(loginRes.data.expireAt)
this.setUser(user)
this.setPermissions(permissions)
this.setRoles(roles)
// 增加debug数据透明度
console.log(user, permissions, roles, loginRes.data.token, expireAt)
setAuthorization({token: loginRes.data.token, expireAt: expireAt})
// 获取路由配置
getRoutesConfig().then(result => {
const routesConfig = result.data.data
loadRoutes(routesConfig)
// 首页没作好,先显示次级页面
this.$router.push('/release/list')
this.$message.success(loginRes.message, 3)
})
} else {
// 原来的代码已注释,新加了logging状态终止,跳出弹窗3秒提示错误。
// this.error = loginRes.message
this.logging = false
this.$message.error(loginRes.data.message, 3)
}
}
参考URL:
https://blog.csdn.net/python_anning/article/details/109120654
https://blog.csdn.net/qq_42327755/article/details/108486382
https://blog.csdn.net/qq_35753140/article/details/90266283
https://www.jianshu.com/p/a399b98ab05b