[React] 소셜로그인 구현 방법 정리

공이·2023년 11월 24일
3

React

목록 보기
1/4
post-thumbnail

최근 소셜로그인 기능을 백엔드(Spring boot 이용)와 협업하여 구현해야했는데,
조사하면서 사람들마다 다양한 방식으로 구현하고 있어서 혼란스러웠다.
그래서 알아낸 구현방법들을 정리해보고자 한다.
참고로 구글 소셜로그인을 기준으로 작성된 글인데, 다른 플랫폼의 소셜로그인 구현 방식도 유사하다고 함!

✨FE 라이브러리를 이용한 토큰 발급 방식

gapi (Google API Client Library)와 react-google-login나 @react-oauth/google과 같은 자바스크립트 혹은 리액트 라이브러리를 사용하는 방식이다.
이 방식을 사용하면 클라이언트 사이드에서 사용자 인증을 직접 처리하고 토큰을 관리할 수 있다.
구글 클라우드 플랫폼에서 OAuth 클라이언트 ID와 클라이언트 secret을 발급받은 뒤, 이 정보를 코드에 해당 라이브러리에서 요구하는 방식대로 입력하면 소셜로그인을 구현할 수 있다.
이 방식은 리액트에서 구글로 로그인 요청을 하면 구글에서 유저데이터와 토큰을 리액트에 다이렉트로 보내주는 방식이다. 서버 사이드 코드 없이 프론트엔드에서 모든 인증 흐름을 처리할 수 있지만, 이 경우 보안에 더 많은 주의가 필요하다고 한다.이후 위의 그림과 같이 유저데이터를 백엔드에 보내 유저 정보를 관리할 수 있다.

✨FE와 BE 간 핑퐁을 통한 토큰 발급 방식

이 방식은 구글클라우드플랫폼에서 OAuth 클라이언트 ID를 만들 때 리디렉션 URI를 클라이언트 URI로 설정하느냐, 서버 URI로 설정하느냐에 따라 다시 두 가지로 나눌 수 있다. 참고로 response_type을 code로 설정했을 경우를 전제하였따.
여기서 설정할 수 있다.

1) 리디렉션 URI를 클라이언트 URI로 설정할 경우

리디렉션 URI를 클라이언트 URI(ex. localhost:3000/example)로 설정할 경우다.

즉 정리해보자면,

1. 클라이언트가 로그인 버튼을 누르면 구글 소셜 로그인 링크로 이동한다.
2. 클라이언트가 로그인 할 계정을 클릭한다.
3. 설정한 리디렉션 클라이언트 URI로 구글이 인가코드(code)를 쿼리파라미터로 설정하여 보낸다. 이 인가코드는 재사용이 불가능하다.
4. FE는 쿼리파라미터에서 인가코드를 추출하여 BE에 보낸다.
5. 백엔드는 획득한 인가코드로 구글 측에 액세스토큰을 요청한다.
6. 백엔드는 획득한 액세스토큰으로 구글 측에 사용자 정보를 요청한다.
7. 백엔드는 획득한 유저 정보를 데이터베이스에 저장하고 JWT 토큰을 발급한다.
8. 백엔드는 발급한 JWT 토큰과 함께 클라이언트 URI로 리다이렉트를 한다.
9. 클라이언트는 백엔드로부터 JWT 토큰을 획득한다.

코드 예시(4단계~9단계)(출처)
인가코드가 쿼리파라미터를 통해 클라이언트 측으로 전달되어 이를 추출한 뒤, 다시 BE로 토큰을 요청하는 코드다.

import axios from "axios";
import { useEffect, useContext } from "react";
import { Navigate, useNavigate } from "react-router-dom";
import { IsLoginContext } from "../../context/AuthContext";

export const GoogleRedirect = () => {
  const navigate = useNavigate();
  const href = window.location.href;
  let params = new URL(document.location).searchParams;
  let GOOGLE_CODE = params.get("code");
  const { setIsLogin } = useContext(IsLoginContext);

  useEffect(() => {
    fetch(`http://13.124.131.171:8080/api/v1/login/google?code=${GOOGLE_CODE}`,{
        method: "POST",
        headers: {
          "Content-Type": "application/json;",
        },
      })
      .then(res =>{
          return res.json();
        })
      .then(data => {
        console.log(data);
        localStorage.setItem('token', data.accessToken);
        localStorage.setItem('nickname', data.nickname);
        setIsLogin(true);
        navigate("/");
      })
      .catch(error =>{
        console.log('Error:', error);
      })

  }, []);

  return(
    <>
    </>
  );
};

2) 리디렉션 URI를 서버 URI로 설정할 경우

리디렉션 URI를 서버 URI(ex. localhost:8080/example)로 설정할 경우다.
위의 그림들을 정리하자면 소셜로그인을 위해 아래와 같은 단계를 거친다.

1. 클라이언트가 로그인 버튼을 누르면 구글 소셜 로그인 링크로 이동한다.
2. 클라이언트가 로그인 할 계정을 클릭한다.
3. 설정한 리디렉션 서버 URI로 구글이 인가코드(code)를 쿼리파라미터로 설정하여 보낸다. 이 인가코드는 재사용이 불가능하다.
4. 백엔드는 획득한 인가코드로 다시 구글에 계정에 대한 액세스토큰을 요청한다.
5. 백엔드는 획득한 액세스토큰으로 구글 측에 사용자 정보를 요청한다.
6. 백엔드는 획득한 유저 정보를 데이터베이스에 저장하고 JWT 토큰을 발급한다.
7. 백엔드는 발급한 JWT 토큰과 함께 클라이언트 URI로 리다이렉트를 한다.
8. 클라이언트는 백엔드로부터 JWT 토큰을 획득한다.

