프로젝트(개인)

Spring boot와 JPA , react 를 이용한 Google Oauth2 구현

그zi운아이 2024. 1. 30. 11:24

소개

Google OAuth는 사용자가 자신의 Google 계정을 사용하여 다른 애플리케이션에 로그인할 수 있게 해주는 인증 시스템입니다. 사용자가 애플리케이션에서 Google 로그인을 하면, Google은 사용자의 신원을 확인하고 애플리케이션에 필요한 정보를 제공합니다. 이 과정은 아래와 같이 진행됩니다.

 

  1. 사용자(프론트엔드):
    • 사용자는 웹 브라우저 상의 프론트엔드에서 Google 로그인 버튼을 클릭합니다.
  2. Google 인증 페이지(구글 서비스):
    • 프론트엔드는 사용자를 Google 로그인 페이지로 리디렉션합니다.
    • 사용자는 자신의 Google 계정으로 로그인하고 애플리케이션에 특정 권한을 부여합니다.
  3. 인증 코드(구글 서비스):
    • Google 로그인 성공 후 Google은 사용자를 프론트엔드로 리디렉션하며 인증 코드를 URL에 포함시켜 전달합니다.
  4. 프론트엔드(사용자의 브라우저):
    • 프론트엔드 애플리케이션은 이 인증 코드를 캡처하고 백엔드 서버로 전송합니다.
  5. 백엔드(서버):
    • 백엔드 서버는 인증 코드와 자체 클라이언트 ID 및 시크릿을 사용하여 Google의 토큰 교환 엔드포인트로 요청을 보냅니다.
    • Google은 백엔드로부터의 요청을 검증한 후 정보를 제공합니다.

Google API 프로젝트 설정

  1. Google Cloud Console:
    • Google API를 사용하기 위해서는 먼저 Google Cloud Console에서 프로젝트를 생성해야 합니다.
    • 프로젝트를 생성하고 OAuth 동의 화면을 구성한 다음, 필요한 API를 활성화합니다.

 

2. API 사용자 인증 정보

  • OAuth 2.0 클라이언트 ID와 클라이언트 시크릿을 생성합니다. 이 정보들은 백엔드 서버가 Google에 토큰을 요청할 때 사용됩니다.

 

3. OAuth 동의 화면 구성:

  • 대시보드에서 'OAuth 동의 화면'을 선택합니다.
  • 애플리케이션 이름, 이메일 주소, 도메인 등 사용자 동의 화면에 표시할 정보를 입력합니다.
  • 사용자가 로그인할 때 요청할 정보의 범위(예: 프로필, 이메일, 오픈ID 등)를 설정합니다.

 

 

4. 리디렉션 URI:

  • 구글 인증 후 사용자가 리디렉션될 프론트엔드 애플리케이션의 URI를 등록합니다.

백엔드(springboot 3.2.1, JPA)

application.yml 설정:

  • spring.security.oauth2.client.registration.google: 이 섹션은 Google OAuth2 인증을 위한 설정입니다.
    • client-id: Google OAuth2 클라이언트 ID입니다. Google Cloud Console에서 생성된 OAuth 2.0 클라이언트 ID를 여기에 입력합니다.
    • client-secret: Google OAuth2 클라이언트 시크릿입니다. Google Cloud Console에서 생성된 클라이언트 시크릿을 여기에 입력합니다.
    • scope: OAuth2 스코프를 지정합니다. 일반적으로 profile, email, openid 등이 사용됩니다.

 

Gradle 설정:

  • Gradle 빌드 파일에는 다음과 같이 의존성을 추가해야 합니다

 

Entity 및 DTO 생성

GoogleInfoDto : 구글에서 받은 정보를 전달하는 객체로 UserRegisterDto로 변환이어 전달됩니다

  • email: 사용자의 이메일 주소
  • nickname: 사용자의 닉네임
  • profilePictureUrl: 사용자의 프로필 사진 URL

 

UserRegistDto  : 사용자 등록시에 사용되는 GoogleInfoDto에서 변환되며 확장과 유지보수에 용이하도록 한번 더 분리했습니다. 

  • email: 사용자의 이메일 주소
  • nickname: 사용자의 닉네임
  • profilePictureUrl: 사용자의 프로필 사진 URL
  • role: 사용자의 역할

 

UserEntity : User 클래스는 시스템의 사용자 정보를 나타내는 엔터티입니다. 이 클래스는 데이터베이스에 저장되며 사용자의 정보를 관리하는 데 사용됩니다.

  • id: 사용자의 고유 식별자
  • userBaseInfo: 사용자의 기본 정보 (UserBaseInfo 임베디드 타입을 사용)
  • rol: 사용자의 역할

 

 

AuthService 생성

  • Google OAuth를 통해 인증된 사용자의 토큰을 검증하고 사용자 정보를 가져오는 주요 로직을 포함하고 있습니다.
@Service
@Slf4j
public class AuthService {

    @Value("${spring.security.oauth2.client.registration.google.client-id}")
    String clientId;

    // Google OAuth 토큰을 검증하고 사용자 정보를 반환합니다.
    public GoogleInfoDto authenticate(String token) {
        return extractUserInfoFromToken(token);
    }

