개인 프로젝트를 진행하면서 소셜로그인을 구현해야겠다는 생각으로 진행했다.
처음 카카오 로그인과, 깃허브 로그인을 진행했었는데, 이것들은 의외로 설명대로 하니 바로 되어서 1~2일 만에 가능했다. 그런데 구글로그인에서 너무 막혀서 DB자체를 싹 갈아엎고, 3일을 걸려서 겨우 성공했다.
초보 개발자(호소인)이므로 그냥 내가 나중에 보고 참고하기 위한 글이기에 제대로 안적혀 있을 수 있으니 참고하길 바람.
django로 구글로그인을 진행하는데, 자료가 많아보이는데 많지가 않았다. 다들 하고 나면 생각보다 수월해서 그런가... 하꼬개발자인 난 너무 힘드러따 흑흑흑
https://console.cloud.google.com/apis/credentials?project=analog-woodland-424805-e2
여기 페이지에 들어가서 필요한 절차를 끝냈다. 이 부분은 검색하면 많이 나오니 여기선 패스
그리고 코드를 작성하면서 아래 2블로그를 참고하였다.
그런데 문제는 이렇게하면 django에서만 돌리면 가능한데, django를 server로 이용하려니 제대로된 작동이 되지않았다.
우선 사용하기는 dj-rest-auth를 사용하면서, user도 custom해주었다.
내가 지금 하는 방식은 개별 소셜 로그인도 하나의 사용자가 되게하기 때문에, 제안하는 방식과는 조금 다르게 진행했다.
우선 처음 작성한 코드는 아래처럼이다.
완벽한 코드는 아니지만, 작동하는 것을 확인했다.
내가 처음 이 코드를 보면서 이해가 제대로 되지 않은 이유가 google_callback이라는 것 때문이다.
redirect_uri에서 코드를 받아오고 이 코드를 이용해서 토큰과 정보를 받아오는데,
코드를 받을때도 있고, 토큰을 받을 때도 있으니, 머릿속에서 제대로 정립이 되지않았다.
그냥 django에서는 토큰을 받도록하는 api를 redirect_uri라고 생각하고, 프론트에서는 code를 받는 곳을 redirect_uri라고 생각하기로 했다.
path('google/login/', views.google_login, name='google_login'),
path('google/callback/', views.google_callback, name='google_callback'),
BASE_URL = 'http://127.0.0.1:8000/'
# GOOGLE 소셜로그인
state = os.environ.get("STATE")
GOOGLE_CALLBACK_URI = BASE_URL + 'accounts/google/callback/'
## 1번
def google_login(request):
scope = "https://www.googleapis.com/auth/userinfo.email"
client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
return redirect(f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&response_type=code&redirect_uri={GOOGLE_CALLBACK_URI}&scope={scope}")
#######
def google_callback(request):
client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
client_secret = os.environ.get("SOCIAL_AUTH_GOOGLE_SECRET")
# 사용자의 구글 코드
code = request.GET.get('code')
print('코드', code)
# 1. 받은 코드로 구글에 access token 요청
token_req = requests.post(
f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={GOOGLE_CALLBACK_URI}&state={state}")
### 1-1. json으로 변환 & 에러 부분 파싱
token_req_json = token_req.json()
error = token_req_json.get("error")
print(token_req)
print(token_req_json)
### 1-2. 에러 발생 시 종료
if error is not None:
raise JSONDecodeError(error)
### 1-3. 성공 시 access_token 가져오기
access_token = token_req_json.get('access_token')
#################################################################
# 2. 가져온 access_token으로 이메일값을 구글에 요청
email_req = requests.get(f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}")
email_req_status = email_req.status_code
### 2-1. 에러 발생 시 400 에러 반환
if email_req_status != 200:
return JsonResponse({'err_msg': 'failed to get email'}, status=status.HTTP_400_BAD_REQUEST)
### 2-2. 성공 시 이메일 가져오기
email_req_json = email_req.json()
email = email_req_json.get('email')
user_id = email_req_json.get('user_id')
print('email_req ===', email_req)
print('email_req.json() ===', email_req.json())
print('email_req_status ===', email_req_status)
print('user_id ===', user_id) #str
print('이건 작동함')
# return JsonResponse({'access': access_token, 'email':email})
# 유저 여부 확인 후 있으면 로그인, 없으면 회원가입 후 로그인
# db를 social에 국한 하지 말고 완전히 따로따로 만들자.
try:
user = User.objects.get(email=email, social='google')
refresh_token = RefreshToken.for_user(user) # jwt발급
response_data = {
'refresh': str(refresh_token),
'access': str(refresh_token.access_token),
# 'email': email,
'message': '가져오기 성공',
'userInfo' : {
'nickname' : user.nickname,
'social' :user.social,
'email' : user.email,
'profile_url' : user.profile_image_url
}
}
print('response_data====', response_data)
# return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
return JsonResponse(response_data, status=status.HTTP_400_BAD_REQUEST)
except User.DoesNotExist:
nickname = f"{random.choice(nickname_a)}{random.choice(nickname_b)}{random.randint(1000, 9999)}"
data = {
'email' : email,
'username' : email + str(random.randint(100000, 1000000)),
'nickname' : nickname,
'social' : 'google',
'social_id' : str(user_id),
}
try:
user = User.objects.create(
email=data.get("email"),
username=data.get("username"),
nickname=data.get("nickname"),
social=data.get("social"),
social_id=data.get("social_id")
)
user.set_unusable_password() # 소셜로그인이니까 No password!
user.save()
user = User.objects.get(username=data.username, social=data.social, email=email)
refresh_token = RefreshToken.for_user(user) # 자체 jwt 발급
response_data = {
'refresh': str(refresh_token),
'access': str(refresh_token.access_token),
'message': '저장성공',
'userInfo' : {
'nickname' : user.nickname,
'social' :user.social,
'email' : user.email,
'profile_url' : user.profile_image_url
}
}
print('response_data====', response_data)
# return Response(response_data, status=status.HTTP_201_CREATED)
return JsonResponse(response_data, status=status.HTTP_400_BAD_REQUEST)
except:
response_data = {
'message': '저장실패'
}
print('response_data====', response_data)
# return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
return JsonResponse(response_data, status=status.HTTP_400_BAD_REQUEST)
리액트에서 로그인 할 수 있게 버튼을 먼저 만들어줬다.
<div onClick={() => {navigateGoogleLogin()}}>
<img src={process.env.PUBLIC_URL+"images/googleLogo.png"} alt=""/>
<span className='loginBtnFont googleTextColor'>Google 로그인</span>
<div></div>
</div>
그리고 눌렀을 때 작동할 함수를 작성해줬다.
winodw.locaiton.href를 통해서 코드를 받아올 수 있게 하였다.
const googleScope = "https://www.googleapis.com/auth/userinfo.email"
const googleClientId = process.env.REACT_APP_GOOGLE_CLIENT_ID
// const googleCallBackUri = base_url + 'accounts/google/callback/'
const googleRedirectUri = process.env.REACT_APP_GOOGLE_REDIRECT_URI;
function navigateGoogleLogin(){
window.location.href =`https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&response_type=code&redirect_uri=${googleRedirectUri}&scope=${googleScope}`
}
또한 redirection주소에 맞는 컴포넌트를 만들어줬다. 그곳으로 이동하도록 해줬다.
GoogleRedirection.tsx
그리고 이 안에서 code를
const code = params.get("code");
를 통해 해당 컴포넌트에서 받아오고, 설정해준 back server로 토큰과 정보를 발급받을 수 있게 post요청을 보내도록했다.
// 구글 로그인
export async function getGoogleLoginCode(code:string) { // async, await을 사용하는 경우
try {
const response = await axios.post(`${base_url}accounts/google/callback/`, { code: code})
return response
} catch (e) {
// 실패 시 처리
console.error(e.response.status);
}
}
django에서는 urls.py를 토큰을 받아올 수 있도록 하는 1개의 주소만을 사용했다.
path('google/callback/', views.google_callback, name='google_callback'),
그리고 google_callback은 아래처럼 했다.
```python
# GOOGLE 소셜로그인
state = os.environ.get("STATE") # 이건 정확히 어떻게 쓴느건지 모르겠지만 아무렇게 적었다.
GOOGLE_CALLBACK_URI = 설정한 redirect_url # 코드 받아오는 컴포넌트 주소
@api_view(['GET','POST'])
def google_callback(request):
if request.method == 'POST':
print('request.data=-==',request.data)
client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
client_secret = os.environ.get("SOCIAL_AUTH_GOOGLE_SECRET")
# 사용자의 구글 코드
# code = request.GET.get('code')
code = request.data.get('code')
print('코드===', code)
# 1. 받은 코드로 구글에 access token 요청
token_req = requests.post(
f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={GOOGLE_CALLBACK_URI}&state={state}")
### 1-1. json으로 변환 & 에러 부분 파싱
token_req_json = token_req.json()
error = token_req_json.get("error")
print(token_req)
print(token_req_json)
### 1-2. 에러 발생 시 종료
if error is not None:
raise JSONDecodeError(error)
### 1-3. 성공 시 access_token 가져오기
access_token = token_req_json.get('access_token')
#################################################################
# 2. 가져온 access_token으로 이메일값을 구글에 요청
email_req = requests.get(f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}")
email_req_status = email_req.status_code
### 2-1. 에러 발생 시 400 에러 반환
if email_req_status != 200:
# return JsonResponse({'err_msg': 'failed to get email'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'err_msg': 'failed to get email'}, status=status.HTTP_400_BAD_REQUEST)
### 2-2. 성공 시 이메일 가져오기
email_req_json = email_req.json()
email = email_req_json.get('email')
user_id = email_req_json.get('user_id')
print('email_req ===', email_req)
print('email_req.json() ===', email_req.json())
print('email_req_status ===', email_req_status)
print('user_id ===', user_id)
print('이건 작동함')
# return JsonResponse({'access': access_token, 'email':email})
# 유저 여부 확인 후 있으면 로그인, 없으면 회원가입 후 로그인
# db를 social에 국한 하지 말고 완전히 따로따로 만들자.
try:
user = User.objects.get(email=email, social='google')
refresh_token = RefreshToken.for_user(user) # jwt발급
response_data = {
'refresh': str(refresh_token),
'access': str(refresh_token.access_token),
# 'email': email,
'message': '가져오기 성공',
'userInfo' : {
'nickname' : user.nickname,
'social' :user.social,
'email' : user.email,
'profile_url' : user.profile_image_url
}
}
print('response_data1====', response_data)
return Response(response_data, status=status.HTTP_202_ACCEPTED)
# return JsonResponse(response_data, status=status.HTTP_202_ACCEPTED)
except User.DoesNotExist:
nickname = f"{random.choice(nickname_a)}{random.choice(nickname_b)}{random.randint(1000, 9999)}"
data = {
'email' : email,
'username' : email + str(random.randint(100000, 1000000)),
'nickname' : nickname,
'social' : 'google',
'social_id' : str(user_id),
}
try:
user = User.objects.create(
email=data.get("email"),
username=data.get("username"),
nickname=data.get("nickname"),
social=data.get("social"),
social_id=data.get("social_id")
)
user.set_unusable_password() # 소셜로그인이니까 No password!
user.save()
user = User.objects.get(username=data.username, social=data.social, email=email)
refresh_token = RefreshToken.for_user(user) # 자체 jwt 발급
response_data = {
'refresh': str(refresh_token),
'access': str(refresh_token.access_token),
'message': '저장성공',
'userInfo' : {
'nickname' : user.nickname,
'social' :user.social,
'email' : user.email,
'profile_url' : user.profile_image_url
}
}
print('response_data2====', response_data)
return Response(response_data, status=status.HTTP_201_CREATED)
# return JsonResponse(response_data, status=status.HTTP_400_BAD_REQUEST)
except:
response_data = {
'message': '저장실패'
}
print('response_data3====', response_data)
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
# return JsonResponse(response_data, status=status.HTTP_400_BAD_REQUEST)
response_data = {
'message': '저장실패'
}
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
코드는 POST요청만으로 가능하게 했고, 첫 try except문에서 유저가 있는지를 확인했다.
그리고 두번째 try except문에서 유저 정보 데이터를 저장하기로 했다.
참고로 개인적으로 쓰고 있는 컴럼들인 nickname과 social, social_id는 각각 닉네임, 어느 소셜 로그인인지('google','kakao','github',...), 해당 소셜의 id를 기입할 수 있도록 만들었다.
우선 allauth에서 제공해주는 social db를 이용하지 않는다는 것과, 토큰 또한 simple-jwt에서 제공해주는 수동 토큰 제작 방법으로 토큰을 전달하게 했다.
번거롭긴 하겠지만, 이 방법이 내가 만듦에 있어서는 더 직관적이라고 판단했기에 이 방식을 채택했다.
Creating tokens manually
from rest_framework_simplejwt.tokens import RefreshToken
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
로그아웃은 dj-rest-auth에서 제공해주는 앤드포인트를 이용했다.
dj-rest-auth/logout/
dj-rest-auth/token/verify/
dj-rest-auth/token/refresh/
를 이용했다.
이렇게 하니 그래도 어찌저찌 작동이 된다. jwt로 토큰도 잘 받아오는 것을 확인할 수 있었다.
어쩌다 보니 jwt까지 손을 대긴 했는데, 이거 permission을 어떻게 건드려야할 지 모르겠다.
또한, axios요청시 header를 어떻게 해야할지도 감이 안온다. 하다보면 되겠지...?!
다시 잘되네?? 헤더를 아래의 양식으로 주니 잘 된다.
headers : {
"Authorization" : `Bearer ${token}`
}
수정을 하나 하긴 해야할 듯 하다.
분명 저장도 잘되는것을 확인했는데, 저장하고, 즉시 들고 오는것이 불가능한 건지 에러가발생했다.
db를지우고 다시시도도 해봐야겠다.
물론 이미 DB로 저장됬으므로 처음이 안되는거지 이후에는 잘 들고온다.
user = User.objects.get(username=data.username, social=data.social, email=email)
이 부분이 잘 못 되어있었다.
수정후 꺼낼 때, 아래처럼 해야한다. data.get("") 또는 data[""]의 방식으로 해야되는데 이 부분을 잘 못 작성했다. 아래처럼 수정하니 문제는 해결되었다.
user = User.objects.get(username=data.get("username"), social=data.get("social"), email=email)