[빌리지 프로젝트] Kakao 로그인 기능을 추가해보자

wodnr_P·2023년 8월 17일
0

이전까지 공지 게시글, 관리비 장부를 마무리 했다고 생각했지만 한 가지 빠뜨린 요구사항이 있었습니다.

같은 건물 코드를 공유하고 있는 관리자와 사용자만이 글과 장부를 조회할 수 있어야 한다는 것을 구현해야 했고, 이 때문에 일부 로직을 수정했습니다.

추가적으로 일반 사용자는 카카오 로그인만으로 사용자 인증을 할 수 있다는 기능을 추가 해보았습니다.


📌 일부 로직 수정

요구 사항

  • 같은 건물 코드를 공유하고 있는 관리자와 사용자만 공지 글과 관리비 장부를 조회할 수 있어야한다.

위 요구 사항을 충족 시키기 위해서 핵심은 다음과 같습니다.

  • 관리자는 건물 고유 코드를 가지고 있어야 함.
  • 공지 게시글과 관리비 장부를 조회하는 로직에서는 사용자의 건물 고유 코드를 확인하는 과정이 필요.

이를 코드로 구현하기 위해 먼저 관리자 회원가입 로직을 수정했습니다.

# building_name을 hash_encoding
def generate_building_code(building_name):
    hash_text = hashlib.sha256(building_name.encode()).hexdigest()
    return hash_text[:20]
    
class AdminSignupAPIView(APIView):
    def post(self, request):
    ...
    # 관리자 building_code 생성 및 저장 
        if request.data['building_name'] is not None:
            building_code = generate_building_code(request.data['building_name'])
            ...

관리자 회원가입 시 요청 받은 건물의 이름을 generate_building_code 함수를 통해 해쉬 인코딩 후, 앞에서 부터 20자리까지 슬라이싱하여 building_code를 생성 해주었습니다.

생성된 building_code는 마이페이지에서 복사하여 다른 사람들에게 공유할 수 있고, 일반 사용자는 공유 받은 코드를 마이페이지에서 입력하여 저장하게 됩니다.

이 작업은 Front 개발 할 때 구현하도록 하고, 공지 게시글과 관리비 장부를 조회시 건물 고유 코드를 확인하는 과정은 다음과 같이 추가했습니다.

class BoardAPIView(APIView):
 	def get(self, request):
    	...
			# 현재 로그인 한 사용자 
            login_user = User.objects.get(pk = decode_access_token(auth[1]))
            # 글을 작성한 사용자
            write_user = User.objects.get(pk = board.values('user')[0].get('user'))
            
            # 현재 사용자와 글을 작성한 사용자의 건물 고유 코드 비교
            if login_user.building_code != write_user.building_code:
                return Response(status=status.HTTP_401_UNAUTHORIZED)
              	...

현재 로그인 한 사용자를 조회하고 글을 작성한 사용자의 정보를 조회 후, 이들의 건물 고유 코드가 같지 않으면 인증되지 않는 사용자로 HTTP 401을 반환하도록 구성했습니다.

구현을 중점으로 작성한 코드라서 조회 API의 성능이 어떻게 될 지 아직은 잘 모르겠지만, 이후에 리팩토링 과정이 필요하다면 더 나은 방법을 찾아보겠습니다.


⭐️ 카카오 로그인 기능 추가

Oauth2.0?

사용자가 애플리케이션에 모든 권한을 넘기지 않고, 사용자 대신 서비스를 이용할 수 있게 해주는 HTTP 기반의 보안 프로토콜 입니다.

사용자 입장

  • 여러 서비스들을 하나의 계정으로 관리할 수 있게되어 편해집니다.

개발자 입장

  • 민감한 사용자 정보를 다루지 않아 위험 부담이 줄어 듭니다.
  • 서비스 제공자로 부터 사용자 정보를 활용 할 수 있습니다.

자원 소유자 (Resource Owner)
: 정보의 소유권을 가진 사용자를 의미 합니다.

자원 서버 (Resource Server)
: 자원 소유자의 정보를 보관하고 있는 서버이고 카카오, 구글, 네이버 등이 있습니다.

인가 서버 (Authorization Server)
: 자원 소유자를 인증합니다. Client 에게 Access token 을 발행하는 역할 입니다.

클라이언트 (Client)
: 제 3자 어플리케이션을 의미하고, 사용자 동의하에 자원 서버에게 사용자 정보를 요청 할 수 있습니다.


카카오 로그인 과정

다음은 Oauth2.0 기반의 카카오 로그인 과정입니다. Kakao developers 문서 참고 kakao_oauth2.0 developer

