Skip to content

Commit 17aefde

Browse files
authored
Merge: 카카오 로그인, 구글 소셜로그인 기능 구현
[Feature/social-login] 카카오 로그인, 구글 소셜로그인 기능 구현
2 parents 0457189 + 8e3ee47 commit 17aefde

File tree

8 files changed

+216
-46
lines changed

8 files changed

+216
-46
lines changed

apps/chat/consumers.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ async def connect(self) -> None:
4040
chatroom = await self.get_chatroom(self.chatroom_id)
4141

4242
if chatroom is None:
43-
await self.close(code=1008, reason="해당 채팅방이 존재하지 않습니다.")
43+
raise ValueError("해당 채팅방이 존재하지 않습니다.")
4444
user = self.scope["user"]
4545
if not await database_sync_to_async(check_entered_chatroom)(chatroom, user):
46-
await self.close(code=1008, reason="해당 채팅방에 존재하는 유저가 아닙니다.")
46+
raise ValueError("해당 채팅방에 존재하는 유저가 아닙니다.")
4747
await self.channel_layer.group_add(self.chat_group_name, self.channel_name)
4848
await self.accept()
4949
if check_opponent_online(self.chat_group_name):
@@ -62,6 +62,9 @@ async def connect(self) -> None:
6262
"opponent_state": "offline",
6363
},
6464
)
65+
except ValueError as e:
66+
logger.error("예외 발생: %s", e, exc_info=True)
67+
await self.close(code=1008, reason=str(e))
6568
except Exception as e:
6669
logger.error("예외 발생: %s", e, exc_info=True)
6770
await self.close(code=1011, reason=str(e))

apps/chat/utils.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -159,33 +159,33 @@ def get_chatroom_message(chatroom_id: int) -> Any:
159159
# 레디스에 저장된 메시지가 30개가 넘으면 가장 마지막에 저장된 메시지부터 30개를 가져옴
160160
if stored_message_num >= 30:
161161
stored_messages = redis_conn.lrange(key, 0, 29)
162-
messages = [json.loads(msg) for msg in stored_messages]
162+
messages = [json.loads(msg) for msg in reversed(stored_messages)]
163163
return messages
164164

165165
# 30개가 넘지않으면 레디스에 저장된 메시지들을 가져오고
166166
stored_messages = redis_conn.lrange(key, 0, -1)
167-
messages = [json.loads(msg) for msg in stored_messages]
167+
messages = [json.loads(msg) for msg in reversed(stored_messages)]
168168

169169
# 데이터베이스에서 30 - stored_message_num을 뺀 개수만큼 가져옴
170-
db_messages = Message.objects.filter(chatroom_id=chatroom_id).order_by("-created_at")
170+
db_messages = Message.objects.filter(chatroom_id=chatroom_id).order_by("created_at")
171171

172172
# 디비에 저장된 메시지가 30-stored_message_num 보다 많으면 슬라이싱해서 필요한 만큼의 데이터를 가져옴
173173
if db_messages.count() >= 30 - stored_message_num:
174174
serialized_messages = MessageSerializer(db_messages[: 30 - stored_message_num], many=True).data
175-
return messages + serialized_messages # type: ignore
175+
return serialized_messages + messages # type: ignore
176176

177177
# 디비에 저장된 메시지가 30-stored_message_num 보다 적으면 db에 저장된 채팅방의 모든 메시지를 가져옴
178-
serialized_messages = MessageSerializer(db_messages, many=True).data
179-
return messages + serialized_messages # type: ignore
178+
serialized_messages = MessageSerializer(db_messages.order_by("created_at"), many=True).data
179+
return serialized_messages + messages # type: ignore
180180

181181
# 레디스에 해당 채팅방 그룹 네임으로 지정된 키값이 없으면 데이터베이스에서 채팅 메시지를 가져옴
182182
db_messages = Message.objects.filter(chatroom_id=chatroom_id)
183183
if db_messages:
184184
if db_messages.count() >= 30:
185-
serialized_messages = MessageSerializer(db_messages.order_by("-created_at")[:30], many=True).data
185+
serialized_messages = MessageSerializer(db_messages.order_by("created_at")[:30], many=True).data
186186
return serialized_messages
187187

188-
serialized_messages = MessageSerializer(db_messages, many=True).data
188+
serialized_messages = MessageSerializer(db_messages.order_by("created_at"), many=True).data
189189
return serialized_messages
190190

191191
# 어디에도 데이터가 존재하지않으면 None을 반환

apps/user/urls.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
CustomLoginView,
1818
CustomSignupView,
1919
DeleteUserView,
20+
KakaoLoginView,
21+
GoogleLoginView,
2022
SendCodeView,
2123
)
2224

