Flutter에서 Apple Login 구현하기

sumong·2023년 1월 11일
4
post-thumbnail

참고 자료 : https://kyungsnim.net/160


Apple Login을 추가해야 하는 이유와 불평불만...

: 우리의 갓-애플께서는 앱에 SNS 로그인이 있는 경우, 애플ID로 로그인하는 기능이 같이 있어야만 검수를 통과시켜줍니다.

사실 궁금합니다. 애플 로그인 쓰시는 분이 많나요? 아무리 봐도 ‘애플 로그인을 써보렴!’ 이러면서 강요하는 느낌이지만, 애플이 하라고 하니까 어쩌겠어요, 해야죠.

그동안 앱개발자로 일하면서 구글과 애플의 여러 가지 정책들을 봤지만, 그 중에서도 애플 로그인 강제는 가장 이해가 안 되는 정책 1등이라고 생각합니다. 나머지 정책들은 편의성이나 보안 관련 이유라도 있었지, 이건 진짜로 무슨 이유 때문인지도 모르겠어요. 심지어 공식 문서는 친절하지도 않고요. 애플 생태계를 강요하는 ‘갑질'의 느낌입니다. 그래서 참 싫네요.


전체 Flow

Google Firebase를 사용한다는 점이 핵심입니다!

  1. Firebase 초기 설정 및 Firebase Authentication에서 Apple Sign in 활성화
  2. Apple Developer 페이지에서 애플 로그인 기능 활성화
  3. Flutter에서 Apple로 로그인하는 코드 작성

Apple Sign in에서 제공하는 정보

  • 이름 : 필수로 받아올 수 있습니다.
    단, 이 이름이 ‘실명’임을 보장하진 않습니다. (유저가 수정 가능)
  • 이메일 : 유저의 선택에 따라 제공받지 못할 수 있습니다.
  • 전화번호, 프로필 사진 등 다른 SNS 로그인에서 제공하는 정보는 아예 받아올 수 없습니다.
    (오로지 이름과 이메일만 받을 수 있습니다.)

아래는 Apple Sign in 버튼 클릭 시 나오는 화면입니다.


Firebase Authentication에서 Apple Sigin in 활성화

⛔ 여기서는 Firebase 회원가입 및 앱 등록 과정은 제외했습니다.

  1. Firebase Console에 들어갑니다.

  2. 생성된 앱 프로젝트에 들어간 뒤, Authentication 메뉴를 누릅니다.

  3. Sign-in method 탭에 들어간 뒤, ‘로그인 제공업체' 화면의 ‘새 제공업체 추가' 버튼을 누릅니다.

  4. 로그인 제공업체에서 ‘Apple’을 선택합니다.

  5. ‘사용 설정'을 눌러서 활성화하고, 아래의 승인 콜백 URL을 복사해둡니다. 그리고 ‘저장' 버튼을 누릅니다.
    승인 콜백 URL은 Apple Developer 페이지에서 애플 로그인 기능을 활성화할 때 사용할 것입니다.


Apple Developer 페이지에서 애플 로그인 기능 활성화

⛔ 여기서는 애플 개발자 계정을 등록하는 과정은 제외했습니다.
또한 기존 프로젝트에 대한 설정이 이미 존재하고, 거기에 애플 로그인을 추가하는 방법을 설명했습니다.
(새 프로젝트의 경우 약간 다를 수 있습니다.)

  1. 이 링크를 클릭해서 애플 개발자 사이트에 접속한 후, Account 페이지로 이동합니다.

  2. Identifiers로 이동한 뒤, 기존 앱에 대한 Identifier를 클릭합니다. (없다면 신규 Identifier를 만듭니다.)

  3. 아래로 내려서 ‘Sign In with Apple’ 왼쪽의 체크박스를 클릭하고, 오른쪽에 생성된 Edit 버튼을 클릭합니다.

  4. ‘Server to Server Notification Endpoint’의 입력창에 위의 1-6에서 받은 승인 콜백 URL을 입력하고, ‘Save’ 버튼을 누릅니다.

  5. 맨 위 오른쪽의 ‘Save’ 버튼을 누르고, 이후 나오는 팝업에서 ‘Confirm’ 버튼을 누릅니다.

  6. Keys로 이동한 뒤, 기존 Key를 클릭하고 ‘Edit’ 버튼을 누릅니다.
    (없다면 새로 만듭니다.)

  7. Sign in with Apple’에 체크하고, 우측의 ‘Configure’ 버튼을 클릭합니다.

  8. Configure Key의 Primary App ID로 현재 작업 중인 앱을 선택합니다. 이후 ‘Save’를 클릭합니다.

  9. ‘Continue’ 버튼을 클릭합니다.

  10. ‘Save’ 버튼을 클릭합니다.


Flutter에서 Apple로 로그인하는 코드 작성

⚠️ 아래 코드는 신뢰도가 상대적으로 낮은 외부 라이브러리를 같이 써서 작성한 코드입니다. 따라서 신뢰할 수 있는 라이브러리만 써서 코드를 작성하고 싶다면, Firebase’만’ 써서 리팩토링하시는 걸 추천드립니다.
(Firebase를 통해서 애플 로그인 인증이 가능하기 때문에 문제는 없어 보입니다. 다만 코드 자체는 완전히 달라지겠죠.)

설정이 좀 길죠? 위 설정을 끝마친다면, 드디어 코드를 작성하는 시간이 왔습니다.

  1. pubspec.yaml에 패키지 추가 : 다음의 패키지들을 추가합니다.
