[iOS] 다중 로그인 막기

Jehyeon Lee·2023년 12월 14일
0
  • Issue
    • 앱을 배포했는데 다중사용자에 대한 대책을 안세워서 하나의 유저가 동일한 아이디를 여러 폰에서 실행을 시키는 일이 발생하였습니다.
  • Problem
    • 서버에 유저의 접속 세션을 저장하고 앱을 종료하거나 로그아웃하면 세션을 해제하는 방식으로 구현했습니다. AppDelegate의 applicationWillTerminate 메서드를 사용하여 앱을 종료할 때 세션을 해제하도록 했는데, applicationWillTerminate에서 세션 작업을 위한 서버와의 통신 중에 앱이 비동기 작업을 완료하지 못한 채 종료되어 문제가 발생했습니다.

applicationWIllTerminate란?
iOS에서 라이프사이클 중 일반적으로 시스템에 의해 앱이 종료되기 전에 마지막 작업을 수행하거나 앱 데이터를 저장하거나 앱이 종료되기 전에 필요한 정리를 수행하기 위한 메서드입니다.

  • Solution
    • 해결방법은 applicationWillTerminate 메서드의 끝부분에 앱의 sleep(3)을 두어 앱의 종료시점을 3초정도 늦추고 서버와의 작업을 하게끔 해두었습니다.
    • 한 아디디당 하나의 기기만 로그인 가능하게 끔 세션을 체크하였습니다.
    • 로그인 시 로그인한 세션 데이터를 저장했습니다.
    • 자동 로그인 시 세션 데이터를 저장 시켰습니다.
    • 로그아웃, 앱 종료 시 세션 데이터를 삭제했습니다.

세션저장(로그인)

configButton

private func configButton() {
        nextButton.rx.tap
            .subscribe(onNext: { [weak self] in
                guard let self = self else { return }
                UserDefaultsManager.shared.setUserData(userData: user)
                nextButton.tappedAnimation()
                FirestoreService.shared.saveDocument(collectionId: .session, documentId: user.phoneNumber, data: User.tempUser) { _ in }
                (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.changeRootView(TabBarController(), animated: true)
            })
            .disposed(by: disposeBag)
    }

다음 버튼을 누르면 현재 로그인한 유저가 있는지 체크하는지 확인합니다.

saveDocument

func saveDocument<T: Codable>(collectionId: Collections, documentId: String, data: T, completion: @escaping (Result<Bool, Error>) -> Void) {
        DispatchQueue.global().async {
            do {
                try self.dbRef.collection(collectionId.name).document(documentId).setData(from: data.self)
                completion(.success(true))
                
                print("Success to save new document at \(collectionId.name) \(documentId)")
            } catch {
                print("Error to save new document at \(collectionId.name) \(documentId) \(error)")
                completion(.failure(error))
            }
        }
    }
  • 서버의 collection중 파라미터로 받은 collectionId를 받아 session에 접근하고 document중 유저의 번호를 저장시키는 코드입니다.

세션저장(자동로그인)

sceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        let curentUser = UserDefaultsManager.shared.getUserData()
        
        if UserDefaultsManager.shared.isLogin() {
            let checkService = CheckService()
            let user: User = User.tempUser
            checkService.checkUserId(userId: curentUser.userId) { isUser in
                if isUser {
                    FirestoreService.shared.saveDocument(collectionId: .session, documentId: curentUser.phoneNumber, data: user) { _ in }
                    let rootViewController = TabBarController()
                    self.window?.rootViewController = rootViewController
                } else {
                    let rootViewController = UINavigationController(rootViewController: SignViewController())
                    self.window?.rootViewController = rootViewController
                }
            }
        } else {
            let rootViewController = UINavigationController(rootViewController: SignViewController())
            window?.rootViewController = rootViewController
        }
        window?.makeKeyAndVisible()
    }

저는 유저가 한번 로그인을 하면 유저 디포트에 유저의 정보를 저장시켰습니다. 그래서 유저의 아이디값을 저장시켜놓고 만약 기기에 유저정보가 있다면 이제 서버의 유저의 정보가 있는지 체크를 해야합니다.

checkUserId

func checkUserId(userId: String, completion: @escaping (_ isRight: Bool) -> ()) {
        Loading.showLoading(backgroundColor: .systemBackground.withAlphaComponent(0.8))
        DispatchQueue.global().async {
            self.dbRef.collection("users")
                .whereField("id", isEqualTo: userId)
                .getDocuments { snapShot, err in
                    guard err == nil, let documents = snapShot?.documents else {
                        return
                    }
                    
                    if let document = documents.first {
                        let user = SignInViewModel().convertDocumentToUser(document: document)
                        UserDefaultsManager.shared.setUserData(userData: user)
                    }
                    
                    completion(documents.first != nil)
                }
        }
    }

