AcWing
  • 首页
  • 活动
  • 题库
  • 竞赛
  • 应用
  • 更多
    • 题解
    • 分享
    • 商店
    • 问答
    • 吐槽
  • App
  • 登录/注册

工程课 Django-10.1. Django Rest Framework与JWT身份验证    原题链接    简单

作者: 作者的头像   yume ,  2023-03-19 17:07:32 ,  所有人可见 ,  阅读 40


2


1

Django Rest Framework与JWT身份验证

理论部分

这个技术是用来解决跨域情况下的用户登录与维持登录状态的问题。即处理跨域之后的身份验证问题。

Django自带的auth登录验证系统只能应用与于前端页面与后端页面在同一个网址(域)的情况下,也就是说当前后端分离,后端只提供API给前端,前端通过API提供的数据对页面进行渲染或增加修改的情况下,这种默认的登录验证系统就不起效果了,因为HTTP是一种无状态的协议,也就是说后端服务并不知道是谁发来的请求,我们也就无法验证请求的合法性。我们一般写前后端分离的项目时,都是用ajax向服务器端发起请求以获取后端API的数据,这种方式是无法获取从服务器端返回回来的sessionid的,从而就难以做身份验证。简单来说,就是传统的登录鉴权在前后端分离的情况下,无法将后端的sessionid存到前端对应域名的cookie里,因此无法做前后端分离的身份验证,cookie无法做跨域。

传统的登录鉴权与基于Token的鉴权区别

先来看看传统的登录鉴权跟基于Token的鉴权有什么区别:

以Django的账号密码登录为例来说明传统的验证鉴权方式是怎么工作的,当我们登录页面输入账号密码提交表单后,会发送请求给服务器,服务器对发送过来的账号密码进行验证鉴权,验证鉴权通过后,把用户信息记录在服务器端(django_session表中),同时返回给浏览器一个sessionid用来唯一标识这个用户,浏览器将sessionid保存在cookie中(它是属于http-only的,是不能用js读取到的),之后浏览器的每次请求都一并将sessionid发送给服务器,服务器根据sessionid与记录的信息做对比以验证身份。

Token的鉴权方式就清晰很多了,客户端用自己的账号密码进行登录,服务端验证鉴权,验证鉴权通过生成Token返回给客户端,之后客户端每次请求都将Token放在header里一并发送,服务端收到请求时校验Token以确定访问者身份。

session的主要目的是给无状态的HTTP协议添加状态保持,通常在浏览器作为客户端的情况下比较通用。而Token的主要目的是为了鉴权,同时又不需要考虑CSRF防护以及跨域的问题,所以更多的用在专门给第三方提供API的情况下,客户端请求无论是浏览器发起还是其他的程序发起都能很好的支持。所以目前基于Token的鉴权机制几乎已经成了前后端分离架构或者对外提供API访问的鉴权标准,得到广泛使用。

传统的用户登录认证中,因为http是无状态的,所以都是采用session的认证方式。用户登录成功,服务端就会保存一个session,也会给客户端一个sessionID,以后客户端每次请求,都会携带这个sessionID,服务端会会根据这个sessionID来区分不同的用户。

这种基于cookie+session的认证方式,随着服务从单服务到多服务,缺点就出来了,因为session是存储在服务端,这样服务器的开销就会大起来了。并且session标示丢失,就可能出现安全问题CSRF(跨站请求伪造)。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

Token字面意思是令牌,功能跟Session类似,也是用于验证用户信息的,Token是服务端生成的一串字符串,当客户端发送登录请求时,服务器便会生成一个Token并将此Token返回给客户端,作为客户端进行请求的一个标识,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。与session的不同之处在于,session是将用户信息存储在服务器中保持用户的请求状态,而Token在服务器端不需要存储用户的登录记录,客户端每次向服务端发送请求的时候都会带上服务端发给的Token,服务端收到请求后去验证客户端请求里面带着Token,如果验证成功,就向客户端返回请求的数据。

一些参考文章:

Django-rest-framework的JWT登录和认证

cookie与session的区别

Django解决跨域问题

JSON Web Token 入门教程

