如何在使用DRF djangorestframework-simplejwt包时将JWT令牌存储在HttpOnly Cookie中?

10
我已经使用了一段时间的djangorestframework-simplejwt,现在我想将JWT存储在cookie中(而不是localStorage或前端状态),以便客户端发出的每个请求都包含令牌。因此,我进行了一些研究,并找到了最相关的结果this stackoverflow question,其中作者正在使用djangorestframework-jwt包,该包具有名为JWT_AUTH_COOKIE的预配置设置,用于Cookie。所以我打算转向那个包,但最终发现the package is pretty much dead
虽然有一个djangorestframework-jwtfork被推荐使用,但我想知道是否有任何方法可以使用djangorestframework_simplejwt本身将JWT设置为HttpOnly cookie?
3个回答

14

使用httponly cookie标志和CSRF保护,请按照以下代码操作。

在移动应用程序和Web应用程序中,双方都非常有用。

urls.py:

...
path('login/',LoginView.as_view(),name = "login"),
...

view.py:

from rest_framework_simplejwt.tokens import RefreshToken
from django.middleware import csrf

def get_tokens_for_user(user):
    refresh = RefreshToken.for_user(user)
        
    return {
        'refresh': str(refresh),
        'access': str(refresh.access_token),
    }

class LoginView(APIView):
    def post(self, request, format=None):
        data = request.data
        response = Response()        
        username = data.get('username', None)
        password = data.get('password', None)
        user = authenticate(username=username, password=password)
        if user is not None:
            if user.is_active:
                data = get_tokens_for_user(user)
                response.set_cookie(
                                    key = settings.SIMPLE_JWT['AUTH_COOKIE'], 
                                    value = data["access"],
                                    expires = settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
                                    secure = settings.SIMPLE_JWT['AUTH_COOKIE_SECURE'],
                                    httponly = settings.SIMPLE_JWT['AUTH_COOKIE_HTTP_ONLY'],
                                    samesite = settings.SIMPLE_JWT['AUTH_COOKIE_SAMESITE']
                                        )
                csrf.get_token(request)
                email_template = render_to_string('login_success.html',{"username":user.username})    
                login = EmailMultiAlternatives(
                    "Successfully Login", 
                    "Successfully Login",
                    settings.EMAIL_HOST_USER, 
                    [user.email],
                )
                login.attach_alternative(email_template, 'text/html')
                login.send()
                response.data = {"Success" : "Login successfully","data":data}
                
                return response
            else:
                return Response({"No active" : "This account is not active!!"},status=status.HTTP_404_NOT_FOUND)
        else:
            return Response({"Invalid" : "Invalid username or password!!"},status=status.HTTP_404_NOT_FOUND)

authenticate.py:

from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings

from rest_framework.authentication import CSRFCheck
from rest_framework import exceptions

def enforce_csrf(request):
    """
    Enforce CSRF validation.
    """
    check = CSRFCheck()
    # populates request.META['CSRF_COOKIE'], which is used in process_view()
    check.process_request(request)
    reason = check.process_view(request, None, (), {})
    if reason:
        # CSRF failed, bail with explicit error message
        raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)

class CustomAuthentication(JWTAuthentication):
    
    def authenticate(self, request):
        header = self.get_header(request)
        
        if header is None:
            raw_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None
        else:
            raw_token = self.get_raw_token(header)
        if raw_token is None:
            return None

        validated_token = self.get_validated_token(raw_token)
        enforce_csrf(request)
        return self.get_user(validated_token), validated_token

settings.py:

....
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'authentication.authenticate.CustomAuthentication',
    ),
}

SIMPLE_JWT = {
.....
'AUTH_COOKIE': 'access_token',  # Cookie name. Enables cookies if value is set.
'AUTH_COOKIE_DOMAIN': None,     # A string like "example.com", or None for standard domain cookie.
'AUTH_COOKIE_SECURE': False,    # Whether the auth cookies should be secure (https:// only).
'AUTH_COOKIE_HTTP_ONLY' : True, # Http only cookie flag.It's not fetch by javascript.
'AUTH_COOKIE_PATH': '/',        # The path of the auth cookie.
'AUTH_COOKIE_SAMESITE': 'Lax',  # Whether to set the flag restricting cookie leaks on cross-site requests.
                                # This can be 'Lax', 'Strict', or None to disable the flag.
}