간단하게 요약하자면
1. 서버에서는 GET 요청으로 URI에 인가 코드를 받는다.
2. 인가 코드로 토큰을 요청 한다.
3. 발급 받은 access_token으로 사용자 정보를 확인하고 로그인, 회원가입, 정보 조회 같은 다음 과정을 진행 합니다.


Django all-auth

Django에서는 소셜 로그인 기능 구현을 위해 all-auth라는 패키지가 있습니다.

pip install all-auth

settings.py 설정

# settings.py

INSTALLED_APPS = [

    'django.contrib.sites',
    ...
    # all-auth
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    ...
    # providers - 카카오 뿐 아니라 다른 소셜 로그인도 가능
    'allauth.socialaccount.providers.kakao',    
]

...

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',

    'allauth.account.auth_backends.AuthenticationBackend',
)

SITE_ID = 1

LOGIN_REDIRECT_URL = ''  # 로그인 한 뒤 리다이렉트 경로
ACCOUNT_LOGOUT_ON_GET = True

config/urls.py

urlpatterns = [
	...
	path('accounts/', include('allauth.urls')),
]

Kakao_Developers 사이트 접속 후, 애플리케이션 추가
플랫폼에 사이트 도메인 추가 (개발 중이기 때문에 로컬서버 등록, 이후 배포 서버로 변경 필요)
활성화 & 리다이렉트 URI 설정 Redirect URI는 GET 요청으로 인가 코드를 보냈을 때 code를 받을 수 있는 URI로서, 현재 로직에서 설정한 Redirect URI는 http://users/account/login/kakao/callback 입니다.

다음으로는 동의항목을 설정 해줍니다.

카카오 로그인을 통해 기존 사용자 DB에 저장하기 위해서는 email이 꼭 필요하다고 판단했고, 카카오비즈니스 연결을 통해 이메일만 필수동의 항목으로 연결했습니다. (카카오비즈니스 미 연결시 선택동의만 가능)

이후 python manage.py createsuperuser로 Django Admin계정을 만들어서 Admin에 접속 해줍니다.

소셜 어플리케이션을 생성 해줍니다.
Name은 아무거나 지정하면 되고, Client_id는 Kakao_Developers에서 추가한 앱의 요약 정보 - REST API KEY를 입력해주고, 카카오는 Secret Key를 입력하지 않아도 됩니다.

그리고 Site에는 views.py에 로직을 구성 후 urls.py에서 지정한 로직의 경로를 추가해줍니다.

인가코드를 받기 위한 로직은 다음과 같이 구성했습니다.

# user/views.py

import requests

def kakao_login(request):
    app_key = my_settings.KAKAO['REST_API_KEY']
    redirect_uri = my_settings.KAKAO['MAIN_DOMAIN']+"/users/account/login/kakao/callback"
    kakao_auth_api = "https://kauth.kakao.com/oauth/authorize?response_type=code"
    return redirect(
        f'{kakao_auth_api}&client_id={app_key}&redirect_uri={redirect_uri}'
    )
    
# user/urls.py
urlpatterns = [
	path('account/login/kakao/', kakao_login, name='kakao_login'),
    ...
    ]

Client_id인 REST_API_KEY는 보안상 노출이 되면 안되기 때문에 Django의 Secret Key를 분리하는 것 처럼 처리하여 app_key라는 변수에 담았습니다.

GET요청으로 kakao_login 함수를 실행 했을 때, 동의하고 계속하기를 선택하게 되면 인가 코드가 URI에 담겨서 반환되고 로그인이 진행됩니다.

이후 URI로 받은 인가코드로 access_token을 받고 access_token을 통해 사용자의 email 정보를 추출하기 위해 다음과 같이 코드를 작성했습니다.

# user/views.py