基于Token 的身份验证流程

  • 客户端使用用户名跟密码请求登录
  • 服务端收到请求开始验证用户名与密码
  • 验证成功后,服务端生成一个 Token(可以理解为一种加密算法,一般为有效信息 + 一个加密字符串)并把这个 Token发送给客户端
  • 客户端收到 Token 以后可以把它存储起来,可以存放在Cookie里或者Local Storage 里
  • 客户端再次向服务端请求资形式源的时候携带服务端生成的 Token发送给服务器
  • 服务端收到请求,然后去验证客户端请求里面携带的 Token,如果验证成功(如读取里面的user_id信息从而知道是哪一个用户传过来的),就向客户端返回请求的数据,否则拒绝请求

JWT用户认证.png

JWT的构成

JSON Web Token(JWT)由三部分组成,这些部分由点(.)分隔,分别是header(头部),payload(有效负载)和signature(签名)。

如下示例为一个JWT:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsInR5cGUiOiJqd3QifQ.eyJ1c2VybmFtZSI6InhqayIsImV4cCI6MTU4MjU0MjAxN30.oHdfcsUftJJob66e5mL1jLRpJwiG0i9MOD5gzM476eY

  • header(头部):

JWT的头部承载两部分:声明类型,声明加密算法

python headers = { "type":"jwt", "alg":"HS256" }

然后将头部进行base64加密。(该加密是可以对称解密的),构成了第一部分

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsInR5cGUiOiJqd3QifQ

  • payload(有效负载)

载荷就是存放有效信息的地方,这个名字像是特指飞机上承载的货品,这些有效信息包含三部分:

  • 标准中注册声明(建议但不强制使用):
    • iss:jwt签发者。
    • sub:jwt所面向的用户
    • aud:接收jwt的一方
    • exp:jwt过期时间,这个过期时间必须大于签发时间
    • nbf:定义在什么时间之前,该jwt都是可用的
    • lat:jwt的签发时间
    • jti:jwt的唯一身份表示,主要用来作为一次性token,从而回避重放攻击。
  • 公共的声明:
    • 可以添加任何信息,一般添加用户相关信息。但不建议添加敏感信息,因为该部分在客户端可解密
  • 私有的声明:
    • 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

python { "username": "xjk", }

构成了第二部分:eyJ1c2VybmFtZSI6InhqayIsImV4cCI6MTU4MjU0MjAxN30

  • signature(签名)

  • jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

    • header(base64后的)
    • payload(base64后的)
    • secret(私钥)
  • 将header和payload使用Base64编码生成一下再加入签名字符secret(密码加盐)用(header中声明的加密算法加密一遍,得到唯一的签名,用来防止其他人来篡改Token中的信息。
  • signature = 加密算法(header + "." + payload, 密钥)

构成了第三部分:oHdfcsUftJJob66e5mL1jLRpJwiG0i9MOD5gzM476eY

jwt实现原理.png

签名的目的:最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

  • JWT实现原理 ,一种比较原始的手搓JWT Token的方法

  • JWT解码网址

Token基本实现原理

Token 也称作令牌,是由服务器端返回的字符串,此字符串是通过base64编码后得到的,编码前信息为json,包括加密方法、需要公开的用户信息、签名也就是密文。服务器端只有开发者随机设置的密钥。而服务器端是将用户信息和密钥一起通过加密方法加密,将得到的密文与需要公开的用户信息与加密方法通过base64编码为字符串。当客户端请求带上 Token,服务器端用同样方法构造Token,相同则允许请求,不同则报401 错误。这个加密采用的加密算法,一般有此特点:从源串计算出加密串很好算,但从加密串算出源串一般不可能。

IMG_1151(20230318-211953).PNG.PNG)

之后当客户端向服务端发起请求时,只需要携带Token给服务端,服务端根据Token中的用户有效信息加上私钥再做一遍加密算法,看看结果是否与Token中的“加密之后的结果”一致,若一致说明验证成功。

通过DRF simple jwt构造的jwt会返回两个值,分别是 access和refresh,access就是上文我们所说的 Token,refresh 是用来是刷新access 串, refresh请求每次都是用post方法,post方法的参数会在请求的body里面,相对来说更加安全。 access 和refresh 的过期时间不同,access 一般是 5 分钟,· 一般是14天,因为有些 get 方法也需要登录之后才能访问,意味着 Token会显式的显示在url链接里,这样不太安全,因此 Token 的有效期一般比较短。

