코드이그나이터4 마크다운 블로그 MVP 만들기 - 9 - 소셜 로그인 기능 만들기

고은연·2021년 10월 27일
1

소셜 로그인 기능 만들기

이번 예제는 https://github.com/koeunyeon/ci4/commits/blog-social-login에 코드가 있습니다.

드디어 로그인 기능을 만들 시간이 왔습니다.
이제껏 우리는 아무나 들어와서 글을 쓸 수 있는 시스템을 만들었었습니다. 물론 익명 게시판도 나름의 의미가 있기는 하지만, 본연의 목적은 로그인을 수반한 블로그 시스템이었으므로 본연의 목적에 맞게 개발해 봅시다.

소셜 로그인이란?

소셜 로그인이란 다른 회사의 회원 정보를 가지고 우리 서비스에 로그인하는 것을 말합니다. 최근에는 "구글로 로그인", "네이버로 로그인" 등 포털이나 큰 기업의 회원 정보로 로그인하는 일이 자주 있죠.

사용자들은 "뭔가 알 수 없는 사이트에 내 정보를 넘기는 것"을 무척 싫어하고, 회원 가입을 귀찮아 하기도 합니다. 소셜 로그인의 경우 만약 브라우저에 소셜 서비스가 로그인된 상태라면 버튼 클릭 하나만으로 회원가입과 로그인이 일어나므로 편리한 데다가, 만약 특정 사이트에 가입했다가도 맘에 안들면 언제든지 사용을 중지시킬 수 있어서 편리합니다.

작은 서비스를 런칭하는 스타트업에서도 장점이 많습니다.

  • a.) 우선 회원가입 기능을 만드는 것은 특별히 난이도가 있는 작업은 아닙니다만, 회원 정보는 무척 관리하기 까다롭습니다. 개인정보 보호법에 따라 1년 이상 접속하지 않은 접속자에 대한 휴면 처리, 3년 이상 접속하지 않으면 강제 삭제. 1년에 1회 이상 개인정보 사용의 통지, 같은 법률적인 문제가 먼저 걸립니다. 게다가 비밀번호를 암호화해서 관리해야 하므로 암호화 키가 있어야 하고, 개인정보는 복호화 가능한 암호화가 필요하므로 추가적인 조치가 또 필요합니다. 심지어는 데이터베이스가 유출될 경우 법적인 책임도 져야 합니다.
  • b.) 직접 회원가입을 만드는 것보다 소셜 로그인이 훨씬 빨리 만들 수 있습니다. 이게 가장 큰데, 복잡한 문제를 생각하기보다는 그냥 얼른 만들어버리는 것이 속도면에서 유리합니다.

다만 신규 서비스를 런칭하는 입장에서는 주의해야 하는 점이 몇 개 있습니다.

  • a.) 소셜 로그인을 제공하는 업체(구글,페이스북,네이버..)가 언제 소셜 로그인 기능을 중단할 지 모른다는 점입니다. 혹 소셜 로그인 서비스를 중단하지 않더라도 모종의 이유로 내 서비스가 연동이 안되는 점도 우려할 점이죠.
  • b.) 사실상 회원 정보가 소셜 로그인 제공자에게 종속됩니다. 추가 정보를 입력받지 않으면 소셜에서 제공하는 정보만으로는 회원을 운용하는 데 번거로움이 많으실 거라 생각합니다.

언급한 장단점을 인지한 채, 일단 소셜 로그인으로 회원 기능을 추가하겠습니다.
이번 챕터에서는 "구글 소셜 로그인"만 구현합니다. 우리가 타겟으로 삼는 개발자들 중에서는 구글 아이디가 없는 사람은 거의 없을 것이라고 판단했기 때문입니다. 물론 여러가지 소셜 로그인을 구현하면 좋기야 하겠지만, 빠른 서비스 출시를 위해 만드는 시간 뿐만 아니라 테스트하는 시간도 무척 아까우므로 일단 하나만 만들겠습니다.

소셜 로그인의 원리

소셜 로그인은 대부분 oAuth라는 기술을 이용합니다. 위키피디아에 따르면 oAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단입니다. 공개 표준이므로 누구나 스펙만 맞추면 개발할 수 있습니다.
쉽게 말하면 oAuth는 "출입증" 을 발급받는 절차 같은 겁니다. 간단한 흐름은 아래와 같습니다.

  1. 사용자는 마크다운 블로그의 /oauth/google 주소로 접속합니다.
  2. 마크다운 블로그는 이미 인증이 된 사용자인지 확인하고, 인증이 되어 있지 않다면 소셜 서비스(ex. 구글)의 인증 페이지로 이동시킵니다.
  3. 소셜 서비스는 사용자가 소셜 서비스에 로그인되어 있는지 확인하고, 로그인되어 있다면 마크다운 블로그 서비스의 콜백 주소로 인증 토큰을 전달합니다.
  4. 마크다운 블로그의 콜백 주소는 인증 토큰을 전달받은 다음, 소셜 사이트의 정보를 소셜 사이트에 요청합니다.
  5. 응답받은 정보로 서비스에서 활용합니다.

