Skip to content

Commit ec6a11b

Browse files
authored
Merge: 네이버, 구글, 카카오 소셜 로그인 완성 및 리팩토링
[refactor/social] 네이버, 구글, 카카오 소셜 로그인 완성 및 리팩토링
2 parents 480ef14 + 81aab08 commit ec6a11b

File tree

5 files changed

+136
-165
lines changed

5 files changed

+136
-165
lines changed

apps/user/views.py

+123-158
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, Optional
22
from urllib.request import urlopen
33

44
import requests
@@ -150,186 +150,151 @@ def post(self, request: Response, *args: Any, **kwargs: Any) -> Response:
150150
return Response({"message": "Email confirmation successful."}, status=status.HTTP_200_OK)
151151

152152

153-
class KakaoLoginView(APIView):
153+
class OAuthLoginView(APIView):
154154
permission_classes = [permissions.AllowAny]
155155

156+
def get_provider_info(self) -> dict[str, Any]:
157+
raise NotImplementedError
158+
156159
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-
)
160+
code = request.data.get("code")
161+
if not code:
162+
return Response({"msg": "인가코드가 필요합니다."}, status=status.HTTP_400_BAD_REQUEST)
171163

164+
provider_info = self.get_provider_info()
165+
token_response = self.get_token(code, provider_info)
172166
if token_response.status_code != status.HTTP_200_OK:
173167
return Response(
174-
{"msg": "카카오 서버로 부터 토큰을 받아오는데 실패하였습니다."},
175-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
168+
{"msg": f"{provider_info['name']} 서버로 부터 토큰을 받아오는데 실패하였습니다."},
169+
status=status.HTTP_400_BAD_REQUEST,
176170
)
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-
)
186171

187-
if response.status_code != status.HTTP_200_OK:
172+
access_token = token_response.json().get("access_token")
173+
profile_response = self.get_profile(access_token, provider_info)
174+
if profile_response.status_code != status.HTTP_200_OK:
188175
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,
176+
{"msg": f"{provider_info['name']} 서버로 부터 프로필 데이터를 받아오는데 실패하였습니다."},
177+
status=status.HTTP_400_BAD_REQUEST,
233178
)
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]
242179

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)
180+
return self.login_process_user(profile_response.json(), provider_info)
252181

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-
},
182+
def get_token(self, code: str, provider_info: dict[str, Any]) -> requests.Response:
183+
return requests.post(
184+
provider_info["token_url"],
185+
headers={"Content-Type": "application/x-www-form-urlencoded"},
260186
data={
261-
"client_id": client_id,
262-
"client_secret": client_secret,
263-
"code": code,
264187
"grant_type": "authorization_code",
265-
"redirect_uri": redirect_uri,
188+
"code": code,
189+
"redirect_uri": provider_info["redirect_uri"],
190+
"client_id": provider_info["client_id"],
191+
"client_secret": provider_info.get("client_secret"),
266192
},
267193
)
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}"
194+
195+
def get_profile(self, access_token: str, provider_info: dict[str, Any]) -> requests.Response:
196+
return requests.get(
197+
provider_info["profile_url"],
198+
headers={
199+
"Authorization": f"Bearer {access_token}",
200+
"Content-type": "application/x-www-form-urlencoded;charset=utf-8",
201+
},
278202
)
279203

280-
# 상태코드로 요청이 실패했는지 확인
281-
if info_response.status_code != 200:
282-
return Response(
283-
{"message": "구글 api로부터 액세스토큰을 받아오는데 실패했습니다."},
284-
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
285-
)
204+
def login_process_user(self, profile_res_data: dict[str, Any], provider_info: dict[str, Any]) -> Response:
205+
# 각 provider의 프로필 데이터 처리 로직
206+
email = profile_res_data.get(provider_info["email_field"])
207+
nickname = profile_res_data.get(provider_info["nickname_field"])
208+
profile_img_url = profile_res_data.get(provider_info["profile_image_field"])
209+
if provider_info["name"] == "네이버":
210+
profile_data = profile_res_data.get("response")
211+
if profile_data:
212+
email = profile_data.get(provider_info["email_field"])
213+
nickname = profile_data.get(provider_info["nickname_field"])
214+
profile_img_url = profile_data.get(provider_info["profile_image_field"])
215+
elif provider_info["name"] == "카카오":
216+
account_data = profile_res_data.get("kakao_account")
217+
if account_data:
218+
email = account_data.get(provider_info["email_field"])
219+
profile_data = account_data.get("profile")
220+
if profile_data:
221+
nickname = profile_data.get(provider_info["nickname_field"])
222+
profile_img_url = profile_data.get(provider_info["profile_image_field"])
286223

