You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于DRF、NextJS和Simple JWT实现仅邮箱OAuth登录

问题描述

我正在开发一个基于Django Rest Framework(DRF)和NextJS的项目,采用Simple JWT处理用户认证。现在需要实现OAuth认证(Kakao),是一种仅通过邮箱无需密码的登录机制。

现有两个视图类:

class TeacherLoginView(TokenObtainPairView):
    serializer_class = LoginSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        try:
            user = authenticate(
                request=request,
                email=request.data["email"],
                password=request.data["password"],
            )

            if user != None:
                if not user.is_teacher:
                    return Response(
                        {"is_teacher": "재원생은 {URL}을 통해서 로그인 바랍니다."},
                        status=status.HTTP_401_UNAUTHORIZED,
                    )
                if not user.is_active:
                    return Response(
                        {"is_active": "미승인된 계정입니다. 관리자에게 문의하세요."},
                        status=status.HTTP_401_UNAUTHORIZED,
                    )
                if not ip_check(request, user):
                    return Response(
                        {"error": "학원 외부에서 접속은 불가합니다. 관리자에게 문의하세요."},
                        status=status.HTTP_403_FORBIDDEN,
                    )

            serializer.is_valid(raise_exception=True)

            if user:
                user.last_login = timezone.now()
                user.save()

        except TokenError as e:
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)


class TeacherKakaoLoginView(APIView):
    permission_classes = [AllowAny]

    def post(self, request):
        try:
            code = request.data.get("code")
            access_token = requests.post(
                '''skip'''
            )
            access_token = access_token.json().get("access_token")

            user_data = requests.get(
                '''skip'''
            )
            user_data = user_data.json()
            kakao_account = user_data.get("kakao_account")
            profile = kakao_account.get("profile")
            try:
                user = User.objects.get(email=kakao_account.get("email"))
                TeacherLoginView.post(request, user) # 这里有问题
                return Response(status=status.HTTP_200_OK)
            except User.DoesNotExist:
                user = User.objects.create(
                    email=kakao_account.get("email"),
                    username=profile.get("nickname"),
                    oauth="kakao",
                    is_teacher = True,
                    is_active = False,
                )
                user.set_unusable_password()  
                user.save()
                TeacherLoginView.post(request, user) # 这里有问题
                return Response(status=status.HTTP_200_OK)
        except Exception:
            return Response(status=status.HTTP_400_BAD_REQUEST)

TeacherLoginView是已正常工作的常规JWT认证视图,TeacherKakaoLoginView是新建的Kakao OAuth处理视图。

需求

  • 允许用户通过Kakao OAuth仅用邮箱登录
  • 登录成功后颁发JWT令牌

困惑点

  • 如何调整TeacherLoginView适配无密码的OAuth登录
  • 如何修改TeacherKakaoLoginView实现OAuth并颁发JWT

解决方案

1. 重构认证逻辑,分离通用校验与令牌生成

先把TeacherLoginView中的用户校验逻辑抽成独立函数,让密码登录和OAuth登录能复用同一套校验规则:

# 新增通用用户校验函数
def validate_teacher_user(request, user):
    if not user.is_teacher:
        return Response(
            {"is_teacher": "재원생은 {URL}을 통해서 로그인 바랍니다."},
            status=status.HTTP_401_UNAUTHORIZED,
        )
    if not user.is_active:
        return Response(
            {"is_active": "미승인된 계정입니다. 관리자에게 문의하세요."},
            status=status.HTTP_401_UNAUTHORIZED,
        )
    if not ip_check(request, user):
        return Response(
            {"error": "학원 외부에서 접속은 불가합니다. 관리자에게 문의하세요."},
            status=status.HTTP_403_FORBIDDEN,
        )
    return None  # 校验通过返回None

再修改TeacherLoginView,保留密码登录逻辑,同时新增静态方法用于生成JWT令牌:

from rest_framework_simplejwt.tokens import RefreshToken

