본문으로 바로가기

728x90

파이어 베이스는 각종 앱을 쉽게 만들기 위해 제공되는 프로젝트 플랫폼입니다. OAuth2 로그인, 데이터베이스, 기계학습과 호스팅 등을 제공합니다. 파이어 베이스를 잘 활용하면 비용 없이 앱 서비스를 제공이 가능합니다. 또한, 구글에서 제공되는 다양한 확장 기능을 통합할 수 있습니다.

 

필자는 AWS 기반으로 개인적인 프로젝트를 진행한 적이 있는데 이때 서버의 운영비용이 개인적으로 부담하기에는 지불해야 할 비용이 높았습니다. 물론 제공되는 서버의 사양도 낮아서 간혹 메모리 부족으로 죽는 현상도 발생하였습니다.

 

이번 연재에서는 파이어 베이스 기반의 OAuth2 로그인과 데이터베이스를 사용하는 예제를 위주로 설명합니다. 그리고 추가적으로 카카오와 네이버의 OAuth2를 파이어 베이스에 확장 적용하는 방법에 대해서도 구현할 예정입니다.

 

연재는 아래와 같은 순서로 작업할 연재할 예정이며 순서는 달라질 수 있습니다.

 

프로젝트 생성

1. Vue + Firebase + Vuetify

 

인증

2. 전자우편/비밀번호

3. 전자우편 링크

4. 구글 로그인

5. 페이스북 로그인

6. 카카오톡 로그인

7. 네이버 로그인

 


이번 포스트에서는 Firebase의 전자우편 링크 로그인 기능을 구현합니다. 전자우편 링크는 실제 존재하는 전자우편인지 체크하는 기능과 암호의 관리가 불필요한 처리방식입니다. Firebase에서는 다음과 같이 장점을 설명하고 있습니다.

Firebase 인증을 사용하면 로그인 링크를 이메일로 전송해서 사용자가 바로 로그인하게 할 수 있습니다. 이 과정에서 사용자의 이메일 주소도 인증됩니다. 이메일로 로그인하는 경우 다음과 같은 많은 이점이 있습니다.
  • 편리한 가입 및 로그인
  • 여러 애플리케이션에서 비밀번호를 재사용할 위험이 적음: 재사용하면 아무리 보안등급이 높은 비밀번호라 해도 보안이 약화될 수 있음
  • 사용자를 인증하는 동시에 사용자가 이메일 주소의 합법적인 소유자인지 확인하는 기능
  • 액세스 가능한 이메일 계정만 있으면 로그인 가능: 전화번호 또는 소셜 미디어 계정 소유를 필요로 하지 않음
  • 사용자가 모바일 기기에서 번거롭게 비밀번호를 입력하거나 기억할 필요 없이 안전하게 로그인 가능
  • 이전에 이메일 식별자(비밀번호 또는 제휴)로 로그인한 기존 사용자는 이메일만 사용하여 로그인하도록 업그레이드 가능: 사용자가 비밀번호를 기억하지 못하더라도 비밀번호를 재설정하지 않고 계속 로그인 가능

 

Firebase 프로젝트 이메일 인증 설정하기

Firebase 프로젝트 설정 > Authentication > Sign-in method에서 이메일/비밀번호 수정을 선택합니다. 설정하는 팝업창에서 [이메일 링크(비밀번호가 없는 로그인)]을 사용 설정한 후 저장합니다. 사용하는 방법은 iOS, Android, 웹을 클릭하시면 적용가이드 문서로 연결됩니다.

이메일 링크 설정

 

Vue 프로젝트 수정

로그인 페이지 수정하기

암호가 필요 없으므로 암호 필드를 삭제합니다.

<template>
  <v-container
    class="fill-height"
    fluid
  >
    <v-row
      align="center"
      justify="center"
    >
      <v-col
        cols="12"
        sm="8"
        md="4"
      >
        <v-card class="elevation-12">
          <v-toolbar
            color="primary"
            dark
            flat
          >
            <v-toolbar-title>SignIn</v-toolbar-title>
            <v-spacer></v-spacer>
          </v-toolbar>
          <v-card-text>
            <v-form>
              <v-text-field
                label="Login"
                name="login"
                prepend-icon="mdi-account"
                type="text"
                v-model="email"
              ></v-text-field>
            </v-form>
          </v-card-text>
          <v-card-actions>
            <v-btn color="secondary" @click="$router.push('/signup')">SignUp</v-btn>
            <!-- <router-link to="/signup">SignUp</router-link> -->
            <v-spacer></v-spacer>
            <v-btn color="primary" @click="signin">SignIn</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