서버와 통신을 하여 유저 collection에 유저기기의 id 값이 있는지 체크를 하여 컴플리션 핸들러로 빼서 bool 값을 리턴 받도록 하였습니다.

scene 메서드 중간부

checkService.checkUserId(userId: curentUser.userId) { isUser in
                if isUser {
                    FirestoreService.shared.saveDocument(collectionId: .session, documentId: curentUser.phoneNumber, data: user) { _ in }
                    let rootViewController = TabBarController()
                    self.window?.rootViewController = rootViewController
                } else {
                    let rootViewController = UINavigationController(rootViewController: SignViewController())
                    self.window?.rootViewController = rootViewController
                }
            }

만약 유저가 있다면 이제부터 session Collection에 유저의 번호를 저장시킵니다.
만약 없다면 초기화면으로 돌아가게끔 코드를 작성했습니다.

세션삭제

applicationWillTerminate

 func applicationWillTerminate(_ application: UIApplication) {
        let checkService = CheckService()
        checkService.disConnectSession()
        sleep(3)
    }

AppDelegate에서 앱이 종료될 시점에 disConnectSession이라는 메서드를 사용하여 앱의 종료시점에서 세션을 제거 했습니다. 이부분에서 sleep을 두지 않으면 서버와의 작업이 끝나지 않고 앱을 종료시켜버립니다.

disConnectSession

func disConnectSession() {
        let currentUser = UserDefaultsManager.shared.getUserData()
        let phoneNumber = currentUser.phoneNumber
        print(phoneNumber)
        FirestoreService.shared.dbRef.collection("session").document(phoneNumber).delete { err in
            if let err = err {
                print("Error updating document: \(err)")
            } else {
                print("Document successfully updated")
            }
        }
    }
  • 일단 현재 유저의 정보를 가져왔습니다.
  • 세션에는 현재 로그인한 유저의 번호를 저장시키도록 하였습니다.
  • 그리고 파이어베이스의 session컬렉션에 있는 document(유저번호) 값을 삭제시킵니다.

세션검색

FirestoreService.shared.loadDocument(collectionId: .session, documentId: number, dataType: User.self) { [weak self] result in
                        guard let self = self else { return }
                        
                        switch result {
                        case .success(let user):
                            guard user != nil else { return }
                            showCustomAlert(alertType: .onlyConfirm, titleText: "경고", messageText: "다른기기에서 접속중입니다.", confirmButtonText: "확인", comfrimAction: { [weak self] in
                                guard let self = self else { return }
                                navigationController?.popViewController(animated: true)
                            })
                        case .failure(let err):
                            print("SingInVIewController 세션부분 에러입니다. error: \(err) ")
                        }
                    }

밑의 메서드를 이용하여 컴플리션으로 .success와 .failure을 받습니다.
만약 서버와 통신이 성공이되면 이제 user가 있는지 판단을 합니다.
있다면 알럿을 통해 이미 접속된 유저를 알려주고 navigation을 pop 시킵니다. 그렇게 그전 화면으로 돌아가게끔 작성했습니다.

loadDocument

func loadDocument<T: Codable>(collectionId: Collections, documentId: String, dataType: T.Type, completion: @escaping (Result<T?, Error>) -> Void) {
        DispatchQueue.global().async {
            self.dbRef.collection(collectionId.name).document(documentId).getDocument { (snapshot, error) in
                if let error = error {
                    print("Error to load new document at \(collectionId.name) \(documentId) \(error)")
                    completion(.failure(error))
                    return
                }
                
                if let snapshot = snapshot, snapshot.exists {
                    do {
                        let documentData = try snapshot.data(as: dataType)
                        print("Success to load new document at \(collectionId.name) \(documentId)")
                        completion(.success(documentData))
                    } catch {
                        print("Error to decode document data: \(error)")
                        completion(.failure(error))
                    }
                } else {
                    completion(.success(nil))
                }
            }
        }
    }

서버의 collection을 enum타입으로 정의하여 .session의 값이 들어가있습니다. session안에 documentId 값으로 유저의 번호가 있다면 completion(.success(documentData))로 유저의 정보를 넘깁니다.
만약 있다면 completion(.success(documentData))안에 User타입의 user값이 리턴되기에 있으면 막는로직을 통하여 로그인이 제한됩니다.

profile
공부한거 느낌대로 써내려갑니당

0개의 댓글

관련 채용 정보