Ssul's Blog

React Native 구글 소셜로그인 구현 본문

dev/기능구현

React Native 구글 소셜로그인 구현

Ssul 2023. 2. 12. 17:30
배경: React(프론트)-Django(백앤드)로 구성된 웹앱 서비스를 리액트 네이티브로 ios/android앱 구현하기 

 

들어가기. 구글로그인 프로세스

1. 구글 버튼을 눌러 구글서버로 소셜로그인/회원가입 요청(https://accounts.google.com/o/oauth2/v2/auth)

2. 구글 로그인창 > 로그인 > 성공하면, 구글 토큰리다이렉트 주소 호출(https://oauth2.googleapis.com/token)

3. 그 토큰으로 구글 정보 요청(https://www.googleapis.com/oauth2/v1/userinfo) 해서 받고,

4. 쿠키나 token 백엔드에서 커스터마이징 셋팅 후, 프론트로 전달

 

0. 구성별(React, RN) 소셜로그인 프로세스

0-1. (웹앱) React-Django 구성(지금 서비스)

1. 프론트에서 소셜로그인 버튼 클릭 > 백앤드 서버 api호출

2. 백앤드에서 구글 서버로 로그인 요청-https://accounts.google.com/o/oauth2/v2/auth

3. 구글 로그인창 > 로그인완료 > 로그인 요청 함께 준 리다이렉트 (백앤드)주소 호출

def request_google_login(request):
    state = get_random_string(32, allowed_chars=string.ascii_letters + string.digits)
    query = urlencode(
        {
            "client_id": GOOGLE_CLIENT_ID,
            "redirect_uri": REDIRECT_URI,
            "response_type": "code",
            "scope": "profile email",
            "access_type": "offline",
            "state": state,
        }
    )
    response = django_redirect(f"{GOOGLE_OAUTH_URL}?{query}")
    cb = re.search("cb=(.+)", request.get_full_path())
    cb = APP_URL + (cb and cb[1])
    response.set_cookie("cb", cb, httponly=True)
    response.set_cookie("state", state, httponly=True)
    return response

4. 백앤드에서 구글 서버로 토큰요청-https://oauth2.googleapis.com/token > 토큰받고 > 토큰으로 유저정보 요청 > 유저정보 받음

def authorize_with_google(request):
    cb = request.COOKIES.get("cb")
    if not cb or cb == "None":
        cb = "/"

    csrf_token = request.COOKIES.get("state")
    if csrf_token != request.GET.get("state"):
        # error in login
        return build_unauthorized_response(cb)

    token_res = requests.post(
        GOOGLE_TOKEN_URL,
        {
            "code": request.GET.get("code"),
            "client_id": GOOGLE_CLIENT_ID,
            "client_secret": GOOGLE_CLIENT_SECRET,
            "redirect_uri": REDIRECT_URI,
            "grant_type": "authorization_code",
        },
    )
    if not token_res.ok:
        return redirect(f"{APP_URL}/error?type=auth&status=424&next={cb}")

    token_body = token_res.json()

    user_res = requests.get(
        GOOGLE_GET_PROFILE_URL,
        {"alt": "json", "access_token": token_body.get("access_token")},
    )
    if not user_res.ok:
        return redirect(f"{APP_URL}/error?type=auth&status=424&next={cb}")

    account = user_res.json()

    response = build_authorized_response(cb)
    response.set_cookie("provider", "google", httponly=True, max_age=60 * 60 * 1)
    response.set_cookie(
        "email", account.get("email"), httponly=True, max_age=60 * 60 * 1
    )
    response.set_cookie(
        "service_id", account.get("id"), httponly=True, max_age=60 * 60 * 1
    )
    return response

5. 받은 유저정보를 프론트에 Respone

6. 프론트는 받은 정보로 백엔드 서버에 로그인 api호출

7. 가입한 계정이면 로그인 > 메인화면

8. 가입하지 않은 계정이면 > 회원가입 마져 진행

 

0-2. (앱) React native-Django 구성

기존 리액트와 동일한 과정으로 풀어내고 싶었지만, 아직 RN에 대한 지식이 많이 부족해서....외부 링크를 창으로 띄우는 것이 쉽지 않았다. 그래서 1-5과정을 RN에서 처리하고, 6-8을 서비스 서버와 소통하는 형태로 진행

1-5과정. RN에서 소셜로그인 호출 > 로그인 완료 > 토큰받아서 > 토큰으로 User정보 획득

6. 받은 정보로 백엔드 서버에 로그인 > 백앤드서버에서 accessToken 생성 후, Response

@api_view(["post"])
@permission_classes([])
def authorize(request):
    if request.META.get("HTTP_LOE_SECRET_KEY") != os.getenv("HTTP_SECRET_KEY"):
        raise PermissionDenied(_("there is no permission"))
    username = hashlib.md5(request.data.get("email").encode()).hexdigest()
    user: Union[User, None] = User.objects.filter(username=username).first()
    if not user:
        raise NotFound(_("not found"))
    if user.is_deleted:
        user.restore()
    login(request, user)
    return Response({"msg": "ok",
                     "accessToken": request.data['accessToken']})

7. 받은 accessToken을 EncrypteStorage에 보관

8. 앱은 매번 EncrypteStorage의 accessToken 유무를 체크하여 로그인 여부 판단

 

 


그럼 소셜 로그인을 구현해보자!

 

1. 구글 로그인 api 계정발급

1. https://console.cloud.google.com/

2. API 및 서비스 > 사용자 인증정보 클릭

3. 사용자 인증정보만들기 > OAuth 클라이언트 ID 만들기

4. 애플리케이션 유형선택:

*우선 웹클라이언트, IOS 두개만 만들어도 되는것 같다. 난 android도 만들었지만...클라이언트ID가 같아서 따로 사용안했다는

**구글 OAuth생성 관련은 잘 정리해주신분들이 많으니 그거 참고해주세요 :)

1-1. ios(아이폰앱)관련 추가 작업

- OAuth의 iOS URL 스키마를

- xCode -> TARGETS(프로젝트밑에) > info > URL Types +버튼 누르고, > URL Schemes에 입력

1-2. 구글 콘솔에서 PLIST를 다운받아서 복붙(우선 나는 1-1만해도 해결되었다. )

 

2. google-signin 패키지 설치

npm i @react-native-google-signin/google-signin

 

3. 코딩

import {
  GoogleSignin,
  statusCodes,
} from '@react-native-google-signin/google-signin';


GoogleSignin.configure({
  scopes: ['https://www.googleapis.com/auth/drive.readonly'],
  webClientId:
    '여기에 구글 클라이언트ID', 
  offlineAccess: true, 
  hostedDomain: '',
  forceCodeForRefreshToken: true,
  accountName: '',
  iosClientId:
    '여기에 ios URL 스키마..(중요)여기선 똑바로쓰기...com이 제일 뒤로',
  googleServicePlistPath: ''
  openIdRealm: '',
  profileImageSize: 120,
});



function IntroPage({navigation}: IntroPageScreenProps) {
  const dispatch = useAppDispatch();
  const toSignIn = useCallback(() => {
    navigation.navigate('SignIn');
  }, [navigation]);
  const toSignUp = useCallback(() => {
    navigation.navigate('SignUp');
  }, [navigation]);
  const signInGoogle = async () => {
    console.log('google');
    try {
   	  //이 두줄로 소셜로그인 완료하고, 구글의 user정보 획득
      await GoogleSignin.hasPlayServices();
      const userInfo = await GoogleSignin.signIn();
      console.log('userInfo', userInfo);

      // /auth/authorize로 로그인 시도해서 성공하면 가입한 아이디니 바로 메인, 아니면 추가입력으로
      const API_URL =
        Platform.OS === 'ios'
          ? 'http://localhost:8000'
          : 'http://10.0.2.2:8000';
      console.log(API_URL);
      console.log('http:', Config.HTTP_SECRET_KEY);

      const response = await axios.post(
        `${API_URL}/auth/authorize`,
        {email: `google_${userInfo.user.id}@xxxxxxx.com`},
        // TODO: HTTP_SECRET_KEY설정 필요
        {headers: {'SECRET-KEY': Config.HTTP_SECRET_KEY || ''}},
      );
      console.log(response.data.accessToken);
      dispatch(
        //요 친구들이 다 action.payload에 저장
        userSlice.actions.setUser({
          email: response.data.email,
          accessToken: response.data.accessToken,
        }),
      );
      // 받은 accessToken을 스토리지에 저장
      await EncryptedStorage.setItem('accessToken', response.data.accessToken);
      
    } catch (error) {
      if (error.code === statusCodes.SIGN_IN_CANCELLED) {
        // user cancelled the login flow
      } else if (error.code === statusCodes.IN_PROGRESS) {
        // operation (e.g. sign in) is in progress already
      } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
        // play services not available or outdated
      } else {
        // some other error happened
      }
    }
  };

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
        backgroundColor: 'white',
        alignItems: 'center',
        justifyContent: 'center',
      }}>
      <View>
        <View
          style={{
            justifyContent: 'center',
            alignItems: 'center',
          }}>
          <Logo />
        </View>
        <View
          style={{
            justifyContent: 'center',
            flexDirection: 'row',
            margin: 5,
          }}>
          <Text>습관만들고 일기쓰면서,</Text>
          <Text
            style={{
              color: '#6562F5',
              fontWeight: 'bold',
            }}>
            {' '}
            용돈벌어요!
          </Text>
        </View>
        <View
          style={{
            margin: 30,
          }}>
          <LogoImgage />
        </View>
        <View
          style={{
            justifyContent: 'center',
            flexDirection: 'row',
            margin: 5,
          }}>
          <Pressable
            style={{
              borderRadius: 35,
              backgroundColor: '#FBE74D',
              alignItems: 'center',
              justifyContent: 'center',
              padding: 15,
              margin: 8,
            }}
            onPress={() => {
              console.log('click');
            }}>
            <KakaoIcon />
          </Pressable>
          <Pressable
            style={{
              borderRadius: 35,
              backgroundColor: '#f3f3f3',
              padding: 15,
              margin: 8,
            }}
            onPress={signInGoogle}>
            <GoogleIcon />
          </Pressable>
        </View>
        <View
          style={{
            justifyContent: 'center',
            alignItems: 'center',
            margin: 5,
          }}>
          <Text>또는</Text>
        </View>
        <Pressable
          style={{
            justifyContent: 'center',
            alignItems: 'center',
            backgroundColor: '#6562F5',
            borderRadius: 20,
            margin: 10,
            padding: 10,
          }}
          onPress={toSignIn}>
          <Text
            style={{
              color: 'white',
              fontWeight: 'bold',
            }}>
            이메일로 로그인
          </Text>
        </Pressable>
        <View
          style={{
            justifyContent: 'center',
            flexDirection: 'row',
          }}>
          <Text>OO서비스가 처음이라면, </Text>
          <Pressable
            style={{
              justifyContent: 'center',
              alignItems: 'center',
            }}
            onPress={toSignUp}>
            <Text
              style={{
                color: '#6562F5',
                fontWeight: 'bold',
              }}>
              회원가입
            </Text>
          </Pressable>
        </View>
      </View>
    </View>
  );
}

export default IntroPage;

 

이렇게 하면, 기존 이메일 입력로그인과 동일하게, 소셜로그인 완료 후, accessToken이 리듀서와 로컬스토리지에 생성. 그리고 이를 통하여 isLoggedIn이 true가 되어 로그인 된 화면으로 이동하게 된다