基于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
相关产品推荐
相关产品推荐

