Backend/Spring

Spring boot 3.x + flutter OAuth2(Google, Kakao) 로그인/회원가입 구현 1 - flutter

keepbang 2024. 7. 31. 15:23

▤ 목차

    🙇‍♂️ Flutter 개발자가 아니라 코드 작성부분은 다소 생략하였습니다.ㅠㅠ

     

     

    Flutter로 하는 OAuth2 로그인 방법

    Spring security로 OAuth2로그인을 구현 하려고 하면 보통 web으로 로그인 하는게 많이 보입니다. 하지만 kakao developer 문서를 보면 네이티브 앱에서는 리다이렉트 방식을 사용 할 수 없다고 나옵니다.

    그래서 공식 문서를 참고하여 프론트(Flutter)백엔드(Spring)의 역할을 나눠 개발하기로 했습니다.

     

    GoogleKakao 로그인 개발을 하기 위해 아래 두 문서를 참고했습니다.

     

    [참고]

     

     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

     

    google_sign_in | Flutter package

    Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account.

    pub.dev

     

     

    • 프론트 역할
      • access token 발급 후 api 호출
      • access token 만료 처리
    • 백엔드 역할
      • access token으로 사용자 정보 조회 후 로그인 / 회원가입 처리
      • token 발급

    위 내용을 토대로 시퀀스 다이어 그램을 그려봤습니다.

    시퀀스 다이어그램 설명

    1. [flutter] OAuth 로그인(Kakao, Google sdk 사용)
    2. [spring] 로그인 후 받은 access_token으로 application(spring server)에 token 발급 api 요청
    3. [spring] access_token으로 google 및 kakao 사용자 조회 api 호출
    4. [spring] 조회된 사용자로 회원 검증 및 임시 회원가입 및 토큰 발급
    5. [flutter] access_token 만료 처리

     

    Flutter 설정

    pubspec.yaml 파일에 sdk 파일을 추가합니다.

    dependencies:
    ...
      kakao_flutter_sdk_user: ^1.2.1
      google_sign_in: ^5.0.7
      
    ...

     

     

    그 다음 소셜 로그인을 위한 IOS/Android 세팅을 해야 합니다.

     

    저는 아래 블로그 글을 참고하여 세팅해줬습니다.

     

    [Flutter] Kakao Login ① - 준비

    소셜 로그인 구현 세번째는 카카오 로그인 입니다. 세팅방법은 소셜 로그인중에서는 제일 간단하지 싶습니다. Kakao Developers 카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오

    dalgoodori.tistory.com

     

     

    [Flutter] Google Login ① - 준비

    소셜 로그인 구현 두번째는 구글 로그인 입니다. 파이어베이스를 사용하면 훨씬 간단하지만 파이어베이스 없이 구현해보겠습니다. 먼저 프로젝트를 만들기 위해 Google Cloud Platform 에서 계정의

    dalgoodori.tistory.com

     

     

    Flutter 로그인 코드 작성

    카카오 developer 문서구글 SDK api 문서를 참고하여 코드를 작성해줍니다.

     

    Future<void> googleLogin() async {
        var googleLoginHelper = new GoogleLoginHelper();
    
        googleLoginHelper.login()
        .then((accessToken) {
          log('accessToken: $accessToken');
    
          if (accessToken == null) {
            EasyLoading.showError('로그인/회원가입에 실패했습니다.',
                duration: const Duration(seconds: 3),
                maskType: EasyLoadingMaskType.black,
                dismissOnTap: false);
            return;
          }
    
          autoLogin(accessToken, LoginPlatform.GOOGLE)
              .then((value) => afterLogin(value))
              .then((value) => googleLoginHelper.logout(accessToken));
    
        });
      }
    
    Future<void> kakaoLogin() async {
        var kakaoLoginHelper = new KakaoLoginHelper();
    
        kakaoLoginHelper.login().then((accessToken) {
          log('accessToken: $accessToken');
    
          if (accessToken == null) {
            EasyLoading.showError('로그인/회원가입에 실패했습니다.',
                duration: const Duration(seconds: 3),
                maskType: EasyLoadingMaskType.black,
                dismissOnTap: false);
            return;
          }
    
          autoLogin(accessToken, LoginPlatform.KAKAO)
              .then((value) => afterLogin(value))
              .then((value) => kakaoLoginHelper.logout());
        });
    }
      
    Future<AuthModel> autoLogin(String token, LoginPlatform platform) {
        return repo.login(code: token, platform: platform);
    }
    
    // repo ...
    
      Future<AuthModel> login({required String code, required LoginPlatform platform}) async {
        try {
          Response response = await apiClient.request(
            '/auth/login/${platform.name}',
            options: Options(method: 'POST'),
            data: {'code': code},
          );
          return AuthModel.fromJson(response.data['data']);
        } on DioException catch (ex) {
          log(ex.response.toString());
          String errorMessage = json.decode(ex.response.toString())["errorMessage"];
          throw Exception(errorMessage);
        }
      }

     

     

    이런 식으로 Controller 쪽 코드를 작성해주고 kakao, google 각각 helper 클래스를 생성하여 로그인을 할 수 있도록 구현합니다.

     

    class KakaoLoginHelper {
    
      Future<String> getKakaoKeyHash() async {
        var key = await KakaoSdk.origin;
        return key;
      }
    
      Future<String?> login() async {
        var key = await getKakaoKeyHash();
        log('kakaologinHelper + $key');
        if (await isKakaoTalkInstalled()) {
          try {
            OAuthToken token = await UserApi.instance.loginWithKakaoTalk();
            log('카카오톡으로 로그인 성공 : ${token.toString()}');
            return token.accessToken;//토큰 정보
          } catch (error) {
            print('카카오톡으로 로그인 실패 $error');
    
            // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
            // 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
            if (error is PlatformException && error.code == 'CANCELED') {
              return null;
            }
            // 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인
            try {
              OAuthToken token = await UserApi.instance.loginWithKakaoAccount();
    
              log('카카오계정으로 로그인 성공 : ${token.toString()}');
              return token.accessToken; //토큰 정보
            } catch (error) {
              print('카카오계정으로 로그인 실패 $error');
            }
          }
        } else {
          try {
            OAuthToken token = await UserApi.instance.loginWithKakaoAccount();
            log('카카오계정으로 로그인 성공 : ${token.toString()}');
            return token.accessToken; //토큰 정보
    
          } catch (error) {
            print('카카오계정으로 로그인 실패 $error');
          }
        }
        return null;
      }
    
      Future<void> logout() async {
        try {
          UserApi.instance.logout();
        } catch (error) {
          print('카카오 로그인 실패 $error');
        }
      }
    
    }
    
    ...
    
    class GoogleLoginHelper {
    
      final GoogleSignIn googleSignIn = GoogleSignIn();
    
      Future<String?> login() async {
        try {
          final GoogleSignInAccount? googleSignInAccount = await googleSignIn
              .signIn();
    
          final GoogleSignInAuthentication googleSignInAuthentication = await googleSignInAccount!
              .authentication;
    
          print(googleSignInAuthentication.accessToken);
    
          return googleSignInAuthentication.accessToken;
        } catch (error) {
          print(error);
        }
      }
    
      Future<void> logout(String? accessToken) async {
        await revokeToken(accessToken!);
    
        await googleSignIn.signOut();
        print('User signed out');
      }
    
      Future<void> revokeToken(String token) async {
        final response = await http.post(
          Uri.parse('https://oauth2.googleapis.com/revoke'),
          headers: {'Content-Type': 'application/x-www-form-urlencoded'},
          body: 'token=$token',
        );
    
        if (response.statusCode == 200) {
          print('Token revoked successfully');
        } else {
          print('Failed to revoke token');
        }
      }
    
    }

     

     

    flutter는 여기까지 마무리하고 login 함수에서 호출하고 있는 API를 다음 블로그 글에서 만들어 보겠습니다.

    'Backend > Spring' 카테고리의 다른 글

    이커머스 서비스에서 동시성 문제 처리해보기  (0) 2024.05.08
    Spring Transactional Isolation  (0) 2023.01.04
    Spring Transactional Propagation  (0) 2022.12.28
    Spring Bean 설정 방법  (0) 2021.08.29
    스프링 빈(Bean)  (0) 2021.08.20