def kakao_callback(request):
    # 카카오 login redirection
    data = {
        "grant_type":"authorization_code",
        "client_id":my_settings.KAKAO['REST_API_KEY'],
        "redirect_uri":my_settings.KAKAO['MAIN_DOMAIN']+"/users/account/login/kakao/callback",
        "code":request.GET['code']
    }

    # token 요청
    kakao_token_api = "https://kauth.kakao.com/oauth/token"
    access_token = requests.post(kakao_token_api, data=data).json()['access_token']
    

    # 토큰 기반 사용자 정보 반환
    kakao_user_api = "https://kapi.kakao.com/v2/user/me"
    header = {"Authorization":f"Bearer {access_token}"}
    user_info = requests.get(kakao_user_api, headers=header).json()

    kakao_id = user_info["id"]
    kakao_email = user_info["kakao_account"]["email"]
    
    # 카카오 이메일로 사용자 모델 필터
    user = CustomAbstractBaseUser.objects.filter(email=kakao_email).first()
    # 만약 해당 사용자가 없으면
    if not user:
        new_user = CustomAbstractBaseUser.objects.create(
            email=kakao_email,
            kakao_check=kakao_id
            )
        # 해당 사용자 로그인 
        return token_create(new_user)
    
    # eamil은 존재, kakao 사용자가 아니면 일반 회원가입 유도
    if not user.kakao_check:
        return KakaoSigninException()
    
    # 전부 아닐 경우, 이미 이메일이 있고, 카카오 사용자이므로 로그인
    else:
        access_token = create_access_token(user.id)
        access_exp = access_token_exp(access_token)
        refresh_token = create_refresh_token(user.id)
        
        res_data={
            'access_token' : str(access_token),
            'access_exp' : str(access_exp),
            'refresh_token' : str(refresh_token)
        }
        
        response = JsonResponse(
            data=res_data,
            status=status.HTTP_201_CREATED, 
            content_type='application/json', 
            )
        response.set_cookie(key='refreshToken', value=str(refresh_token), httponly=True)         #리프레쉬 토큰 쿠키에 저장
        return response
    
    
# user/urls.py
urlpatterns = [
	path('account/login/kakao/callback/', kakao_login, name='kakao_callback'),
    ...
    ]

카카오 로그인을 진행하면 자동으로 토큰을 받고, 정보를 추출하고, 회원가입, 로그인을 모두 처리하게 됩니다.

현재 Response로 json 결과 값을 받으려고 했으나 반복되는 오류로 JsonResponse를 통해 로그인 후 생성된 새로운 토큰들이 반환되게 작성했습니다.

카카오 로그인으로는 단순히 email 사용자 인증의 역할만 하면 되기 때문에, 사용자 이메일 정보를 추출하여 기존 회원가입 or 로그인시 쓰던 토큰 생성 로직을 활용하여 새로운 토큰을 발급 받도록 했습니다.


시행착오

기존 User모델에 카카오 사용자 정보를 저장하게 되면 email 데이터 중복 문제로 인해 카카오 로그인 사용자가 서비스를 사용할 수 없는 상황이 고려되었습니다.

만약 User 모델에 관리자 혹은 일반 사용자의 email이 카카오 사용자의 이메일과 중복되는 상황이 있을 경우, email 데이터 중복성이 발생하게 됩니다.

처음 이 문제를 해결하기 위해서 기존 User모델을 상속 받아 Kakao_User 모델을 새롭게 생성하는게 어떨까 했지만, 그렇게 될 경우 기존 사용자 모델이 맺고 있는 관계 모두 Kakao_User 모델도 맺어 주어야하며 전체 로직 또한 추가 로직이 필요하다고 판단 되어 다른 방법을 생각했습니다.

그래서 최종적으로 문제를 해결하기 위해 기존 User모델에서 kakao 사용자인지 확인하는 kakao_check 필드를 추가했습니다.

결론적으로 카카오 로그인을 통해 추출한 email 정보로 User 모델에서 필터링하여 중복되는 이메일이 있는지 확인하고,

없을 경우 사용자 모델에 kakao_check 필드에 kakao 사용자의 고유한 아이디 값을 함께 추가하여 카카오 사용자임을 구분하면서 기존 사용자 모델을 활용 할 수 있었습니다.

그래서 기존 모델에서 email 중복 여부와 기존 카카오 로그인 사용자인지 확인하여 로그인 하는 것, 카카오 로그인으로 아예 새롭게 회원가입 하는 경우 모두를 구분할 수 있도록 조건을 부여할 수 있었습니다.


회고

Front 개발을 Django에서 작성하게 될 경우 JSONResponse로 구성한 return 값을 HttpResponse로 변경하여 카카오 로그인 결과 화면은 템플릿과 연동되도록 변경 할 계획입니다.

기존 사용자 모델에 카카오 사용자 정보를 저장하기 위한 시행착오는 DB와 로직 구성에 대해 좀 더 깊이 생각할 수 있는 좋은 기회였습니다.

역시 사용자 모델 관련 로직은 프로젝트 초기에 마무리 하는 것이 좋다는 것을 깨달았습니다. 아직 더미 데이터를 넣은 것이 아니라 다행이지만, 사용자 모델 수정 중 migration 문제가 발생하여 테스트하던 데이터를 모두 초기화 했습니다.

Oauth 구현이 첫 시도라 미흡한 부분이 있겠지만 앞으로 더 고민하며 공부해야겠습니다!
수정사항, 질문, 피드백 등 언제나 환영입니다 :D

profile
발전하는 꿈나무 개발자 / 취준생

0개의 댓글