핵심은 소셜 서비스 제공자로부터 "토큰"을 발급받은 다음 "토큰"을 이용해 사용자의 정보에 접근할 수 있는 것입니다.

좀 더 자세하게 알고 싶으시면 OAuth와 춤을 글에 차근차근 설명되어 있으니 읽어보셔도 좋습니다.

소셜 로그인 라이브러리 설치

소셜 로그인 기능을 업체별로 직접 구현할 수도 있겠지만, 굳이 그럴 필요는 없습니다. 이미 oAuth를 한번 더 감싸서 각 소셜 로그인 업체별로 사용하기 쉽게 만들어 놓은 라이브러리들이 언어마다 다 존재하거든요.
PHP에서도 Hybridauth - https://hybridauth.github.io/ 라는 프로젝트가 있습니다. 2021년 10월 현재 46개의 소셜 로그인을 지원하고 있네요.

컴포저를 이용해서 설치해 보겠습니다.

php composer.phar require hybridauth/hybridauth

마이그레이션으로 데이터베이스 테이블 생성

이제 마이그레이션 사용법에 점점 익숙해져 가시리라 생각합니다. 먼저 터미널에서 마이그레이션 파일을 생성합니다.

php spark migrate:create member

데이터를 어떻게 저장할 지 생각해 봅시다.

  1. 먼저 소셜 업체는 여러 개 있을 수 있습니다. 지금은 구글 로그인만 구현할 예정이지만, 언제 소셜 로그인 업체가 추가될 지 모르므로 소셜 업체 이름을 저장하는 것이 좋겠습니다.
  2. 사용자를 식별하는 고유 ID가 필요합니다. 이 데이터는 마크다운 블로그가 가진 고유 식별자가 아니라, 소셜 업체가 가진 고유 식별자입니다.
  3. 회원 이름 정도는 필요할 것 같습니다. 그래야 본인이 로그인했을 때 표기할 수 있을 테니까요.

이를 바탕으로 마이그레이션 파일을 채워봅시다.

<?php namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class Member extends Migration
{
   public function up()
   {
        $this->forge->dropTable('members', true); // (1)

        $this->forge->addField([
            'member_id' => [
                'type' => 'BIGINT',
                'unsigned' => true,
                'auto_increment' => true,
            ],
            'social_name' => [
                'type' => 'VARCHAR',
                'constraint' => '100',
            ],
            'identifier' => [
                'type' => 'VARCHAR',
                'constraint' => '100',
            ],
            'member_name' => [
                'type' => 'VARCHAR',
                'constraint' => '100',
                'null' => true,

            ],
            'created_at' => [
                'type' => 'VARCHAR',
                'constraint' => '25',
            ],
            'updated_at' => [
                'type' => 'VARCHAR',
                'constraint' => '25',
            ],
            'deleted_at' => [
                'type' => 'VARCHAR',
                'constraint' => '25',
                'null' => true,
            ]
        ]);
        $this->forge->addKey('member_id', true);
        $this->forge->createTable('members');
   }

   //--------------------------------------------------------------------

   public function down()
   {
      //
   }
}

마이그레이션을 실행해서 members 테이블을 생성합니다.
소스코드

php spark migrate

마이그레이션 결과는 아래와 같습니다.

Running all new migrations...
        Running: (App) 2021-10-25-060654_App\Database\Migrations\Member
Done migrations.

Migrations 디렉토리에 {YYYY-MM-DD}-{6자리숫자}-Member{YYYY-MM-DD}-{6자리숫자}-Post.php 파일이 둘 다있는데도, 실제 실행은 Member.php만 되었습니다. 이는 코드이그나이터가 내부적으로 마지막 실행을 한 버전을 기록하고 있기 때문입니다.
phpmyadmin 에서 ci4db.migrations 테이블의 데이터를 확인해 보세요.

MemberModel 모델 만들기

데이터베이스 테이블이 생성되었으니, 그에 상응하는 모델을 만듭니다.
app/Models/MemberModel.php

<?php


namespace App\Models;


use CodeIgniter\Model;