    // 토큰에서 Google 사용자 정보를 추출합니다.
    private GoogleInfoDto extractUserInfoFromToken(String token) {
        try {
            log.info("token : {}", token);
            GoogleIdTokenVerifier verifier = createGoogleIdTokenVerifier();
            // 토큰 검증
            GoogleIdToken idToken = verifier.verify(token);
            Payload payload = idToken.getPayload();
            // Payload로부터 사용자 정보 추출
            return convertPayloadToGoogleInfoDto(payload);

        } catch (GeneralSecurityException | IOException e) {
            throw new BusinessException(ErrorMessages.SECURITY_EXCEPTION);
        }
    }

    // Payload를 GoogleInfoDto로 변환합니다.
    private GoogleInfoDto convertPayloadToGoogleInfoDto(Payload payload) {
        String email = payload.getEmail();
        String name = payload.get("name").toString();
        String pictureUrl = payload.get("picture").toString();
        return new GoogleInfoDto(email, name, pictureUrl);
    }

    // Google ID 토큰 검증기를 생성합니다.
    private GoogleIdTokenVerifier createGoogleIdTokenVerifier() {
        return new GoogleIdTokenVerifier.Builder(
                new NetHttpTransport(), new JacksonFactory())
                .setAudience(Collections.singletonList(clientId))
                .build();
    }
}

 

SecurityConfig 생성

  • Spring Security의 설정을 담당하며, Google OAuth2를 사용하기 위한 필수 구성 요소 중 하나입니다. 이 클래스는 인증 메커니즘, 필터, 권한 설정 등을 정의하여 애플리케이션의 보안을 구성합니다.

 

로그인 및 DB 저장 

UserController : 구글이 클라이언트에게 제공한 인증코드를 가지고 정보를 얻고 로그인을 진행합니다.

  • Google로부터 받은 토큰을 authService.authenticate 메소드에 전달하여 사용자 정보(GoogleInfoDto)를 추출합니다.
  • 추출된 사용자 정보를 LoginService의 processUserLogin 메소드에 전달하여 JWT 토큰을 생성합니다.
  • 생성된 토큰들을 HTTP 세션에 저장하고, 클라이언트에 응답으로 반환합니다.

 

 

LoginService : Google OAuth를 통해 인증된 사용자 정보를 처리하고(저장 Or 호출), 사용자의 로그인 및 토큰 생성을 담당합니다

  • GoogleInfoDto를 받아 사용자를 찾거나 생성합니다 (getOrCreateUser).
  •  JWT 액세스 토큰과 리프레시 토큰을 생성합니다 (generateAuthTokens)

 

UserRegistryService : Dto를 받아 User 엔터티로 변환하고, 이를 데이터베이스에 저장합니다.

UserRpoisotry : 데이터에 접근하기 위한 JPA 리포지토리입니다.


프론트 엔드(react)

NPM 패키지 설치

  • Google OAuth를 사용하기 위해 필요한 NPM 패키지를 설치해야 합니다
npm install @react-oauth/google

 

GoogleLoginButton

import {GoogleLogin} from "@react-oauth/google";
import {GoogleOAuthProvider} from "@react-oauth/google";

const GoogleLoginButton = () => {
  const clientId = //클라이언트 아이디 입력

  const handleLoginSuccess = async (response) => {
    console.log(response);

    // Google의 ID 토큰 추출
    const idToken = response.credential;



    try {
      // 백엔드 서버로 ID 토큰 전송
      const res = await fetch('/api/auth/google', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify( idToken)
      });

      if (!res.ok) {
        throw new Error('Failed to authenticate');
      }


      window.location.href = '/';
    } catch (error) {
      console.error('Error during login:', error);
    }
  };

  const handleLoginFailure = (error) => {
    console.log(error);
  };

  return (
      <GoogleOAuthProvider clientId={clientId}>
        <GoogleLogin
            onSuccess={handleLoginSuccess}
            onFailure={handleLoginFailure}
        />
      </GoogleOAuthProvider>
  );
};

export default GoogleLoginButton;

마무리

이렇게 Google OAuth를 활용한 사용자 인증과 로그인 과정의 구현에 대한 설명을 마치게 되었습니다. 글 내용에 잘못된 부분이 있다면 지적해주시면 감사하겠습니다!!!

 

저는 로그인 성공 시 JWT 토큰을 발급받아 세션에 저장하는 방식을 선택했습니다. 일반적으로 JWT는 Stateless한 특성 때문에 서버의 부담을 줄여주는 장점을 가지고 있지만 이번 구현에서는 다른 고려사항을 우선했습니다. 특히, 프론트엔드에서 JWT 토큰이 탈취당하는 상황을 방지하기 위해  세션을 사용하기로 결정했습니다. 또한, 현재의 웹 애플리케이션에 대한 트래픽 예상이 과도하게 높지 않아, Stateless의 장점을 포기하는 대신 보안을 강화하는 방향을 선택했습니다. 

다음 포스팅은 이렇게 했을 때 발생한 문제점에 대해 포스팅 해보도록 하겠습니다!