@@ -32,6 +34,8 @@
3234
path("password/reset/confirm/", PasswordResetConfirmView.as_view(), name="rest_password_reset_confirm"),
3335
# path('login/', LoginView.as_view(), name='rest_login'),
3436
path("login/", CustomLoginView.as_view(), name="login"),
37+
path("login/social/kakao/", KakaoLoginView.as_view(), name="kakao_login"),
38+
path("login/social/google/", GoogleLoginView.as_view(), name="google_login"),
3539
path("logout/", LogoutView.as_view(), name="rest_logout"),
3640
path("detail/", UserDetailsView.as_view(), name="rest_user_details"),
3741
path("leave/", DeleteUserView.as_view(), name="delete-user"),
@@ -42,5 +46,4 @@
4246
# re_path(r"^confirm-email/(?P<key>[-:\w]+)/$", CustomConfirmEmailView.as_view(), name="account_confirm_email"),
4347
path("send-code/", SendCodeView.as_view(), name="send-code"),
4448
path("confirm-email/", ConfirmEmailView.as_view(), name="confirm-email"),
45-
# path("google/", GoogleLogin.as_view(), name="google_login")
4649
]

apps/user/views.py

+190-32
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,23 @@
1-
import logging
21
from typing import Any
2+
from urllib.request import urlopen
33

4-
from allauth.account import app_settings
5-
from allauth.account import app_settings as allauth_account_settings
6-
from allauth.account import app_settings as allauth_settings
7-
from allauth.account.models import (
8-
EmailConfirmation,
9-
EmailConfirmationHMAC,
10-
get_emailconfirmation_model,
11-
)
12-
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
13-
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
4+
import requests
145
from dj_rest_auth.app_settings import api_settings
15-
from dj_rest_auth.models import get_token_model
16-
from dj_rest_auth.registration.views import RegisterView, SocialLoginView
6+
from dj_rest_auth.jwt_auth import set_jwt_cookies
7+
from dj_rest_auth.registration.views import RegisterView
178
from dj_rest_auth.utils import jwt_encode
189
from dj_rest_auth.views import LoginView
1910
from django.conf import settings
2011
from django.core.cache import cache
21-
from django.http import Http404, HttpResponse, HttpResponseRedirect
22-
from django.shortcuts import render
12+
from django.core.files.base import ContentFile
2313
from django.utils import timezone
24-
from drf_spectacular.utils import (
25-
extend_schema,
26-
extend_schema_serializer,
27-
extend_schema_view,
28-
inline_serializer,
29-
)
30-
from rest_framework import permissions, serializers, status
31-
from rest_framework.exceptions import NotFound
32-
from rest_framework.generics import CreateAPIView
33-
from rest_framework.permissions import AllowAny
14+
from drf_spectacular.utils import extend_schema
15+
from rest_framework import permissions, status
3416
from rest_framework.request import Request
3517
from rest_framework.response import Response
3618
from rest_framework.views import APIView
3719