내가 이번에 진행한 프로젝트는 리다이렉트 URI가 서버 URI로 설정되어 있어 이 방법으로 구현했다. 다음은 내가 작성한 코드다.

로그인 링크로 이동(1단계)

import React, { useState } from 'react';
import { AiOutlineClose } from 'react-icons/ai';
import styles from './LoginModal.module.scss';
import GoogleLogo from '../../assets/images/GoogleLogo.png';

const LoginModal = ({ onModalClose }) => {
  const handleClose = () => {
    onModalClose(false);
  };
  const handleLogin = () => {
    window.location.href = 'http://localhost:8080/oauth2/authorization/google';
  };

  return (
    <div className={styles.modalBackgound}>
      <div className={styles.modalContainer}>
        <div className={styles.modalHeader}>
          <div className={styles.item}>로그인</div>
          <AiOutlineClose className={styles.button} onClick={handleClose} />
        </div>
        <div className={styles.modalButtonContainer}>
          <button className={styles.button} onClick={handleLogin}>
            <img src={GoogleLogo} alt="구글" />
            Sign in using Google
          </button>
        </div>
      </div>
    </div>
  );
};

export default LoginModal;

이 경우는 백엔드 서버의 특정 엔드포인트로 리디렉션하고, 그 서버가 사용자를 OAuth 서비스 제공자의 인증 페이지로 리디렉션되도록 구현되어있는 경우다.
그런데 클라이언트 측에서 직접 OAuth 클라이언트 ID와 window.location.href를 아래와 같이 설정하여 이동할 수도 있다. 즉 클라이언트에서 직접 OAuth 서비스 제공자의 인증 페이지로 리디렉션할 수 있다.

window.location.href =`https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${GOOGLE_REDIRECT_URI}&response_type=code&scope=${GOOGLE_SCOPE}`

jwt 토큰 획득(8단계)
해당 리다이렉트 컴포넌트의 route path는 /login이고, 로그인이 성공되면 메인화면(/)으로 이동한다.
이 경우에는 토큰을 쿼리파라미터로 받아왔고, 이를 로컬스토리지에 저장했다.
그러나 이 방식이 보안상 안전한 방식은 아니다.

import React, { useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useSetRecoilState } from 'recoil';
import { loginState } from '../store/loginStore';

const LoginRedirect = () => {
  const setIsLoggedin = useSetRecoilState(loginState);
  const navigate = useNavigate();

  useEffect(() => {
    // URL에서 쿼리 파라미터 추출
    const queryParams = new URLSearchParams(window.location.search);
    const jwtToken = queryParams.get('jwt');

    if (jwtToken) {
      // 토큰을 로컬 스토리지에 저장
      localStorage.setItem('ziio-token', jwtToken);
      setIsLoggedin(true);
      navigate('/'); // 홈으로 리다이렉트
    } else {
      // 토큰이 없으면 로그인 실패 처리
      console.error('로그인 실패');
    }
  }, []);

  return <></>;
};

export default LoginRedirect;

이처럼 프론트엔드는 처음 로그인요청과 마지막 토큰을 받아오는 로직만 짜면 된다.

➕response_type 설정: token vs code

window.location.href ="https://accounts.google.com/o/oauth2/auth?" +
  "client_id={클라이언트 ID}&"+
  "redirect_uri={리디렉션 URI}&"+
  "response_type=token&"+
  "scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile";

구글 로그인 페이지로 이동할 때 response_type을 token으로 지정하는 방법과 code로 지정하는 방법이 있다.
이 파라미터는 OAuth 서버에게 클라이언트가 인증 과정을 통해 어떤 종류의 응답을 기대하는지를 알려준다.

response_type=token

response_type=token은 Implicit Grant 플로우를 사용하는 것을 의미한다고 한다.
사용자가 인증을 성공하면, OAuth 서버는 액세스 토큰을 직접 반환한다. 이 토큰은 URL의 쿼리파라미터 부분에 포함되어 리디렉션 URI로 전달된다.
여기서 리디렉션 URI를 클라이언트 측으로 설정하면, FE는 액세스토큰을 받아 BE에 전송하고, BE는 이 액세스 토큰으로 OAuth 서버에 유저 정보를 요청한다.
반면, 리디렉션 URI를 서버 측으로 설정하면 BE로 액세스토큰이 바로 전송될 것이다.
response_type=token은 간단하고 클라이언트 측에서 처리하기 쉽지만 액세스 토큰이 브라우저를 통해 직접 전송되기 때문에 보안상의 위험이 더 크다. 특히 XSS(크로스 사이트 스크립팅) 공격에 취약할 수 있다.

response_type=code

response_type=code는 Authorization Code Grant 플로우를 사용하는 것을 의미한다고 한다.
사용자가 인증을 성공하면, OAuth 서버는 인증 코드만을 클라이언트혹은 서버에게 반환한다. 이 코드는 서버 측에서 액세스 토큰과 교환되어야 한다.
클라이언트는 받은 인증 코드를 백엔드 서버로 전송하고, 서버는 이 코드를 사용하여 OAuth 서버로부터 액세스 토큰을 요청한다.
액세스 토큰이 클라이언트 측에 직접 노출되지 않으므로, 보안성이 더 강화된다. 특히, client secret을 사용하여 토큰을 요청하므로, 보안성이 더욱 향상된다.


참고글

0개의 댓글