이 포스팅을 통해 정리하고자 하는 것.
1. OAuth 2.0이 무엇인지 알고 갑니다.
2. 프론트(React) 와 백엔드(django)의 REST API 통신 흐름을 명확하게 잡습니다.
1. OAuth가 뭔가요?
■ 외부서비스에서 인증을 가능하게 하고 그 서비스의 API를 이용하게 해주는 것.
자신이 소유한 리소스에 소프트웨어 애플리케이션이 접근할 수 있도록 허용해 줌으로써 접근 권한을 위임해주는 개방형 표준 프로토콜
■ 일반 로그인은 회원가입할 때 사용했던 아이디와 비밀번호를 통한 인증(Authentication)이라면
■ OAuth는 타사 서비스 (Google, facebook)의 이메일 정보에 우리가 만든 서비스의 접근을 인가(Authorization) 하여 사용자를 인증(Authentication) 한다.
■ Authentication(인증) : 유저가 누구인지 확인하는 절차 ex) 회원가입, 로그인
■ Authorization(인가) : 유저에 대한 권한을 허락하는 것 ex) 게시판에서 다른 사람이 쓴 글을 나는 수정할 수 없다.
OAuth1.0 → OAuth2.0 뭐가 달라졌나요?
■ 기능의 단순화, 규모의 확장성을 지원하기 위해 만들어졌다. 웹서비스 → 웹&앱
■ 인증 및 보안 : 서명 → https 기본
■ 접근 토큰에 유효기간을 부여하고, 만료 시 재발급 토큰을 이용하게 한다. ( 전에는 만료기간이 없었음 )
2. 소셜 로그인 REST API 흐름
[삽질 스토리]
처음에 소셜 로그인을 구현하면서 특정 블로그를 정독하며 따라가서 구현하긴 했는데(나는 백엔드다),
[백에서 1~7번]을 다 맡아서 구현(블로그 방식)을 하니 토큰이 발행이 되긴 했지만 프론트에서 토큰을 제어할 수가 없었다.
왜 그럴까 생각하다가 보니 원인을 찾았다.
프론트서버의 요청을 받고 토큰을 응답에 실어서 보내는 형태[3번, 7번]가 되어야 프론트서버가 토큰을 활용할 텐데, [백에서 1~7번]을 해버리면서 요청 과정이 없어 응답을 돌려줄 열려있는 pipeline이 없었다.
이거 잘못되었구나 싶어서, 프론트에게 연락해 1,2,3번에 해당하는 로직을 떼어주고 해야 하는 필요한 요청을 설명해주고
결국 밑의 1~8번 흐름이 완성이 되었다.(프론트분들 많이 귀찮게 했는데 잘 들어주시고 구현해주셔서 감사했다..)
이제부터 [프론트에서의 작업 1~3번, 8번+ 백에서의 4~7번]을 같이 살펴보자
(구글 클라우드 서비스 등록 관련은 따로 정리하지 않았습니다)
[1] 로그인, 동의 여부 확인
구글소셜 로그인 버튼에 아래 url설정을 해주어야 한다.
const GOOGLE_AUTH_URL = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${SOCIAL_AUTH_GOOGLE_CLIENT_ID}&response_type=code&redirect_uri=${OAUTH2_REDIRECT_URI}&scope=${scope}`;
여기서 필요한 변수는 3개다.
1. {SOCIAL_AUTH_GOOGLE_CLIENT_ID}
https://console.developers.google.com/apis/
■ 이동 후, 발급받은 구글 소셜로그인 API 키를 넣어주면 된다. private 한 정보라 노출이 되지 않게 주의하자!
2. {OAUTH2_REDIRECT_URI}
■ 인가 code를 받아올 프론트쪽의 URI를 설정해주면 된다.
■ 이 정보는 위의 구글 클라우드에 정보를 저장해주어야 한다.
■ (이 코드는 이후 백에서 쓸 redirect_uri와 같다)
3. {scope}
const scope = 'https://www.googleapis.com/auth/userinfo.email'
■ 이 친구는 고정값이다.
[2] 인가 코드 받아오기
■ 사용자가 위의 소셜 로그인 버튼을 눌러 정보를 입력하고 동의를 완료하고 나면 구글 서버는 인가 코드를 발급해서 설정해둔 redirect_url에 돌려준다.
■ code = %^$%@$@$#%@^@#%$ << 이런 식으로 parameter로 따라 들어온다.
[3] [GET 요청] 받아온 인가 코드를 백 서버로 넘겨주기
■ 프론트에 받아온 get 요청을 back으로 parameter로 넘겨달라고 요청했더니 아래와 같은 코드를 짜서 넘겨주셨다.
■ 요청(3번)에 대한 응답 (7번)으로 받은 jwt 토큰을 로컬 스토리지에 담는 것까지 필요하다.
const LoginPage = () => {
const navigate = useNavigate();
const [sessionData, setSessionData] = useRecoilState(session);
let code = new URL(window.location.href).searchParams.get("code");
useEffect(() => {
axios
.get("https://cameet.site/accounts/google/callback/", {
params: { code: code },
})
.then((res) => {
setSessionData(res.data);
window.localStorage.setItem("access_token", res.data.access_token);
window.localStorage.setItem("refresh_token", res.data.refresh_token);
navigate("/firstinfo");
});
}, [code]);
[8] 받아온 토큰을 로컬스토리지에 저장 후, 전역변수로 사용할 수 있게 설정해주고, 보내는 요청마다 헤더에 토큰을 담아 보낸다 (인가).
import { atom } from "recoil";
export const session = atom({
key: "session",
default: {
access_token: window.localStorage.getItem("access_token"),
refresh_token: window.localStorage.getItem("refresh_token"),
},
});
[백엔드 파트]
cameet.site/accounts/google/callback/ << 요청이 들어오면 실행되는 view.py
def google_callback(request):
state='random'
client_id = getattr(settings, "SOCIAL_AUTH_GOOGLE_CLIENT_ID")
client_secret = getattr(settings, "SOCIAL_AUTH_GOOGLE_SECRET")
code = request.GET.get('code')
#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={OAUTH2_REDIRECT_URI}&state={state}")
token_req_json = token_req.json()
access_token = token_req_json.get('access_token')
#email정보를 요청하자.
email_req = requests.get(
f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}")
email_req_status = email_req.status_code
if email_req_status != 200:
return JsonResponse({'err_msg': 'failed to get email'}, status=status.HTTP_400_BAD_REQUEST)
email_req_json = email_req.json()
email = email_req_json.get('email')
print(email)
"""
Signup or Signin Request
"""
try:
user = User.objects.get(email=email)
# 기존에 가입된 유저의 Provider가 google이 아니면 에러 발생, 맞으면 로그인
# 다른 SNS로 가입된 유저
social_user = SocialAccount.objects.get(user=user)
if social_user is None:
return JsonResponse({'err_msg': 'email exists but not social user'}, status=status.HTTP_400_BAD_REQUEST)
if social_user.provider != 'google':
return JsonResponse({'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST)
# 기존에 Google로 가입된 유저
print("기존에 있는 유저")
data = {'access_token': access_token, 'code': code}
accept = requests.post(
f"{BASE_URL}accounts/google/login/finish/", data=data)
accept_status = accept.status_code
if accept_status != 200:
return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status)
accept_json = accept.json()
accept_json.pop('user', None)
return JsonResponse(accept_json)
except User.DoesNotExist:
# 기존에 가입된 유저가 없으면 새로 가입
print("새로가입한 유저")
data = {'access_token': access_token, 'code': code}
accept = requests.post(
f"{BASE_URL}accounts/google/login/finish/", data=data)
print("post하고왔음")
accept_status = accept.status_code
if accept_status != 200:
return JsonResponse({'err_msg': 'failed to signup'}, status=accept_status)
accept_json = accept.json()
accept_json.pop('user', None)
return JsonResponse(accept_json)
class GoogleLogin(SocialLoginView):
adapter_class = google_view.GoogleOAuth2Adapter
callback_url = GOOGLE_CALLBACK_URI
client_class = OAuth2Client
[코드 설명]
[4] 받아온 인가 코드를 구글 서버에 보내 access_token을 받아보자
state='random'
client_id = getattr(settings, "SOCIAL_AUTH_GOOGLE_CLIENT_ID")
client_secret = getattr(settings, "SOCIAL_AUTH_GOOGLE_SECRET")
code = request.GET.get('code')
#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={OAUTH2_REDIRECT_URI}&state={state}")
token_req_json = token_req.json()
access_token = token_req_json.get('access_token')
토큰을 요청할 때 필요한 변수는 4개다
1. {client_id} , {client_secret}
2. {code}
■ 프론트에서 받은 인가 코드를 뽑아서 넣어주자.
3. {OAUTH2_REDIRECT_URI}
■ 진짜 이 부분에서 엄청 헤맸다.
서버가 요청하니까 REDIRECT는 서버가 받아야지 암!! 이러면서 서버의 URL 주소를 설정해놨는데...
왜 안되지??????를 진짜 며칠을 고민했다.
■ 결론적으로는 이 부분은 구글 서버에 CODE를 요청한 서버의 REDIRECT_URI와 같아야 한다는 사실을 알게 되었다. 우린 프론트서버에서 CODE를 요청했으므로 프론트서버가 위에 설정해둔 같은 URL를 적어두자!
4. {state}
■ random이란 string값을 넣어준다.
[5,6,7] 설치한 Google OAuth2 패키지를 이용해 로직이 실행된다.
■ 뭔가 급마무리가 된 것 같지만 진짜 정말 해준다. ctrl+클릭클릭으로 패키지를 다 뜯어보는데 아 이래서 장고가 제공해주는 편의성이 좋다는 거구나 생각이 들었다.
class GoogleLogin(SocialLoginView):
adapter_class = google_view.GoogleOAuth2Adapter
callback_url = GOOGLE_CALLBACK_URI
client_class = OAuth2Client
■ 이 세줄로 회원가입이 되어있지 않은 경우 구글에서 받아온 정보를 토대로 내가 설정한 model에 착착 맞게 db에 저장하고 토큰 발행해서 돌려주고, 이미 회원인 경우여도 토큰을 발행해준다.
장고 서버의 DRF 환경설정은 너무나도 정리를 잘해주신 블로거분이 계셔서 이분 글을 참고했다.
마무리 하며..
소셜로그인을 구현하기 까지, 결국에 부족했던건 서버간의 REST한 HTTP통신에 관련한 CS지식이었는데
'HTTP 완벽가이드'를 읽고 정리한게 큰 도움이 되었다.
그리고
[참고 url]
https://wookcode.tistory.com/m/182
'프로젝트 > 창업 프로젝트 (DRF + AWS)' 카테고리의 다른 글
[AWS][24/365 장애 없는 서비스 환경]을 위한 캐밋의 로드맵 (0) | 2022.10.31 |
---|---|
[창업 지원금][AWS 세팅] 대학생창업 중간 회고 (4) | 2022.10.10 |
[점검] 프로젝트 피드백, 단기 목표 설정 (0) | 2022.09.06 |
[AWS EC2 배포][ubuntu] 배포하며 쌓은 내실을 정리해 보자 (0) | 2022.08.31 |
[프로젝트][에러][uWSGI] 클라이언트 연결 닫음 [해결] (0) | 2022.08.29 |