20+
from apps.common.utils import uuid4_generator
3821
from apps.user.api_schema import (
3922
ConfirmRequestSchema,
4023
ConfirmResponseSchema,
@@ -49,11 +32,6 @@
4932
from apps.user.serializers import ConfirmEmailSerializer, SendCodeSerializer
5033
from apps.user.utils import generate_confirmation_code, send_email
5134

52-
# class GoogleLogin(SocialLoginView):
53-
# adapter_class = GoogleOAuth2Adapter
54-
# callback_url = api_settings.GOOGLE_OAUTH2_URL
55-
# client_class = OAuth2Client
56-
5735

5836
@extend_schema(request=SignupRequestSchema, responses=SignupResponseSchema)
5937
class CustomSignupView(RegisterView): # type: ignore
@@ -125,8 +103,6 @@ def get_response(self) -> Response:
125103
data.pop("user", None)
126104
response = Response(data, status=status.HTTP_200_OK)
127105
if api_settings.USE_JWT:
128-
from dj_rest_auth.jwt_auth import set_jwt_cookies
129-
130106
set_jwt_cookies(response, self.access_token, self.refresh_token)
131107
return response
132108

@@ -174,6 +150,188 @@ def post(self, request: Response, *args: Any, **kwargs: Any) -> Response:
174150
return Response({"message": "Email confirmation successful."}, status=status.HTTP_200_OK)
175151

176152

153+
class KakaoLoginView(APIView):
154+
permission_classes = [permissions.AllowAny]
155+
156+
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
157+
code = request.data.get("code") # 프론트에서 보내준 코드
158+
# 카카오 oauth 토큰 발급 url로 code가 담긴 post 요청을 보내 응답을 받는다.
159+
CLIENT_ID = settings.KAKAO_CLIENT_ID
160+
REDIRECT_URI = settings.REDIRECT_URI
161+
token_response = requests.post(
162+
"https://kauth.kakao.com/oauth/token",
163+
headers={"Content-Type": "application/x-www-form-urlencoded"},
164+
data={
165+
"grant_type": "authorization_code",
166+
"code": code,
167+
"redirect_uri": REDIRECT_URI,
168+
"client_id": CLIENT_ID,
169+
},
170+
)
171+
172+
if token_response.status_code != status.HTTP_200_OK:
173+
return Response(
174+
{"msg": "카카오 서버로 부터 토큰을 받아오는데 실패하였습니다."},
175+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
176+
)
177+
# 응답으로부터 액세스 토큰을 가져온다.
178+
access_token = token_response.json().get("access_token")
179+
response = requests.get(
180+
"https://kapi.kakao.com/v2/user/me",
181+
headers={
182+
"Authorization": f"Bearer {access_token}",
183+
"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
184+
},
185+
)
186+
187+
if response.status_code != status.HTTP_200_OK:
188+
return Response(
189+
{"msg": "카카오 서버로 부터 프로필 데이터를 받아오는데 실패하였습니다."},
190+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
191+
)
192+
response_data = response.json()
193+
kakao_account = response_data["kakao_account"]
194+
profile = kakao_account.get("profile")
195+
requests.post("https://kapi.kakao.com/v1/user/logout", headers={"Authorization": f"Bearer {access_token}"})
196+
try:
197+
user = Account.objects.get(email=kakao_account.get("email"))
198+
access_token, refresh_token = jwt_encode(user)
199+
response = Response(
200+
{
201+
"access": str(access_token),
202+
"refresh": str(refresh_token), # type: ignore
203+
"email": user.email,
204+
"nickname": user.nickname,
205+
"profile_image": user.profile_img.url,
206+
},
207+
status=status.HTTP_200_OK,
208+
)
209+
# set_jwt_cookies(response, access_token, refresh_token)
210+
return response # type: ignore
211+
212+
except Account.DoesNotExist:
213+
# 이미지를 다운로드하여 파일 객체로 가져옴
214+
image_response = urlopen(profile.get("profile_image_url"))
215+
image_content = image_response.read()
216+
kakao_profile_image = ContentFile(image_content, name=f"kakao-profile-{uuid4_generator(8)}.jpg")
217+
user = Account.objects.create(
218+
email=kakao_account.get("email"),
219+
nickname=profile.get("nickname"),
220+
profile_img=kakao_profile_image,
221+
)
222+
user.set_unusable_password()
223+
access_token, refresh_token = jwt_encode(user)
224+
response = Response(
225+
{
226+
"access": str(access_token),
227+
"refresh": str(refresh_token), # type: ignore
228+
"email": user.email,
229+
"nickname": user.nickname,
230+
"profile_image": user.profile_img.url,
231+
},
232+
status=status.HTTP_200_OK,
233+
)
234+
# set_jwt_cookies(response, access_token, refresh_token)
235+
return response # type: ignore
236+
except Exception as e:
237+
return Response({"msg": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
238+
239+
240+
class GoogleLoginView(APIView):
241+
permission_classes = [permissions.AllowAny]
242+
243+
def post(self, request: Request) -> Response:
244+
code = request.data.get("code")
245+
246+
client_id = settings.GOOGLE_CLIENT_ID
247+
client_secret = settings.GOOGLE_SECRET
248+
redirect_uri = settings.REDIRECT_URI
249+
250+
if not code:
251+
return Response({"msg": "인가코드가 필요합니다."}, status=status.HTTP_400_BAD_REQUEST)
252+
253+
# 인가코드를 통해 토큰을 가져오는 요청
254+
token_req = requests.post(
255+
# f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={redirect_uri}"
256+
"https://oauth2.googleapis.com/token",
257+
headers={
258+
"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
259+
},
260+
data={
261+
"client_id": client_id,
262+
"client_secret": client_secret,
263+
"code": code,
264+
"grant_type": "authorization_code",
265+
"redirect_uri": redirect_uri
266+
},
267+
)
268+
# 요청의 응답을 json 파싱
269+
token_req_json = token_req.json()
270+
if token_req_json.status_code != 200:
271+
return Response({"msg": token_req_json.get("error")}, status=status.HTTP_400_BAD_REQUEST)
272+
# 파싱된 데이터중 액세스 토큰을 가져옴
273+
google_access_token = token_req_json.get("access_token")
274+
275+
# 가져온 액세스토큰을 통해 사용자 정보에 접근하는 요청
276+
info_response = requests.get(
277+
f"https://www.googleapis.com/oauth2/v1/userinfo?access_token={google_access_token}"
278+
)
279+
280+
# 상태코드로 요청이 실패했는지 확인
281+
if info_response.status_code != 200:
282+
return Response(
283+
{"message": "구글 api로부터 액세스토큰을 받아오는데 실패했습니다."},
284+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
285+
)
286+
287+
# 요청의 응답을 json 파싱
288+
res_json = info_response.json()
289+
# 파싱된 데이터중 이메일값을 가져옴
290+
email = res_json.get("email")
291+
# 파싱된 데이터중 닉네임을 가져옴
292+
nickname = res_json.get("nickname")
293+
try:
294+
user = Account.objects.get(email=email)
295+
access_token, refresh_token = jwt_encode(user)
296+
response_data = {
297+
"access": str(access_token),
298+
"refresh": str(refresh_token),
299+
"email": user.email,
300+
"nickname": user.nickname
301+
}
302+
if user.profile_img:
303+
response_data["profile_image"] = user.profile_img.url
304+
response = Response(response_data, status=status.HTTP_200_OK)
305+
# if api_settings.USE_JWT:
306+
# set_jwt_cookies(response, access_token, refresh_token)
307+
return response
308+
except Account.DoesNotExist:
309+
# 파싱된 데이터에서 프로필 이미지 url을 가져와서 파일로 변환
310+
image_response = urlopen(res_json.get("picture"))
311+
image_content = image_response.read()
312+
google_profile_image = ContentFile(image_content, name=f"google-profile-{uuid4_generator(8)}.jpg")
313+
# 가져온 이메일, 닉네임, 프로필 이미지를 통해 유저 생성
314+
user = Account.objects.create(email=email, nickname=nickname, profile_img=google_profile_image)
315+
user.set_unusable_password()
316+
access_token, refresh_token = jwt_encode(user)
317+
response_data = {
318+
"access": str(access_token),
319+
"refresh": str(refresh_token),
320+
"email": user.email,
321+
"nickname": user.nickname
322+
}
323+
if user.profile_img:
324+
response_data["profile_image"] = user.profile_img.url
325+
response = Response(response_data, status=status.HTTP_200_OK)
326+
# if api_settings.USE_JWT:
327+
# set_jwt_cookies(response, access_token, refresh_token)
328+
return response
329+
330+
except Exception as e:
331+
# 가입이 필요한 회원
332+
return Response({"msg": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
333+
334+
177335
# class CustomConfirmEmailView(APIView):
178336
# permission_classes = [AllowAny]
179337
#

config/settings/base.py

-3
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@
5959
"allauth.socialaccount",
6060
"dj_rest_auth",
6161
"dj_rest_auth.registration",
62-
"allauth.socialaccount.providers.google",
63-
# "allauth.socialaccount.providers.naver",
64-
# "allauth.socialaccount.providers.kakao",
6562
"django_cleanup.apps.CleanupConfig",
6663
"corsheaders",
6764
"django_filters",

config/settings/local.py

+4
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,7 @@
206206
EMAIL_CODE_TIMEOUT = env("EMAIL_CODE_TIMEOUT")
207207
DJANGO_SUPERUSER_EMAIL = env("DJANGO_SUPERUSER_EMAIL")
208208
DJANGO_SUPERUSER_PASSWORD = env("DJANGO_SUPERUSER_PASSWORD")
209+
REDIRECT_URI = env("REDIRECT_URI")
210+
KAKAO_CLIENT_ID = env("KAKAO_CLIENT_ID")
211+
GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID")
212+
GOOGLE_SECRET = env("GOOGLE_SECRET")

config/settings/prod.py

+4
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,7 @@
148148
EMAIL_CODE_TIMEOUT = ENV["EMAIL_CODE_TIMEOUT"]
149149
DJANGO_SUPERUSER_EMAIL = ENV["DJANGO_SUPERUSER_EMAIL"]
150150
DJANGO_SUPERUSER_PASSWORD = ENV["DJANGO_SUPERUSER_PASSWORD"]
151+
REDIRECT_URI = ENV["REDIRECT_URI"]
152+
KAKAO_CLIENT_ID = ENV["KAKAO_CLIENT_ID"]
153+
GOOGLE_CLIENT_ID = ENV["GOOGLE_CLIENT_ID"]
154+
GOOGLE_SECRET = ENV["GOOGLE_SECRET"]

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ daphne = "^4.1.2"
3131
django-stubs-ext = "^5.0.0"
3232
django-filter = "^24.2"
3333
whitenoise = "^6.6.0"
34+
requests = "^2.32.0"
3435

3536

3637
[tool.poetry.group.dev.dependencies]

0 commit comments

Comments
 (0)