소셜 회원가입/로그인 기능을 내 서비스에 추가하기

정한빈·2021년 4월 28일
11
post-thumbnail

졸업한 선배와 프로젝트를 진행하면서 카카오 소셜 로그인을 구현하게 되었다.
그동안 소셜 로그인을 구현할 때는 django-allauth와 같은 외부 라이브러리를 사용해서 로직에 대한 제대로 된 이해가 부족했었다. 외부 라이브러리에 의존하지 않고 직접 구현한 과정을 쓰려고 한다.


🤔 다른 서비스는 어떻게 구현했지?

사용자들이 자주 찾는 서비스를 보고 어떻게 구현해야 할 지 갈피를 잡아보자. 두 가지 사례를 준비했다.

아이디어스(idus)

아이디어스는 계정의 가입 여부를 전화번호로 구분한다.
또한 처음에 처음 가입한 소셜 계정으로만 로그인 할 수 있도록 했다.
만약 네이버 계정으로 로그인 했다면, 카카오톡, 페이스북, 트위터로 가입할 수 없다.
여기를 참고했다.

원티드, 프로그래머스

원티드, 프로그래머스는 계정의 가입 여부를 이메일 주소로 구분한다.
Facebook, Github 소셜 로그인을 지원하면서, 서비스 자체 회원가입, 로그인도 지원하고 있다.
하나의 계정에 Facebook, Github 소셜 계정을 연동할 수도 있다.


나는 위 두가지 방법중에 원티드, 프로그래머스 방식을 택했다.
하나의 계정으로 여러 소셜 계정을 연동해 사용하는 것이 더 확장성이 높고 UX 면에서도 편리하기 때문이다.

갈피를 잡았으니 어떻게 개발해야 할 지 생각해 보자.

👨 User 모델링을 어떻게 할까?

우리의 서비스는 서비스 자체 로그인, 소셜 로그인을 모두 지원해야 한다. 하지만, 서비스 자체 로그인은 비밀번호를 필요로 하지만 소셜 로그인은 비밀번호를 필요로 하지 않는다.

이런 구조에 대해서 어쩌면 좋을 지 검색을 해봤더니 스택 오버플로우 에서 아주 좋은 답변을 찾았다.

When a user first registers with your site, and logs in via Facebook, for example, you will have to create a row in the users table. However, there will be no password, activation_key and resetpassword_key involved. So you may want to move those fields to a separate table, such that your users table only contains the core user data, and no data that is only relevant for a single login mechanism (username/password).

User 테이블에는 유저 정보 (email, name, is_active, etc.)만 넣고, 로그인 매커니즘에 관련된 데이터(password, etc.)는 별도의 테이블을 만들어 줘야 한다는 내용이다.

코드로 구현해보면

import peewee as pw


class User(pw.Model):  # 유저의 정보만 포함
    email = pw.CharField(max_length=256, unique=True)
    name = pw.CharField(max_length=256)
    is_active = pw.BooleanField(default=False)


class LocalAuthentication(pw.Model):  # 서비스 자체 로그인 관련 데이터
    user = pw.ForeignKeyField(User, backref="local_authentication_info")
    password = pw.CharField(max_length=256)


class SocialAuthentication(pw.Model):  # 소셜 로그인 관련 데이터
    user = pw.ForeignKeyField(User, backref="provider_authentication_info")
    platform = pw.CharField(max_length=30)
    sns_service_id = pw.CharField(max_length=100, unique=True)

위와 같이 구현 할 수 있다.

🧩 API 구조를 어떻게 짤까?

기능이 크게 로그인, 회원가입, 계정 연동이 존재하니 sns/kakao/login, sns/kakao/register, sns/kakao/connect 이렇게 따로 만들면 될까?
만약 이와 같이 만들게 된다면 회원가입 되지 않은 유저가 로그인 할때, 연동 되지 않은 유저가 로그인 할 때 등의 상황을 프론트에서 처리하기 번거로울 것이다. 좀 더 좋은 방법을 찾아보자

검색을 해봐도 좋지만 원티드의 소셜 로그인 기능을 보고 따라해 보고 싶어졌다.
위 사진은 원티드 로그인 에서 개발자 도구를 키고 Facebook으로 계속하기 버튼을 누른 후의 액션을 캡쳐해 온 것이다.

원티드는 소셜 계정 연결, 로그인을 api/v4/callback/auth/facebook 에서 모두 처리한다.
이 방식은 여러 이점이 있다.

  • 사용자에게 oauth_token을 노출시키지 않기 때문에 보안면에서 이점이 있다.
  • 하나의 API에서 모든 요청을 처리하기 때문에 불필요한 과정이 생략된다.
  • 여러 과정(회원가입, 연동, 로그인)이 생략되어 처리 속도가 상승한다.

우리도 회원 가입, 로그인, 소셜 계정 연결을 모두 sns/kakao/callback 에서 처리해보자.

@router.get(
    "/kakao/callback", response_model=GetUserSchema, status_code=status.HTTP_200_OK
)
def callback_kakao(response: Response, code: str):
    """
    KAKAO 로그인의 Redirect URL 입니다.
    받은 code로 oauth_token을 발급 받고,
    유저가 회원가입 되어 있지 않다면 회원가입을 하고,
    계정이 연동되어 있지 않다면 계정 연동을 하고,
    로그인을 해 jwt 토큰과 유저 정보를 반환해 줍니다.
    """
    oauth_token = KAKAOOAuthController().get_oauth_token(code)
    user, token = KAKAOOAuthController().callback_process(oauth_token)
    response.headers["Authorization"] = "jwt " + token
    return user.__data__

🥨 로직을 어떻게 짤까?

우리는 로그인, 계정 연동, 회원가입을 하나의 API에서 처리해야 한다. 각각의 분기점을 나눠서 생각해 보자.

이메일 존재 O이메일 존재 X
계정 연동 O3. 로그인/
계정 연동 X2. 계정 연동1. 회원가입

위 번호의 순서대로 체크해 나가자.

  • 우리의 서비스는 프론트의 요청과 함께 건너온 codeoauth_token을 발급받아 리소스 서버로 사용자 정보를 요청한다.
  • 받아온 사용자 정보(email)가 회원가입 되어 있는지 확인한다. (존재 하지 않으면 1. 회원가입 과정을 거친다)
  • 계정이 연동되어 있는지 확인한다. (존재하지 않으면 2. 계정 연동 과정을 거친다)
  • 3. 로그인 과정을 거친다.

위의 과정을 코드로 옮겨보면 아래와 같다.

def callback_process(self, oauth_token: str):
	user_info = self._get_kakao_user_info(oauth_token)
	user_email = user_info["kakao_account"]["email"]
	sns_service_id = user_info["id"]

	exists_user = User.filter(User.email == user_email).first()
	if not exists_user:  # 1. 이메일이 존재하지 않으면 회원가입
    	exists_user = self.create_social_user(user_email)

	exists_social_auth = SocialAuth.filter(
		SocialAuth.platform == self.platform,
		SocialAuth.sns_service_id == sns_service_id,
		SocialAuth.user == exists_user.id,
	).first()
	if not exists_social_auth:  # 2. 소셜 계정 연동이 안 되어 있으면 계정 연동
		self.connect_social_login(oauth_token, exists_user)

	return self.socical_login(oauth_token)  # 3. 로그인 (토큰 발급)

나머지 작업 (라우터 설정, 인증 미들웨어 등) 에 대한 내용은 이 링크에서 확인 할 수 있다.

profile
항상 고민하며 개발합니다

0개의 댓글