配置DJango Rest Framework与JWT

集成Django Rest Framework

mark

简书

安装

pip install djangorestframework
pip install pyjwt
pip install

#######
如果出现could not find a version....报错信息的话,可能是python解释器的路径与pip的安装路径不同,引用不到权限导致的,所以要修改一下指令:
python3 -m pip install djangorestframework
python3 -m pip install pyjwt

然后在settings.py的INSTALLED_APPS中添加rest_framework

#INSTALLED_APPS中添加注册信息 
INSTALLED_APPS = [
    'rest_framework',
     ...
    'corsheaders',
]

Class-Based Views

from rest_framework.views import APIView
from rest_framework.response import Response

class SnippetDetail(APIView):
    def get(self, request):  # 查找
        ...
        return Response(...)

    def post(self, request):  # 创建
        ...
        return Response(...)

    def put(self, request, pk):  # 修改
        ...
        return Response(...)

    def delete(self, request, pk):  # 删除
        ...
        return Response(...)


集成jwt验证

安装

pip install djangorestframework-simplejwt

然后我们需要告诉DRF我们使用jwt认证作为后台认证方案

在settings.py中添加:

INSTALLED_APPS = [
    ...
    'rest_framework_simplejwt',
    ...
]

# REST_FRAMEWORK中全局配置认证方式、权限方式。
# 如settings.py文件中没有REST_FRAMEWORK,请自主写入 

REST_FRAMEWORK = {
    ...
    # DEFAULT_AUTHENTICATION_CLASSES设置默认的认证类,这里用token,也可以设置session或自定义的认证     # 用户登陆认证方式
    'DEFAULT_AUTHENTICATION_CLASSES': (
        ...
        'rest_framework_simplejwt.authentication.JWTAuthentication',# 进行token认证 
    )
    ...
}

注意:INSTALLED_APPS后都需要执行一个指令:python3 manage.py collectstatic

配置

在settings.py中添加:

from datetime import timedelta # 导入datetime库生成时间参数 
...
# SIMPLE_JWT是token配置项,参数很多,可查看官网https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
SIMPLE_JWT = {
    # ACCESS_TOKEN_LIFETIME设置token令牌有效时间 
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 
    # REFRESH_TOKEN_LIFETIME设置token刷新令牌有效时间 
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY, # 注意这里的SECRET_KEY需要改成自己的字符串密钥(一个随机字符串)
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    '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',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

添加获取jwt和刷新jwt的路由

jwt已经帮我们实现了登录功能,不需要再自己手写了,直接引入就好了。

登录有两个API:

  1. api/token/:获取Token令牌
  2. api/token/refresh/:刷新Token令牌

在总项目下的urls.py中加入自己定义的url,给自己的新建的app一个总的路由,inclue中写入自己appname中urls.py的路径

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    ...
    # 类.as_view()将类转换为函数写法
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ...
]

访问对应的URL可以得到rest framework work给我们写好的调试页面:

restframwork_api.png

我们需要用POST的方法获取密钥

登出功能的实现只需要在客户端将用户的JWT删掉即可

手动获取jwt

from rest_framework_simplejwt.tokens import RefreshToken

def get_tokens_for_user(user):
    refresh = RefreshToken.for_user(user)

    return {
        'refresh': str(refresh),
        'access': str(refresh.access_token),
    }


将jwt验证集成到Django Channels中

创建文件channelsmiddleware.py

"""General web socket middlewares
"""

from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.tokens import UntypedToken
from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication
from channels.middleware import BaseMiddleware
from channels.auth import AuthMiddlewareStack
from django.db import close_old_connections
from urllib.parse import parse_qs
from jwt import decode as jwt_decode
from django.conf import settings
@database_sync_to_async
def get_user(validated_token):
    try:
        user = get_user_model().objects.get(id=validated_token["user_id"])
        # return get_user_model().objects.get(id=toke_id)
        return user

    except:
        return AnonymousUser()