287-
# 요청의 응답을 json 파싱
288-
res_json = info_response.json()
289-
# 파싱된 데이터중 이메일값을 가져옴
290-
email = res_json.get("email")
291-
# 파싱된 데이터중 닉네임을 가져옴
292-
nickname = res_json.get("nickname")
293224
try:
294225
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
308226
except Account.DoesNotExist:
309-
# 파싱된 데이터에서 프로필 이미지 url을 가져와서 파일로 변환
310-
image_response = urlopen(res_json.get("picture"))
227+
user = self.create_user(email=email, nickname=nickname, profile_img_url=profile_img_url, provider_info=provider_info) # type: ignore
228+
229+
access_token, refresh_token = jwt_encode(user)
230+
response_data = {
231+
"access": str(access_token),
232+
"refresh": str(refresh_token),
233+
"email": user.email,
234+
"nickname": user.nickname,
235+
}
236+
if user.profile_img:
237+
response_data["profile_image"] = user.profile_img.url
238+
return Response(response_data, status=status.HTTP_200_OK)
239+
240+
def create_user(
241+
self, email: str, nickname: str, profile_img_url: Optional[str], provider_info: dict[str, Any]
242+
) -> Account:
243+
if profile_img_url:
244+
image_response = urlopen(profile_img_url)
311245
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)
246+
profile_image = ContentFile(image_content, name=f"{provider_info['name']}-profile-{uuid4_generator(8)}.jpg")
247+
else:
248+
profile_image = None
249+
user = Account.objects.create(email=email, nickname=nickname, profile_img=profile_image)
250+
user.set_unusable_password()
251+
user.save()
252+
return user
253+
254+
255+
class KakaoLoginView(OAuthLoginView):
256+
def get_provider_info(self) -> dict[str, Any]:
257+
return {
258+
"name": "카카오",
259+
"redirect_uri": settings.KAKAO_REDIRECT_URI,
260+
"token_url": "https://kauth.kakao.com/oauth/token",
261+
"profile_url": "https://kapi.kakao.com/v2/user/me",
262+
"client_id": settings.KAKAO_CLIENT_ID,
263+
"client_secret": settings.KAKAO_CLIENT_SECRET,
264+
"email_field": "email",
265+
"nickname_field": "nickname",
266+
"profile_image_field": "profile_image_url",
267+
}
268+
269+
270+
class GoogleLoginView(OAuthLoginView):
271+
def get_provider_info(self) -> dict[str, Any]:
272+
return {
273+
"name": "구글",
274+
"redirect_uri": settings.GOOGLE_REDIRECT_URI,
275+
"token_url": "https://oauth2.googleapis.com/token",
276+
"profile_url": "https://www.googleapis.com/oauth2/v1/userinfo",
277+
"client_id": settings.GOOGLE_CLIENT_ID,
278+
"client_secret": settings.GOOGLE_SECRET,
279+
"email_field": "email",
280+
"nickname_field": "name",
281+
"profile_image_field": "picture",
282+
}
283+
284+
285+
class NaverLoginView(OAuthLoginView):
286+
def get_provider_info(self) -> dict[str, Any]:
287+
return {
288+
"name": "네이버",
289+
"redirect_uri": settings.NAVER_REDIRECT_URI,
290+
"token_url": "https://nid.naver.com/oauth2.0/token",
291+
"profile_url": "https://openapi.naver.com/v1/nid/me",
292+
"client_id": settings.NAVER_CLIENT_ID,
293+
"client_secret": settings.NAVER_CLIENT_SECRET,
294+
"email_field": "email",
295+
"nickname_field": "nickname",
296+
"profile_image_field": "profile_image",
297+
}
333298

334299

335300
# class CustomConfirmEmailView(APIView):