class MemberModel extends Model
{
    protected $table = 'members';
    protected $allowedFields = ['social_name', 'identifier', 'member_name'];
    protected $primaryKey = "member_id";

    protected $useSoftDeletes = true;
    protected $useTimestamps = true;
}

구글 소셜 로그인 인증키 받기

구글 로그인을 사용하려면, 우선 "서비스 제공자"가 구글에서 인증키를 발급받는 과정이 필요합니다. "마크다운 블로그"를 만드는 우리가 "서비스 제공자"이므로, 직접 인증키를 발급받아 보겠습니다.
구글 API 프로젝트 생성 - https://console.developers.google.com/projectcreate 페이지에서 프로젝트를 생성합니다.

처음으로 프로젝트를 생성했다면 oAuth 동의 화면을 구성해야 합니다. 좌측의 oAuth 동의 화면을 클릭합니다.

oAuth동의 화면이 보여집니다.
내부 / 외부 여부는 외부를 선택합니다. 설명에 적혀있듯이 내부는 폐쇄적인 조직을, 외부는 누구나 접근할 수 있음을 말합니다.
선택 후 만들기 버튼을 누릅니다.

다음 단계는 앱 정보를 입력하는 겁니다. 앱 이름, 지원 이메일, 개발자 연락처 정보는 필수이므로 입력 후 저장 후 계속 버튼을 클릭합니다. 앱 이름은 식별할 수 있는 이름이면 됩니다.

세번째로는 범위를 입력합니다. 아무 입력 없이 기본값으로 설정하겠습니다.

테스트 사용자는 비워두고 진행해 보겠습니다.

마지막으로 입력한 값을 정리한 내용을 볼 수 있습니다.

대시보드로 돌아와 보면, 게시상태가 "테스트"라고 되어 있을 겁니다. 앱 게시 버튼을 눌러서 프로덕션으로 푸시합시다. 나중에 앱을 변경하면 다시 제출해야 한다고 하긴 하지만, 순수 개발 테스트용이므로 실제로 운영 서버에 마크다운 블로그를 배포할 때는 다른 API를 쓸 겁니다. 걱정말고 배포합시다. 게다가 원한다면 언제든지 테스트 상태로 돌아갈 수 있습니다.


아직 안 끝났습니다. 우리는 이제 막 프로젝트를 구성했을 뿐인걸요. :)
이제 프로젝트를 구성했으므로 API 키를 발급받을 차례입니다. 왼쪽의 사용자 인증 정보 탭을 클릭하고 사용자 인증 정보 만들기 버튼을 누른 후 OAuth 클라이언트 ID 만들기를 선택합니다.

애플리케이션 유형은 웹 애플리케이션입니다.
이름은 취향에 따라 지으시면 됩니다.
승인된 리다이렉션 URI의 경우 반드시 입력해야 하는데, 아직 개발중의 단계라면 도메인이 없을 가능성이 높겠죠. 로컬 주소를 넣어도 잘 동작하므로 http://localhost:8080/oauth/google로 넣겠습시다.

만들기 버튼을 누르면 클라이언트ID와 클라이언트 보안 비밀번호를 받을 수 있습니다. 나중에 언제든지 다시 확인할 수 있지만 귀찮으므로 미리 어딘가에 적어두세요. JSON 다운로드 버튼을 눌러서 다운로드해 두어도 좋습니다.

이제 클라이언트 ID와 클라이언트 보안 비밀번호를 가지고 소셜 로그인을 사용할 수 있게 되었습니다.

인증키를 환경 파일에 설정하기

외부 접속 정보를 소스코드 내에 보관하는 것은 몹시 위험합니다. 혹여 공개 프로젝트에서 외부 접속 정보가 공개되거나 한다는 건 보안상 치명적이죠.
내부 프로젝트라고 해도 외부 접속 정보는 따로 분리해서 관리하는 것을 추천합니다. 접속 정보를 분리해 두면 개발, 스테이지, 운영 등 환경에 따라 달라지는 정보를 소스 변경 없이 관리할 수 있기 때문입니다.
다행히도 우리에게는 환경 설정을 담당하는 .env 파일이 있습니다. .env 파일에 우리의 접속 정보를 설정합시다.
/.env

#--------------------------------------------------------------------
# OAUTH
#--------------------------------------------------------------------

oauth.google.id = 클라이언트ID
oauth.google.secret = 클라이언트_보안_비밀번호

클라이언트ID와 클라이언트 보안 비밀번호는 구글에서 받으신 비밀번호로 변경합니다.

oAuth 컨트롤러 만들기

