Dev/Django

WebSite(Class101) Clone Project - Part2.구현 파트 정리(회원가입/로그인 소셜로그인)

sincerely10 2020. 8. 17. 14:52
반응형

안녕하세요. 이 포스트는 시리즈로 구성되어있습니다.

저번 포스트에 이어서 작성하도록 하겠습니다.
클래스 101이라는 사이트에서 제가 맡은 파트에 대해 대략적인 리뷰와 코드를 작성하는 형태로 포스팅을 하겠습니다.

1. user(account)의 회원가입/로그인

가장 기본이 되는 회원가입/로그인이지만 회원가입 때는 특별히 Regular Expression(정규표현식)으로 Validation check를 했습니다.
저는 Django에 내장되어 있는 django.core.validators 라이브러리의 RegexValidator를 import해 사용했습니다. 각 validator는 다음과 같이 선언하고 예외 체크를 했습니다.

class SignUpView(View):
    def post(self, request):
        data                   = json.loads(request.body)
        email_validator        = RegexValidator(regex = "^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$")
        password_validator     = RegexValidator(regex = "^[A-Za-z0-9!@#$%^&+=]{8,100}$")
        phone_number_validator = RegexValidator(regex = "^[0-9]{9,14}$")

        try:
            email_validator(data['email'])
            password_validator(data['password'])
            phone_number_validator(data['phoneNumber'])
            
            ......
            
        except ValidationError:
            return JsonResponse({'message':'INVALID_EMAIL_OR_PASSWORD_OR_PHONE_NUMBER'}, status=400)

정규표현식은 전부 작성하지 않고 이메일, 패스워드, 핸드폰 번호는 대부분 양식이 비슷하기 때문에 일반적인 정규표현식에서 클론 사이트에 맞게 수정하였습니다(자릿수나 포함하는 문자열 등). 그리고 아래와 같이 exception의 경우를 별도 처리했습니다.

2. 소셜 로그인(카카오, 페이스북)

먼저 각 소셜 앱의 소셜로그인에 대한 API 설명 페이지는 다음과 같습니다.

카카오: https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
페이스북: https://developers.facebook.com/docs/graph-api/using-graph-api/common-scenarios/?translation#------------

user 앱과 관련된 API를 만들었기 때문에 카카오와 페이스북 로그인도 만들었습니다. 소셜로그인을 진행할 때, 다른 라이브러리를 활용하는 것이 아닌 구글의 OAuth 2.0만을 활용하여 토큰을 받고 로그인되게끔 했습니다.

아래는 Oauth 2.0의 Flow Chart입니다.

Google Oauth2.0의 인증과정(출처: luiseok.com)

위 Flow에 대해 이해한 과정을 기술해보겠습니다.

1) 로그인 버튼 클릭 시, 소셜 앱(구글, 카카오, 페이스북 등)에 요청 전송

카카오 같은 경우 REST_API_KEY와 REDIRECT_URI를 통해 아래와 같이 request 합니다.

GET /oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code HTTP/1.1
Host: kauth.kakao.com

여기서 REST_API_KEY는 Third Party App(소셜 로그인을 이용해 사용자를 등록하는 앱 - 여기서는 클래스 101의 클론 사이트)에 대한 권한을 주는 key이고,
REDIRECT_URI는 Third Party App에서 로그인이 완료된 경우, 넘어가는 화면을 의미합니다.
저희 팀은 이 과정을 Front-End에서 진행하였습니다.

2) 유저가 미 로그인 상태의 경우 로그인 창을 띄우고 계정을 입력 받음 - 구글 계정으로 로그인

새로운 창이 뜨면 Third Party App에서는 진행할 부분이 없습니다. 여기서는 소셜앱에서 인증을 해야 하기 때문이죠. Front-End에서는 로그인 결과에 따른 Response만을 받게 됩니다.
여기서 Response는 정상 로그인이 된 경우 Authorization Code를 받고, 그렇지 않다면 acccess_denied라는 message를 받습니다.

3) 유저로 부터의 권한 획득

2)에서 만약 한 번도 가입하지 않은 유저라면 초기에 필수 또는 선택으로 얻어야 할 권한을 얻어야 합니다. 2)와 마찬가지로 허가를 하면 Authorization Code 그렇지 않으면 access_denide라는 메세지를 받습니다.