config/settings/base.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"django.middleware.security.SecurityMiddleware",
7979
"django.contrib.sessions.middleware.SessionMiddleware",
8080
"django.middleware.common.CommonMiddleware",
81-
# "django.middleware.csrf.CsrfViewMiddleware",
81+
"django.middleware.csrf.CsrfViewMiddleware",
8282
"django.contrib.auth.middleware.AuthenticationMiddleware",
8383
"django.contrib.messages.middleware.MessageMiddleware",
8484
"django.middleware.clickjacking.XFrameOptionsMiddleware",
@@ -171,7 +171,7 @@
171171
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
172172
"DEFAULT_AUTHENTICATION_CLASSES": (
173173
# "rest_framework_simplejwt.authentication.JWTAuthentication",
174-
# "rest_framework.authentication.SessionAuthentication",
174+
"rest_framework.authentication.SessionAuthentication",
175175
# "rest_framework.authentication.BasicAuthentication",
176176
"dj_rest_auth.jwt_auth.JWTCookieAuthentication",
177177
),
@@ -307,7 +307,7 @@
307307
"PASSWORD_RESET_USE_SITES_DOMAIN": False,
308308
"OLD_PASSWORD_FIELD_ENABLED": False,
309309
"LOGOUT_ON_PASSWORD_CHANGE": False,
310-
"SESSION_LOGIN": False,
310+
"SESSION_LOGIN": True,
311311
"USE_JWT": True, # default: False
312312
"JWT_AUTH_COOKIE": "adfdfd", # default: None
313313
"JWT_AUTH_REFRESH_COOKIE": "rfdfdf", # default: None

config/settings/local.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
"loggers": {
188188
"django.db.backends": {
189189
"handlers": ["console"],
190-
"level": "DEBUG",
190+
"level": "INFO",
191191
},
192192
"django": {
193193
"handlers": ["console", "file"],
@@ -214,10 +214,12 @@
214214
EMAIL_CODE_TIMEOUT = env("EMAIL_CODE_TIMEOUT")
215215
DJANGO_SUPERUSER_EMAIL = env("DJANGO_SUPERUSER_EMAIL")
216216
DJANGO_SUPERUSER_PASSWORD = env("DJANGO_SUPERUSER_PASSWORD")
217-
REDIRECT_URI = env("REDIRECT_URI")
217+
KAKAO_REDIRECT_URI = env("KAKAO_REDIRECT_URI")
218218
KAKAO_CLIENT_ID = env("KAKAO_CLIENT_ID")
219219
KAKAO_CLIENT_SECRET = env("KAKAO_CLIENT_SECRET")
220+
GOOGLE_REDIRECT_URI = env("GOOGLE_REDIRECT_URI")
220221
GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID")
221222
GOOGLE_SECRET = env("GOOGLE_SECRET")
223+
NAVER_REDIRECT_URI = env("NAVER_REDIRECT_URI")
222224
NAVER_CLIENT_ID = env("NAVER_CLIENT_ID")
223225
NAVER_CLIENT_SECRET = env("NAVER_CLIENT_SECRET")

config/settings/prod.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,11 @@
156156
EMAIL_CODE_TIMEOUT = ENV["EMAIL_CODE_TIMEOUT"]
157157
DJANGO_SUPERUSER_EMAIL = ENV["DJANGO_SUPERUSER_EMAIL"]
158158
DJANGO_SUPERUSER_PASSWORD = ENV["DJANGO_SUPERUSER_PASSWORD"]
159-
REDIRECT_URI = ENV["REDIRECT_URI"]
159+
KAKAO_REDIRECT_URI = ENV["KAKAO_REDIRECT_URI"]
160160
KAKAO_CLIENT_ID = ENV["KAKAO_CLIENT_ID"]
161+
GOOGLE_REDIRECT_URI = ENV["GOOGLE_REDIRECT_URI"]
161162
GOOGLE_CLIENT_ID = ENV["GOOGLE_CLIENT_ID"]
162163
GOOGLE_SECRET = ENV["GOOGLE_SECRET"]
164+
NAVER_REDIRECT_URI = ENV["NAVER_REDIRECT_URI"]
165+
NAVER_CLIENT_ID = ENV["NAVER_CLIENT_ID"]
166+
NAVER_CLIENT_SECRET = ENV["NAVER_CLIENT_SECRET"]

tools/test_chat_locust.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def on_start(self) -> None:
6464

6565
if csrf and access and session_cookie:
6666
self.connect(
67-
f"ws://{os.environ.get("BACKEND_HOST")}/ws/chat/{self.chatroom_id}/",
67+
f"wss://{os.environ.get("BACKEND_HOST")}/ws/chat/{self.chatroom_id}/",
6868
header=[f"X-CSRFToken: {csrf}", f"Authorization: Bearer {access}"],
6969
cookie="sessionid=" + session_cookie,
7070
)

0 commit comments

Comments
 (0)