class TeacherLoginView(TokenObtainPairView):
    serializer_class = LoginSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        try:
            user = authenticate(
                request=request,
                email=request.data["email"],
                password=request.data["password"],
            )
            if user:
                # 调用通用校验
                validation_error = validate_teacher_user(request, user)
                if validation_error:
                    return validation_error
                
                user.last_login = timezone.now()
                user.save()

            serializer.is_valid(raise_exception=True)

        except TokenError as e:
            raise InvalidToken(e.args[0])

        return Response(serializer.validated_data, status=status.HTTP_200_OK)

    # 新增静态方法,为已认证用户生成JWT令牌
    @staticmethod
    def generate_jwt_tokens(user):
        refresh = RefreshToken.for_user(user)
        return {
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }

2. 修改Kakao OAuth视图,直接生成JWT令牌

原代码中直接调用TeacherLoginView.post的方式不可行(因为该方法依赖request中的密码参数),改为调用新增的令牌生成方法,同时复用用户校验逻辑:

class TeacherKakaoLoginView(APIView):
    permission_classes = [AllowAny]

    def post(self, request):
        try:
            code = request.data.get("code")
            # 补充完整Kakao access_token获取逻辑
            access_token_response = requests.post(
                "https://kauth.kakao.com/oauth/token",
                data={
                    "grant_type": "authorization_code",
                    "client_id": "YOUR_KAKAO_CLIENT_ID",
                    "redirect_uri": "YOUR_REDIRECT_URI",
                    "code": code
                }
            )
            access_token_response.raise_for_status()  # 捕获HTTP请求错误
            access_token = access_token_response.json().get("access_token")

            # 获取Kakao用户信息
            user_data_response = requests.get(
                "https://kapi.kakao.com/v2/user/me",
                headers={"Authorization": f"Bearer {access_token}"}
            )
            user_data_response.raise_for_status()
            user_data = user_data_response.json()
            kakao_account = user_data.get("kakao_account")
            
            # 检查是否获取到邮箱
            if not kakao_account or not kakao_account.get("email"):
                return Response(
                    {"error": "카카오에서 이메일 정보를 가져올 수 없습니다."},
                    status=status.HTTP_400_BAD_REQUEST
                )
            
            email = kakao_account.get("email")
            profile = kakao_account.get("profile")
            username = profile.get("nickname") if profile else email.split('@')[0]

            try:
                user = User.objects.get(email=email)
                # 校验用户是否符合教师登录要求
                validation_error = validate_teacher_user(request, user)
                if validation_error:
                    return validation_error
            except User.DoesNotExist:
                # 创建新OAuth用户,设置不可用密码
                user = User.objects.create(
                    email=email,
                    username=username,
                    oauth="kakao",
                    is_teacher=True,
                    is_active=False,  # 新用户默认未激活,需管理员审核
                )
                user.set_unusable_password()
                user.save()
                # 返回未激活提示
                return Response(
                    {"is_active": "미승인된 계정입니다. 관리자에게 문의하세요."},
                    status=status.HTTP_401_UNAUTHORIZED
                )
            
            # 更新最后登录时间
            user.last_login = timezone.now()
            user.save()

            # 生成并返回JWT令牌
            tokens = TeacherLoginView.generate_jwt_tokens(user)
            return Response(tokens, status=status.HTTP_200_OK)
        
        except requests.exceptions.RequestException:
            return Response(
                {"error": "카카오 연동 요청에 실패했습니다."},
                status=status.HTTP_400_BAD_REQUEST
            )
        except Exception:
            return Response(status=status.HTTP_400_BAD_REQUEST)

3. 关键说明

  • 校验逻辑复用:通用校验函数保证密码登录和OAuth登录的规则一致,避免重复代码
  • 灵活生成JWT:直接使用Simple JWT的RefreshToken.for_user方法生成令牌,无需依赖密码参数
  • 错误处理增强:新增Kakao API请求的异常捕获,以及用户无邮箱的场景处理,提升代码健壮性
  • 新用户流程对齐:新注册的OAuth用户默认未激活,和现有密码登录的用户审核流程保持一致

内容的提问来源于stack exchange,提问作者chea

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 06:15:08