--------- 或 ------------

通过使用middleware.py:

如何使用中间件进行身份验证

必须:

双方都必须将withCredentials设置为True。

如有疑问,请在评论区留言。


1
在views.py中的第二行,我不知道从哪里获取RefreshTokenGrantrest_framework_simplejwt.tokensRefreshToken,你是指那个吗?还是这是一个自定义方法? - Jalal
1
是的,“RefreshToken”。当我复制这段代码时,我的“VS Code编辑器”片段对其进行了更改。我不知道这个更改是什么时候发生的。谢谢@Jalal。 - Pradip Kachhadiya
1
哦,好的伙计,谢谢。现在回到views.py文件,当我们看到user = authenticate(username=username, password=password)这一行时,我们是在调用序列化器还是在调用我们在CustomAuthentication类中创建的自定义认证方法? - Jalal
1
user = authenticate(username=username, password=password) 这个 authenticate 是来自于 from django.contrib.auth import authenticate,并且在 CustomAuthentication 中继承了 JWTAuthentication。它只检查 JWT 令牌是否有效。如果该令牌有效,则用户可以执行特定任务。否则会引发身份验证凭据未提供的错误。 - Pradip Kachhadiya
1
@Pradip,谢谢,那正是我想说的。我可以再问你一个问题吗?我看到很多关于使用JWT身份验证的混合回复,所以真的安全吗? - Danny
显示剩余11条评论

7
您可以按照以下方法将刷新令牌存储在httpOnly cookie中:

将以下内容添加到views.py:

# views.py
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework_simplejwt.exceptions import InvalidToken

class CookieTokenRefreshSerializer(TokenRefreshSerializer):
    refresh = None
    def validate(self, attrs):
        attrs['refresh'] = self.context['request'].COOKIES.get('refresh_token')
        if attrs['refresh']:
            return super().validate(attrs)
        else:
            raise InvalidToken('No valid token found in cookie \'refresh_token\'')

class CookieTokenObtainPairView(TokenObtainPairView):
  def finalize_response(self, request, response, *args, **kwargs):
    if response.data.get('refresh'):
        cookie_max_age = 3600 * 24 * 14 # 14 days
        response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
        del response.data['refresh']
    return super().finalize_response(request, response, *args, **kwargs)

class CookieTokenRefreshView(TokenRefreshView):
    def finalize_response(self, request, response, *args, **kwargs):
        if response.data.get('refresh'):
            cookie_max_age = 3600 * 24 * 14 # 14 days
            response.set_cookie('refresh_token', response.data['refresh'], max_age=cookie_max_age, httponly=True )
            del response.data['refresh']
        return super().finalize_response(request, response, *args, **kwargs)
    serializer_class = CookieTokenRefreshSerializer

将url.py中的URL更改为使用那些视图来获取和刷新令牌:

