Django rest framework使用jwt认证前后端分离项目

解决了跨域资源共享CORS,自定义认证方式,自定义登陆返回及自定义错误返回,返回中包含token过期时间。不更改源代码,第一次优雅实现,值得记录。

截屏2020-11-13下午4.11.28.png

一,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 "username=sky@demo.com" -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 "username=sky@demo.com" -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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容