4) Response인 Authorization Code를 백엔드 서버에 전송

위 과정에서 얻어진 Authorization Code(이름과 형태는 조금씩 다르지만)를 Front-End에서 받았다면, 이를 Back-End에 전송해주어야 합니다. 물론 무조건 그래야 하는 것은 아니지만 이 과정은 저장된 데이터(mysql과 같은 RDB나 mongoDB와 같은 NoSQL)에서 다뤄 주어야 하기 때문이죠.

저의 프로젝트에서는 Header에 이 Authorization Code 값을 가지고 GET으로 카카오 서버에 요청했습니다.
여기서 헷갈리시기 쉬운 게 이 Authoriztion Code는 소셜 앱에 전달해줘서 정보를 받아오기 위한 목적이지 Third Party App에서 Front-End 단에 전달해줘서 유저를 식별하는 Access Token 값이 아닌 것입니다.

5) 백엔드 서버에서 소셜앱 서버로 유저 정보 요청

4)의 과정에서 GET으로 요청을 하면 소셜앱의 서버에서 사용자가 체크한 정보를 return 해줍니다.

6) 회원 가입 또는 로그인을 하고 Access Token return

만약 기존에 존재하지 않는 회원인 경우 DB에 저장을 하고 Access Token을 return 해줍니다. 기존 회원인 경우 Access Token만 return 해줍니다.

백엔드에서의 처리 과정만 담은(4~6)의 카카오 소셜 로그인 코드를 나타내면 아래와 같습니다.

class KakaoSignInView(View):
    def post(self, request):

        try:
            data            = json.loads(request.body) #4)Front End에서 전달한 code 받아오기
            profile_request = requests.get(    #5)Kakao 서버에 user 정보 요청하기
                "https://kapi.kakao.com/v2/user/me",
                headers = {"Authorization" : f"Bearer {data['access_token']}"}
            )
			
            profile_json  = profile_request.json()
            kakao_account = profile_json.get("kakao_account", None)
            if not kakao_account:
                return JsonResponse({"message" : "INVALID_TOKEN"}, status = 400)

            kakao_profile = kakao_account.get("profile", None)
            email         = kakao_account.get("email", None)
            name          = kakao_profile.get("nickname", None)
	    #6)회원가입 또는 로그인 하기
            if not User.objects.filter(email = email).exists():
                User.objects.create(
                    email = email,
                    name  = name
                )

            kakao_login_user = User.objects.get(email = email)
            token            = jwt.encode({"id" : kakao_login_user.id}, SECRETKEY['secret'], algorithm = ALGORITHM['algorithm'])
            access_token     = token.decode("utf-8")

            return JsonResponse({"access_token" : access_token}, status = 200)

        except KeyError:
            return JsonResponse({"message" : "INVALID_TOKEN"}, status = 400)

 

 

페이스북도 크게 다르지 않은 과정이었습니다. 다만 페이스북은 HTTPS의 Third Party App만을 지원하는 것이 큰 벽이 되어 결국 확인을 하지 못 했습니다. 그럼에도 코드를 공유하면 다음과 같습니다.

class FacebookSignInView(View):
    def post(self, request):
        data = json.loads(request.body)

        try:
            profile_request = requests.get(
                'https://graph.facebook.com/me',
                params = {'fields' : 'email, name', 'access_token' : data['access_token']}
            )

            profile_json = profile_request.json()
            email        = profile_json.get('email', None)
            name         = profile_json.get('name', None)

            if not User.objects.filter(email = email).exists():
                User(
                    email = email,
                    name  = name
                ).save()

            facebook_login_user = User.objects.get(email = email)
            token               = jwt.encode({'id' : facebook_login_user.id}, SECRET['secret'], algorithm = ALGORITHM['algorithm'])
            access_token        = token.decode('utf-8')

            return JsonResponse({'access_token' : access_token}, status = 200)

        except KeyError:
            return JsonResponse({'message' : 'INVALID_TOKEN'}, status = 400)

다음 포스트에 이어서 elastic search를 활용한 검색 API 생성과정을 리뷰해보겠습니다.

반응형