소셜 로그인 기능도 거의 다 끝나갑니다. 이제 실제로 로그인 인증을 처리하는 컨트롤러를 작성하겠습니다.
app/Controllers/Oauth.php

<?php


namespace App\Controllers;


use App\Models\MemberModel;
use CodeIgniter\Controller;
use Hybridauth\Provider\Google; // (1)

class Oauth extends Controller
{
    private function getConfig(){ // (2)
        $config = [
            'callback' => 'http://localhost:8080/oauth/google',
            'keys' => [
                'id' => env("oauth.google.id"),
                'secret' => env("oauth.google.secret")
            ],
        ];

        return $config;
    }

    public function google() // (3)
    {
        $adapter = new Google($this->getConfig());
        $adapter->authenticate(); // (4)

        if ($adapter->isConnected()){ // (5)            
            $userProfile = $adapter->getUserProfile(); // (6)

            $identifier = $userProfile->identifier ?? null; // (7)
            $displayName = $userProfile->displayName ?? null; // (8)

            $model = new MemberModel();
            $exist_data = $model->where([ // (9)
                'identifier'=> $identifier,
                'social_name' => 'google'
            ])->first();                

            $member_id = null;
            if ($exist_data == null){  // (10)
                $member_id = $model->insert([
                    'social_name' => 'google',
                    'identifier' => $identifier,
                    'member_name' => $displayName
                ]);
            }else{
                $member_id = $exist_data['member_id'];  // (11)
            }

            $_SESSION['member_id'] = $member_id;  // (12)
            $this->response->redirect("/post");

        }else{ // (13)
            $this->response->redirect("/post");
        }
    }

    public function logout(){ // (14)
        $adapter = new Google($this->getConfig());
        $adapter->disconnect(); // (15)

        if (isset($_SESSION['member_id'])){ // (16)
            unset($_SESSION['member_id']);
        }

 $this->response->redirect("/post");
    }
}

(1) 소셜 로그인을 사용하기 위해 Hybridauth\Provider\Google; 라이브러리를 불러옵니다.

(2) 최소한의 환경을 설정합니다.
callback 은 구글 로그인이 완료되고 나면 호출할 URL입니다.
keys는 소셜 로그인을 위한 인증 정보입니다. 우리는 idsecret이 소스코드가 아니라 환경 설정 .env 파일에 정의했었습니다. env 헬퍼 함수는 .env 파일의 내용을 읽어오는 함수이므로 결과적으로 .env 파일의 oauth.google.id 항목과 oauth.google.secret 값을 읽어올 수 있게 됩니다.

(3) 구글에 로그인할 엔드포인트입니다. 엔드포인트 하나가 모든 인증을 처리합니다.

(4) 구글 라이브러리를 설정한 후 인증을 시도합니다. Hybridauth 라이브러리는 만약 구글 인증이 되어 있다면 곧바로 결과를 리턴합니다. 구글 인증이 안되어 있다면 인증 페이지로 리다이렉트하게 됩니다. 따라서 인증이 되어 있다면 아래의 로직을, 인증이 안되어 있다면 구글 페이지로 이동합니다.

(5) 접속되었는지 확인합니다. 소셜 로그인의 경우 사용자가 로그인을 취소해 버리면 인증이 안될 수도 있으므로 반드시 authenticate 이후에는 isConnected로 검사해야 합니다.

(6) getUserProfile 메소드는 실시간으로 구글 API를 호출해서 로그인한 사용자의 정보를 가져옵니다.
다르게 가지고 올 수 있는 정보로는 getAccessToken 메소드가 있는데, 이는 접근 가능한 토큰을 나타냅니다. Hybridauth에서는 이 정보를 HYBRIDAUTH::STORAGE라는 이름으로 세션에서 관리합니다.

(7) identifier는 사용자 고유 식별자입니다.하나의 구글 계정당 하나의 고유 식별자가 생깁니다.

(8) displayName은 이름입니다. 구글은 이름을 입력받기는 하지만, 실명을 쓴다는 보장은 없으므로 그냥 보여지는 이름이라고 생각하면 됩니다.

(9) 데이터베이스에 유저가 있는지 검사합니다. identifiersocial_name을 동시에 조건으로 넘겨줌으로써 혹시나 모를 각 소셜 업체간 identifier가 겹치는 문제를 방지합니다. 예를 들어 A사용자의 구글 identifier1234인데, B사용자의 페이스북 identifier1234일 수도 있기 때문입니다.