로그인 화면 예시

로그인 화면 예시

로그인 요청 시 Firebase는 입력한 전자우편에 인증 메일을 발송하게 됩니다. 이 전자우편을 통하여 로그인할 수 있습니다. firebase.auth().sendSignInLinkToEmail 함수를 사용하여 전자우편을 요청합니다. actionCodeSettings에서 url값은 전자우편 수신 후에 홈페이지로 다시 연결되는 주소 값입니다. 웹 앱에서는 반드시 처리해주어야 합니다. Vue Router 기능을 사용할 경우 모드는 history모드여야 합니다. history모드가 아닐 경우에는 회신 주소를 정상적으로 처리할 수 없습니다. 본 예제에서는 /callback/email로 처리하였습니다.

iOS, android, dynamicLinkDomain은 웹 앱에서는 필요하지 않습니다. 이 부분에 대한 자세한 내용은 Firebase 개발자 가이드 문서를 참고하십시오. 로그인 요청 후 네이티브 앱에서 로그인 처리를 위한 방법입니다. 즉, 메일 확인 후 링크가 바로 앱으로 연결되는 전달 값을 설정합니다.

<script>
import firebase from 'firebase'

export default {
  data: () => ({
      email : "",
      isSignin: false,
  }),
  created () {
    var user = firebase.auth().currentUser;
    console.log(user);

    // firebase.auth().onAuthStateChanged(function(user) {
    //   if (user) {
    //     // User is signed in.
    //     console.log(user);
    //   } else {
    //     // No user is signed in.
    //   }
    // });
    if(user) {
      this.isSignin = true;
    }
  },
  methods: {
    signin() {
      console.log('SignIn', this.email);

      if(!this.email) {
        alert('전자우편을 입력하여 주십시오.');
        return;
      }
      
      const protocol = location.protocol;
      const hostName = location.hostname;
      const port = location.port;

      let url = protocol + '//' + hostName + (port ? ':' + port : '');
      // url += '/#/signupfinish';
      url += '/callback/email';

      console.log(url);
      const actionCodeSettings = {
        // URL you want to redirect back to. The domain (www.example.com) for this
        // URL must be whitelisted in the Firebase Console.
        // url: 'https://www.example.com/finishSignUp?cartId=1234',
        url: url,
        // This must be true.
        handleCodeInApp: true,
        // iOS: {
        //   bundleId: 'com.example.ios'
        // },
        // android: {
        //   packageName: 'com.example.android',
        //   installApp: true,
        //   minimumVersion: '12',
        // },
        // dynamicLinkDomain: 'example.page.link',
      };

      const _this = this;
      firebase.auth().sendSignInLinkToEmail(this.email, actionCodeSettings)
      .then(function() {
        // The link was successfully sent. Inform the user.
        // Save the email locally so you don't need to ask the user for it again
        // if they open the link on the same device.
        window.localStorage.setItem('emailForSignIn', _this.email);
        alert("입력하신 전자우편으로 인증메일을 발송하였습니다. 전자우편을 통해서 로그인을 완료하시기 바랍니다.");
        _this.email = '';
      })
      .catch(function(error) {
        // Some error occurred, you can inspect the code: error.code
        console.log(error)
        // if(error.code === "auth/invalid-email") {}
        if(error && error.message) {
          alert(error.message);
        }
      });
    },
    signout() {
      firebase.auth().signOut().then(function() {
        // Sign-out successful.
      }).catch(function(error) {
        // An error happened.
        console.log(error);
      });
    }
  }
}
</script>

메일의 내용은 다음과 같습니다. 내용의 수정은 [프로젝트 설정 > Autentication > Template]에서 수정 가능합니다.

전자우편 예시

로그인 처리 페이지 추가하기

라우팅 경로 추가

/src/router/index.js 에 새로운 로그인 처리를 위한 페이지를 추가합니다.

  {
    path: '/callback',
    name: 'callback',
    redirect: '/signin',
    component: BlankContainer,
    children: [
      {
        path: 'email',
        name: 'callback-email',
        component: () => import('@/views/callback/EmailLink'),
      },
    ]
  },

vue-router 모드 설정

history모드가 아닐 경우에는 인증 주소를 정상적으로 처리할 수 없습니다. 즉, http://localhost:8080/#/callback/email 은 Firebase의 인증처리 후에는 연결될 수 없습니다. 모드를 변경하여 http://localhost:8080/callback/email 형태로 #이 없이 접속해야 합니다.

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

로그인 처리 페이지 추가