class JwtAuthMiddleware(BaseMiddleware):
    def __init__(self, inner):
        self.inner = inner

    async def __call__(self, scope, receive, send):
       # Close old database connections to prevent usage of timed out connections
        close_old_connections()

        # Try to authenticate the user
        try:
            # Get the token
            token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]

            # This will automatically validate the token and raise an error if token is invalid
            UntypedToken(token)
        except:
            # Token is invalid

            scope["user"] = AnonymousUser()
        else:
            #  Then token is valid, decode it
            decoded_data = jwt_decode(token, settings.SIMPLE_JWT["SIGNING_KEY"], algorithms=["HS256"])

            # Get the user using ID
            scope["user"] = await get_user(validated_token=decoded_data)
        return await super().__call__(scope, receive, send)


def JwtAuthMiddlewareStack(inner):
    return JwtAuthMiddleware(AuthMiddlewareStack(inner))

项目实现

后端

rest frame work配合JWT实现获取用户登录状态与登录信息。

appname/get_status.py

from rest_framework.views import   APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

class InfoView(APIView):
    permission_classes = ([IsAuthenticated])

    def get(self,request):
        # 获取当前用户信息
        user = request.user
        return Response({
            'username': user.username,
            'result': "success",
        })

在url路由里面引进该模块,可以把之前手写的login与logout模块去掉了,使用jwt可以替代掉这两个功能。

appname/index.py

from django.urls import path
from backend.calculator.register import signup
from backend.calculator.get_status import InfoView
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path("register/", signup, name="calculator_register"),
    path("get_status/", InfoView.as_view(),name="calculator_get_status"), #name是在后端渲染有用,前端渲染用不到
]

前端

前端是之前写的一个Web App项目网站,里面目前只实现了个计算器(菜),不是本文的重点内容,因此就不强调了,只是展示如何在前端调用后端的API,以实现登录功能与获取登录状态。

基本逻辑

每一次客户端向服务端发送请求,都需要携带access(5分钟)与refresh(14天),如果这个请求需要用到身份验证的话,都需要在后端做一个判断:如果refresh过期了,需要用户重新登录;如果refresh没有过期,access过期了,那么就需要用refresh的API重新获取一个access;如果refresh没有过期,且access也没有过期,则可以直接返回数据。

基本实现(完整版)

前端的客户端可以直接把access与refresh字段直接放在浏览器的local Storage里面,每次可以从local Storage里面读取这两个字段。

判断是否过期的方法:可以同时存一个创建时间,每次判断当前时间与创建时间的时间间隔有没有超过他的保质期,注意要预留时间间隔,防止传到服务器的时候有时间误差。

乞丐版展示

这里只是展示一下功能,因此逻辑可以不那么复杂,先搞个乞丐版的:用户每次刷新页面都需要重新登录,登录后将access与refresh字段直接都存到内存里,存完后写个周期函数,每隔4.5分钟向后端发送请求,重新获取一次access。

这里前端用的是react

登录模块:login.jsx

 handleClick = e => {
        e.preventDefault();
        if (this.state.username === "") {
            this.setState({error_message: "用户名不能为空"});
        } else if (this.state.password === "") {
            this.setState({error_message: "密码不能为空"});
        } else {
                $.ajax({
                    url: "http://127.0.0.1:8000/token/",
                    type: "post",
                    data: {
                        username: this.state.username,
                        password: this.state.password,
                    },
                    dataType: 'json',
                    success: (resp) => {
                        console.log(resp);
                        this.props.set_token(resp.access,resp.refresh,this.state.username);
                        this.props.set_login(true);
                     },
                    error() {
                        this.setState({error_message: "用户名或密码错误"});
                     }
                })

        }
        //console.log(this.state);
     }

取消登录,直接将access与refresh_token去掉就好了

    handleClick = () => {
        this.props.set_token("","","");
        this.props.set_login(false);
    }

注册功能:register.jsx

 handleClick = e => {
        e.preventDefault();
        $.ajax({
            url: "http://127.0.0.1:8000/register/",
            type: "get",
            data: {
                username: this.state.username,
                password: this.state.password,
                password_confirm: this.state.confirmed_password,
            },
            dataType: 'json',
            success: resp => {
                if (resp.result === "success") {
                    window.location.href="/calculator";
                } else {
                       this.setState({error_message: resp.result});
                 }
            }
        });
        console.log(this.state);

     }

0 评论

你确定删除吗?
1024
x

© 2018-2023 AcWing 版权所有  |  京ICP备17053197号-1
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标
请输入绑定的邮箱地址
请输入注册信息