(10) 이미 저장된 데이터가 없다면 사용자를 추가합니다. 굳이 마크다운 블로그에서 회원 정보를 저장하는 이유는, 각 회원이 글을 쓰는 시스템이기 때문입니다. 물론 이론상으로는 identifier + social_name 조합으로 주 키 역할을 대신할 수도 있습니다만, 관리의 번거로움이 따르기 때문에 회원 정보를 따로 관리합니다.
이런식으로 회원 정보를 따로 관리하면 만약 소셜 업체가 더이상 소셜 로그인을 지원하지 않는다거나, 혹은 계정 하나에 여러 소셜 업체의 로그인을 붙인다거나 하는 일을 유연하게 할 수 있습니다. 게다가 추가적으로 회원 이름을 소셜 업체와 다르게 한다거나, 소셜 업체에게는 없는 정보 - 예를 들어 github 주소라거나 하는 것도 관리할 수 있게 되죠.

(11) 이미 회원 정보가 있을 경우 세션에 넣기 위해 주 키를 획득합니다.

(12) 세션에 member_id를 넣습니다. 코드이그나이터4의 session() 객체를 사용하지 않고 순수 PHP 객체인 $_SESSION을 사용한 이유는 이미 Hybridauth에서 세션을 사용하기 위해 session_start()를 해 버렸기 때문입니다.
아시다시피 PHP에서 session_start()는 여러번 나올 수 없는데 이는 PHP에서 세션은 그저 임시 디렉토리에 저장되는 파일 묶음이어서 파일 스트림이 열려있기 때문이죠.
그래서 보통 세션 라이브러리들은 session_start()는 바로 하지 않고 if (isset($_SESSION) == false){session_start();}형식으로 사용하는데 코드이그나이터4의 개발자분들은 마음이 급했던 모양입니다.

(13) 만약 인증이 정상적으로 실행되지 않았으면 글 목록 페이지로 리다이렉트합니다.
현재 페이지로 리다이렉트를 해도 상관없지 않나..라고 생각하실 수도 있는데, 만약 현재 페이지로 다시 리다이렉트를 하게 되면 웹 브라우저 입장에서는 페이지 이동이기 때문에 뒤로가기를 눌러도 /oauth/google 에 접속되어 다시 인증 실패 로직을 탄 후 /oauth/google 페이지로 재이동합니다. 즉, 뒤로가기 버튼이 정상 작동하지 않는 무한 굴레에 빠져버리므로 현재 페이지에서 나갑니다.

(14) 로그아웃 엔드포인트입니다.

(15) 메소드 이름만 보면 구글에 뭔가 로그아웃 정보를 보내는 것 같지만 그렇지 않습니다. 대신 세션에 저장된 인증 정보를 삭제하죠. 세션에 저장된 인증 정보가 삭제되면 사용자의 데이터에 접근할 수 없기 때문에 다시한번 인증을 거쳐야 합니다.
사실 대부분의 소셜 로그인에 로그아웃이란 기능은 없습니다. 대신 소셜 로그인을 쓰는 서비스에서만 로그아웃되는 겁니다. 만약 하나의 브라우저에서 여러개의 서비스가 같은 소셜 로그인을 쓰고 있을 경우, 하나의 서비스에 로그아웃했다고 다른 서비스들도 로그아웃되어 버리면 곤란하니까요.

(16) 세션 정보가 있다면 세션 정보를 삭제합니다.
많은 웹 서비스는 세션에 정보가 있는가 여부로 로그인을 판단합니다. 최근에 앱과의 연동이나 프론트엔트 프레임워크 연동 등에서는 JWT등을 사용하기도 하지만, 일단 지금은 화면이 있는 서버사이드 렌더링 방식으로 개발하고 있으므로 그냥 세션을 사용하겠습니다.

소셜 로그인 확인하기

localhost:8080/oauth/google 에 접속해 봅시다. 처음에는 구글 인증 화면이 나올 겁니다.

계정을 선택하고 나면 http://localhost:8080/post로 다시 돌아올 겁니다.
이제 다시 localhost:8080/oauth/google에 접속해 보면, 구글에 인증하는 과정 없이 바로 http://localhost:8080/post로 돌아오는 것을 볼 수 있습니다.

정상적으로 데이터가 입력되어 있는지 PHPMyAdmin을 통해 확인해 보겠습니다.

profile
중년 아저씨. 10 + n년차 백엔드 개발자. 스타트업과 창업, 솔로프리너와 1인 기업에 관심 많아요.

2개의 댓글

comment-user-thumbnail
2021년 12월 10일

오!! 소셜 로그인까지 달고!! 아주 획기적인 기능입니다~ 얼른 진도 나가야징~

1개의 답글