dependencies:
  the_apple_sign_in: ^1.1.1 #2022.04.14 기준 최신버전
  firebase_auth: ^3.3.14 #2022.04.14 기준 최신버전, 단 Firebase 다른 패키지와 같이 추가해야 함.
  1. 화면에 로그인 버튼을 만듭니다. (코드 생략)

  2. 로그인 관련 메서드를 만듭니다.

Future<void> appleLogin() async {
  // 1. 애플 로그인이 이용 가능한지 체크함.
  if(await TheAppleSignIn.isAvailable()) {
    // 2. 로그인 수행(FaceId 또는 Password 입력)
    final AuthorizationResult result = await TheAppleSignIn.performRequests([
      const AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
    ]);

    // 3. 로그인 권한 여부 체크
    switch(result.status) {
      // 3-1. 로그인 권한을 부여받은 경우
      case AuthorizationStatus.authorized:
        print('로그인 결과 : ${result.credential!.fullName}\n ${result.credential!.email} ');
        break;
      // 3-2. 오류가 발생한 경우
      case AuthorizationStatus.error:
        print('애플 로그인 오류 : ${result.error!.localizedDescription}');
        break;
      // 3-3. 유저가 직접 취소한 경우
      case AuthorizationStatus.cancelled:
        print("취소!!!");
        break;
    }
  } else {
    print('애플 로그인을 지원하지 않는 기기입니다.');
  }
}

Apple Login 전체 예시 코드

링크

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:the_apple_sign_in/the_apple_sign_in.dart';
    
import 'button_test/button_test_page.dart';
    
void main() => runApp(MyApp());
    
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(home: SignInPage());
  }
}
    
class SignInPage extends StatefulWidget {
  
  State<StatefulWidget> createState() => _SignInPageState();
}
    
class _SignInPageState extends State<SignInPage> {
  final Future<bool> _isAvailableFuture = TheAppleSignIn.isAvailable();
    
  String errorMessage;
    
  
  void initState() {
    super.initState();
    checkLoggedInState();
    
    TheAppleSignIn.onCredentialRevoked.listen((_) {
      print("Credentials revoked");
    });
  }
    
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sign In with Apple Example App'),
      ),
      backgroundColor: Colors.grey,
      body: SingleChildScrollView(
        child: Center(
          child: SizedBox(
            width: 280,
            child: FutureBuilder<bool>(
              future: _isAvailableFuture,
              builder: (context, isAvailableSnapshot) {
                if (!isAvailableSnapshot.hasData) {
                  return Container(child: Text('Loading...'));
                }
    
                return isAvailableSnapshot.data
                  ? Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      SizedBox(
                        height: 100,
                      ),
                      AppleSignInButton(
                        onPressed: logIn,
                      ),
                      if (errorMessage != null) 
                        Text(errorMessage),
                      SizedBox(
                        height: 200,
                      ),
                      ElevatedButton(
                        child: Text("Button Test Page"),
                        onPressed: () {
                          Navigator.push(
                            context,
                            MaterialPageRoute(
                              builder: (_) => ButtonTestPage()));
                        },
                      )
                  ])
                  : Text(
                      'Sign in With Apple not available. Must be run on iOS 13+'
                    );
              },
      )))),
    );
  }
    
  void logIn() async {
    final AuthorizationResult result = await TheAppleSignIn.performRequests([
      AppleIdRequest(requestedScopes: [Scope.email, Scope.fullName])
    ]);
    
    switch (result.status) {
      case AuthorizationStatus.authorized:
        // Store user ID
        await FlutterSecureStorage()
          .write(key: "userId", value: result.credential.user);
    
        // Navigate to secret page (shhh!)
        Navigator.of(context).pushReplacement(MaterialPageRoute(
          builder: (_) => AfterLoginPage(credential: result.credential)));
        break;
    
      case AuthorizationStatus.error:
        print("Sign in failed: ${result.error.localizedDescription}");
        setState(() {
          errorMessage = "Sign in failed";
        });
        break;
    
      case AuthorizationStatus.cancelled:
        print('User cancelled');
        break;
    }
  }
    
  void checkLoggedInState() async {
    final userId = await FlutterSecureStorage().read(key: "userId");
    if (userId == null) {
      print("No stored user ID");
      return;
    }
    
    final credentialState = await TheAppleSignIn.getCredentialState(userId);
    switch (credentialState.status) {
      case CredentialStatus.authorized:
        print("getCredentialState returned authorized");
        break;
    
      case CredentialStatus.error:
        print(
                "getCredentialState returned an error: ${credentialState.error.localizedDescription}");
        break;
    
      case CredentialStatus.revoked:
        print("getCredentialState returned revoked");
        break;
    
      case CredentialStatus.notFound:
        print("getCredentialState returned not found");
        break;
    
      case CredentialStatus.transferred:
        print("getCredentialState returned not transferred");
        break;
    }
  }
}
    
class AfterLoginPage extends StatelessWidget {
  final AppleIdCredential credential;
    
  const AfterLoginPage({ this.credential});
    
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Logged in Success️'),
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Text(
                "Welcome ${credential.fullName?.givenName}!",
                style: TextStyle(fontSize: 25),
                textAlign: TextAlign.center,
              ),
              Text(
                "Your email: '${credential.email}'",
                style: TextStyle(fontSize: 15),
                textAlign: TextAlign.center,
              ),
              OutlinedButton(
                child: Text("Log out"),
                onPressed: () async {
                  await FlutterSecureStorage().deleteAll();
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (_) => SignInPage()));
              })
          ]),
    )));
  }
}
profile
Flutter 메인의 풀스택 개발자 / 한양대 컴퓨터소프트웨어학과, HUHS의 화석

0개의 댓글