다음과 같이 로그인 처리 페이지를 추가합니다. 페이지에서는 웹브라우저에 저장된 전자우편주소가 없을 경우(예를 들어, 크롬에서 로그인한 뒤 인터넷 익스플로러에서 링크가 열린 상황)에는 전자우편 주소를 받도록 처리하였습니다. 

<template>
  <v-container
    class="fill-height"
    fluid
  >
    <v-row
      align="center"
      justify="center"
    >
      <v-col
        cols="12"
        sm="8"
        md="4"
      >
        <v-card class="elevation-12"
          v-if="!hasEmail"
        >
          <v-toolbar
            color="primary"
            dark
            flat
          >
            <v-toolbar-title>Email</v-toolbar-title>
            <v-spacer />
          </v-toolbar>
          <v-card-text>
            <v-form>
              <v-text-field
                label="Email"
                name="email"
                prepend-icon="mdi-account"
                v-model="email"
                type="text"
              ></v-text-field>
            </v-form>
          </v-card-text>
          <v-card-actions>
            <v-spacer />
            <v-btn color="primary"
              @click="execSignIn"
            >SignIn</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import firebase from 'firebase';

export default {
  data() {
    return {
      hasEmail : true,
      email: '',
    }
  },
  created() {
    const _this = this;
    // Confirm the link is a sign-in with email link.
    if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
      // Additional state parameters can also be passed via URL.
      // This can be used to continue the user's intended action before triggering
      // the sign-in operation.
      // Get the email if available. This should be available if the user completes
      // the flow on the same device where they started it.
      var email = window.localStorage.getItem('emailForSignIn');
      if (!email) {
        _this.hasEmail = false;
      } else {
        _this.email = email;
        _this.doSignIn();
      }

    } else {
      alert("잘못된 방법으로 접근하셨습니다. 로그인 페이지로 이동합니다.");
      _this.$router.push("/signin");
      return;
    }
  },
  methods: {
    execSignIn() {
      if(!this.email) {
        alert('전자우편을 입력하여 주십시오.');
        return;
      }

      this.doSignIn();
    },
    doSignIn() {
      const _this = this;
      const email = this.email;
      // The client SDK will parse the code from the link for you.
      firebase.auth().signInWithEmailLink(email, window.location.href)
      .then(function(result) {
        // Clear email from storage.
        window.localStorage.removeItem('emailForSignIn');
        // You can access the new user via result.user
        // Additional user info profile not available via:
        // result.additionalUserInfo.profile == null
        // You can check if the user is new or existing:
        // result.additionalUserInfo.isNewUser
        const user = result.user;
        console.log("User : ", user);
        if(result.additionalUserInfo.isNewUser) {
          console.log("신규 사용자...");
          _this.$router.push("/welcome");   // welcome
        } else {
          _this.$router.push("/profile");
        }
      })
      .catch(function(error) {
        // Some error occurred, you can inspect the code: error.code
        // Common errors could be invalid email and invalid or expired OTPs.
        console.log(error)
        if(error.code === "auth/invalid-action-code") {
          alert("정상적이지 않은 접근입니다. 만료된 데이터이거나 이미 사용된 데이터입니다.");
          _this.$router.push("/signin");
          return;
        }
        // if(error && error.message) {
        //   alert(error.message);
        // }
      });
    }
  }
}
</script>

 

회원 프로필 페이지 추가

vue-router에 로그인이 필요한 페이지 관리

......중간생략......
  {
    path: '/profile',
    name: 'profile',
    component: () => import('@/views/Profile'),
    meta: {
      requiresAuth: true
    }
  },
];

......중간생략......

router.beforeEach(async (to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth && !await firebase.auth().currentUser){
    next('signin');
  }else{
    next();
  }
});

회원 프로필 페이지 추가

간단한 인증이 필요한 페이지를 추가합니다.

<template>
  <div>
    Profile....
  </div>
</template>

인증이 필요한 페이지

글을 마치며

전자우편 발송요청 시에 콜백 주소를 다양화하여 로그인 처리가 가능합니다. 즉, 로그인과 회원가입처리가 분리하여 처리할 필요가 있을 경우에는 /signin/email과 /signup/email 콜백으로 나누어 처리하고 로그인 콜백은 로그인 처리를, 회원가입 콜백은 추가적인 정보를 입력받도록 구현이 가능합니다. 본 예제에서는 동일한 내용이라서 별도로 구현하지는 않았습니다.

다음 시간에는 로그인 정보를 vuex에 저장하는 방법과 세션 유지, 상태 변경 등에 관해서 글을 작성할 예정입니다.

 

참고자료

 

소스코드

본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.

 

728x90