# url.py
from .views import CookieTokenRefreshView, CookieTokenObtainPairView # Import the above views
# [...]
urlpatterns = [
    path('auth/token/', CookieTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('auth/token/refresh/', CookieTokenRefreshView.as_view(), name='token_refresh'),
    # [...]
]

如果不按预期工作,请检查您的CORS设置:也许您需要在set_cookie中设置sameSite和secure。
工作流程-使用凭据获取令牌对:
1. 使用有效凭据进行POST /auth/token。 2. 在响应正文中,您会注意到只设置了“access”键。 3. “refresh”键已移动到名为“refresh_token”的httpOnly cookie中。
工作流程-使用刷新令牌获取访问(和可选的刷新)令牌:
1. 使用上一个工作流程中设置的cookie进行POST /auth/token/refresh,请求体可以为空。 2. 在响应正文中,您会注意到只设置了“access”键。 3. 如果您已设置ROTATE_REFRESH_TOKENS,则httpOnly cookie“refresh_token”包含一个新的刷新令牌。
参考:https://github.com/jazzband/djangorestframework-simplejwt/issues/71#issuecomment-762927394

1
为什么不将访问令牌设置为 HTTP Only Cookie,并创建一个自定义身份验证,从标头获取访问 Cookie 并对其进行验证呢? - theTypan
1
还是因为客户端需要检查访问令牌的过期时间? - theTypan

5
我在各处搜索,这是我找到的。我将尝试从设置cookie到检索cookie的整个过程进行解释。我知道我来晚了,但像我一样,肯定还有其他人在寻找答案。如果我做错了什么,请纠正我。
设置cookie 在django文档的项目配置下,您会发现他们使用TokenObtainPairView.as_view()来获取令牌。我们将在单独的视图文件中修改TokenObtainPairView,称之为MyTokenObtainPairView并导入它。请参见下面的代码。
# urls.py
from django.urls import path
from .views import MyTokenObtainPairView

urlpatterns = [
    path("token/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"),
]

from rest_framework_simplejwt.views import TokenObtainPairView
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView


class MyTokenObtainPairView(TokenObtainPairView):
    def post(self, request, *args, **kwargs):
        response = super().post(request, *args, **kwargs)
        token = response.data["access"]
        response.set_cookie("pick_a_name_you_like_for_the_cookie", token, httponly=True)
        return response

你可以在Postman中测试此功能。 假设您是从根目录并使用localhost进行的,端点应该类似于:http://localhost:8000/token/。 您应该使用登录凭据(通常是用户名和密码)运行POST请求。 现在,如果您使用postman,您应该会看到一个名为pick_a_name_you_like_for_the_cookie的cookie。

-> 阅读更多(堆栈)

-> 阅读更多(文档)-这更多用于自定义令牌声明。

-

获取令牌 默认情况下,simplejwt将在标头中查找访问令牌。因此,您将无法使用user = request.user来检索用户,也无法添加权限,例如permission_classes = [IsAuthenticated]@permission_classes([IsAuthenticated])请参阅文档

这是因为默认的身份验证类设置为:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    )
}

我们需要复制并修改JWTAuthentication类。在根目录中创建一个名为任何你想要的的文件。在这种情况下,我们创建了custom_auth.py。从rest_framework_simplejwt/authentication.py复制所有内容。您可以通过进入虚拟环境或直接前往pip安装rest_framework_simplejwt的位置找到此文件。
现在,假设您已经复制了所有内容,请更改custom_auth.py中以下代码:
from .exceptions import AuthenticationFailed, InvalidToken, TokenError
from .settings import api_settings

from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken, TokenError
from rest_framework_simplejwt.settings import api_settings

现在,在名为JWTAuthentication的类内部,您需要将认证函数更改为类似以下内容的函数:

class JWTAuthentication(authentication.BaseAuthentication):
    """
    An authentication plugin that authenticates requests through a JSON web
    token provided in a request header.
    """

    www_authenticate_realm = "api"
    media_type = "application/json"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.user_model = get_user_model()

    def authenticate(self, request):
        cookie = request.COOKIES.get("pick_a_name_you_like_for_the_cookie")
        raw_token = cookie.encode(HTTP_HEADER_ENCODING)
        validated_token = self.get_validated_token(raw_token)

        return self.get_user(validated_token), validated_token
    ...

如果没有cookie等情况,您应该添加一些验证等操作。可能是这样的:

    if cookie is None:
        return None

您可以根据需要自定义错误处理方式。最后返回至settings.py并替换:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    )
}

使用:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "custom_auth.JWTAuthentication",
    )
}

如果我留下了任何漏洞或者有什么不同的做法,请告诉我!:)


看起来没有必要完全复制 JWTAuthentication 类。我们可以继承它,然后重新定义 authenticate 方法。 - Oleg Klimenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接