[ Angular 2: Authentication ] firebase 사용자 인증 활용

프로젝트 설정

프로젝트 생성

$ ng new auth
$ cd auth

 컴포넌트 및 인터페이스 생성

$ cd src/app
$ mkdir protected
$ cd protected
$ ng g c protected --flat -is -it
$ cd ..

$ mkdir unprotected
$ cd unprotected
$ ng g c signin --flat -is -it
$ ng g c signup --flat -is -it
$ cd ..

아래처럼 노란색으로 표시된 파일들이 생성되었다.

자동 생성된 파일 수정

부트스트랩을 사용할 것이므로, index.html에 부트스트랩 CDN link를 추가 한다.

src/index.html

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>Auth</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
</head>

<body>
  <app-root>Loading...</app-root>
</body>

</html>

공유되어 사용될 페이지 헤더를 만든다.

src/app/shared/header.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-header',
  template: `
        <header>
            <nav class="navbar navbar-default">
                <div class="container-fluid">
        
                    <ul class="nav navbar-nav">
        
                        <li><a>Sign Up</a></li>
                        <li><a>Sign In</a></li>
                        <li><a>Protected</a></li>
        
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
        
                        <li><a>Logout</a></li>
                    </ul>
                </div><!-- /.container-fluid -->
        
            </nav>
        
        </header>
  `,
  styles: []
})
export class HeaderComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

User Object 를 정의하는 Interface 를 만든다.

src/app/shared/user.ts

export interface User {
    email: string;
    password: string;
    confirmPassword?: string;
}

로그인에 사용될 기본 폼을 만든다.

src/app/unprotected/signin.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from "@angular/forms";

@Component({
  selector: 'app-signin',
  template: `
        <h3>Please sign up to use all features</h3>
        <form [formGroup]="myForm" (ngSubmit)="onSignin()">
            <div class="input-group">
                <label for="email">E-Mail</label>
                <input formControlName="email" type="email" id="email">
            </div>
            <div class="input-group">
                <label for="password">Password</label>
                <input formControlName="password" type="password" id="password">
            </div>
            <button type="submit" [disabled]="!myForm.valid">Sign In</button>
        </form>
  `,
  styles: []
})
export class SigninComponent implements OnInit {
  myForm: FormGroup;
  error = false;
  errorMessage = '';

  constructor(private fb:FormBuilder) { }

  onSignin(){

  }

  ngOnInit():any {
    this.myForm = this.fb.group({
      email:['',Validators.required],
      password:['',Validators.required]
    });
  }

}

가입에 사용될 기본 폼을 만든다.

src/app/unprotected/signup.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from "@angular/forms";

@Component({
  selector: 'app-signup',
  template: `
        <h3>Please sign up to use all features</h3>
        <form [formGroup]="myForm" (ngSubmit)="onSignup()">
            <div class="input-group">
                <label for="email">E-Mail</label>
                <input formControlName="email" type="email" id="email" #email>
                <span *ngIf="!email.pristine && email.errors != null && email.errors['noEmail']">Invalid mail address</span>
                <!--<span *ngIf="email.errors['isTaken']">This username has already been taken</span>-->
            </div>
            <div class="input-group">
                <label for="password">Password</label>
                <input formControlName="password" type="password" id="password">
            </div>
            <div class="input-group">
                <label for="confirm-password">Confirm Password</label>
                <input formControlName="confirmPassword" type="password" id="confirm-password" #confirmPassword>
                <span *ngIf="!confirmPassword.pristine && confirmPassword.errors != null && confirmPassword.errors['passwordsNotMatch']">Passwords do not match</span>
            </div>
            <button type="submit" [disabled]="!myForm.valid">Sign Up</button>
        </form>
  `,
  styles: []
})
export class SignupComponent implements OnInit {
  myForm: FormGroup;
  error = false;
  errorMessage = '';

  constructor(private fb: FormBuilder) { }

  onSignup() {

  }

  ngOnInit():any {
    this.myForm = this.fb.group({
      email: ['', Validators.compose([
        Validators.required,
        this.isEmail
      ])],
      password: ['', Validators.required],
      confirmPassword: ['', Validators.compose([
        Validators.required,
        this.isEqualPassword.bind(this)
      ])]
    });
  }

  isEmail(control: FormControl): { [s: string]: boolean } {
    if (!control.value.match(/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/)) {
      return { noEmail: true };
    }
  }

  isEqualPassword(control: FormControl): { [s: string]: boolean } {
    if (!this.myForm) {
      return { passwordsNotMatch: true };

    }
    if (control.value !== this.myForm.controls['password'].value) {
      return { passwordsNotMatch: true };
    }
  }

}

비회원 경고 페이지 컴포넌트를 만든다.

src/app/protected/protected.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-protected',
  template: `
    <h1>Protected - you shouldn't be here if not signed in</h1>
  `,
  styles: []
})
export class ProtectedComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

데이터 기반의 폼(Reactive Form)을 사용했으므로 app.module.ts 에서 ReactiveFormsModule 을 import 해준다.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { SignupComponent } from './unprotected/signup.component';
import { SigninComponent } from './unprotected/signin.component';
import { ProtectedComponent } from './protected/protected.component';
import { HeaderComponent } from './shared/header.component';

@NgModule({
  declarations: [
    AppComponent,
    SignupComponent,
    SigninComponent,
    ProtectedComponent,
    HeaderComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

라우터 설정

app.routes.ts

import { Routes, RouterModule } from '@angular/router';
import { SignupComponent } from "./unprotected/signup.component";
import { SigninComponent } from "./unprotected/signin.component";
import { ProtectedComponent } from "./protected/protected.component";

export const APP_ROUTES: Routes = [
    { path: '', redirectTo: '/signup', pathMatch: 'full' },
    { path: 'signup', component: SignupComponent },
    { path: 'signin', component: SigninComponent },
    { path: 'protected', component: ProtectedComponent }
];

export const routing = RouterModule.forRoot(APP_ROUTES);

app.module.ts

.....
import { routing } from "./app.routes";
.....
@NgModule({
.....
.....
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ReactiveFormsModule,
    routing
  ],
.....
.....

app.component.html

<app-header></app-header>
<div class="container">
  <div class="row">
    <div class="col-md-10 col-md-offset-1">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

src/app/shared/header.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-header',
  template: `
        <header>
            <nav class="navbar navbar-default">
                <div class="container-fluid">
        
                    <ul class="nav navbar-nav">
        
                        <li><a [routerLink]="['signup']">Sign Up</a></li>
                        <li><a [routerLink]="['signin']">Sign In</a></li>
                        <li><a [routerLink]="['protected']">Protected</a></li>
        
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
        
                        <li><a>Logout</a></li>
                    </ul>
                </div><!-- /.container-fluid -->
        
            </nav>
        
        </header>
  `,
  styles: []
})
export class HeaderComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

 

Firebase 백앤드 설정

먼저 파이어베이스 사이트에 가입하고 오른쪽 상단 console (콘솔) 링크를 클릭하여 콘솔창으로 이동한다. ‘새 프로젝트 만들기’ 버튼을 클릭하고, 프로젝트 이름과 국가를 정하여 입력한다.

auth 프로젝트가 생성되었다. Authentication 을 클릭하여 들어간다.

‘로그인 방법 설정’ 버튼을 누른다.

로그인 방법 탭에서 ‘이메일/비밀번호’를 선택한다.

다음과 같은 팝업이 뜨면 사용설정을 ‘On’으로 놓고 저장한다.

사용 설정이 완료되었다.

사용자 탭을 클릭하면 다음과 같이 사용자를 추가/삭제/수정/검색 할 수 있는 리스트 페이지가 열린다.

오른쪽 상단에 ‘문서로 이동’을 클릭하고 문서창이 열리고, ‘플랫폼별 Firebase’>’웹’>’시작하기 가이드’ 로 링크를 따라 들어가면, 적용방법에 대한 자세한 설명을 볼 수 있다. 한번 읽어보는 것이 좋다. 여기서는 이 페이지는 건너뛴다.

위 이미지와 같은 페이지에서 오른쪽 상단의 ‘웹 설정’을 클릭하면 다음과  같은 팝업이 뜨는 데, 이 자바스크립트 코드를 src/index.html 에 붙여넣게 하면 된다.

src/index.html

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>Auth</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
</head>

<body>
  <app-root>Loading...</app-root>

  <script src="https://www.gstatic.com/firebasejs/3.7.2/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: "AIzaSyDcOP87ZOL6pnQ7iIpUCnSND0qJlcl7LoQ",
      authDomain: "auth-ea99c.firebaseapp.com",
      databaseURL: "https://auth-ea99c.firebaseio.com",
      storageBucket: "auth-ea99c.appspot.com",
      messagingSenderId: "769895545368"
    };
    firebase.initializeApp(config);

  </script>
  
</body>

</html>

이것으로 Firebase 상에서 설정은 끝났다. 보다 상세한 사항을 체크해보자.

Firebase 사용자 관리하기 페이지로 가서 createUserWithEmailAndPassword 를 클릭한다.

아래 ‘비밀번호 기반 계정으로 Firebase 에 인증하기‘ 페이지에 설명된 내용을 활용하면 된다.

새 사용자 추가

사용자 관리를 위한 Service 를 하나 생성해본다. 위 ‘비밀번호 기반 계정으로 Firebase 에 인증하기‘ 페이지에서 ‘비밀번호 기반 계정생성하기’ 아래 나와있는 코드를 그대로 가져와 약간 수정했다. user interface 를 적용해 email, password 부분을 user Object에서 뽑아 쓴다.

$ cd src/app/shared
$ ng generate service auth

src/app/shared/auth.service.ts

import { Injectable } from '@angular/core';
import { User } from "./user";
// src/index.html 에서 추가해주었던 firebase script 를 선언해준다.
declare var firebase:any;

export class AuthService {
    signupUser(user: User) {
        firebase.auth().createUserWithEmailAndPassword(user.email, user.password)
        .catch(function (error) {
            // Handle Errors here.
            //var errorCode = error.code;
            //var errorMessage = error.message;
            // ...
            console.log(error);
        });
    }
}

app.module.ts 에 AuthService를 등록해준다.

src/app/app.module.ts

....
....
import { AuthService } from "./shared/auth.service";

@NgModule({
  ....
  ....
  providers: [AuthService],
  ....
})
....

다음은 처음에 만들어 두었던 SignupComponent의 비어있는 onSignup  메서드를 완성해주어야한다.

scr/app/unprotected/signup.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from "@angular/forms";
import { AuthService } from "../shared/auth.service";

@Component({
....
....
....
})
export class SignupComponent implements OnInit {
....
....
  constructor(private fb: FormBuilder,private authService:AuthService) { }

  onSignup() {
    this.authService.signupUser(this.myForm.value);
  }

  ngOnInit():any {
....
....

}

실제로 새 사용자를 입력해보자.

http://localhost:4200/signup

firebase 사용자 리스트를 확인해보면, 새로운 사용자가 추가되었다.

사용자 로그인

비밀번호 기반 계정으로 Firebase 에 인증하기‘ 페이지에서 ‘이메일 주소와 비밀번호로 사용자 로그인 처리하기’ 를 참조한다.

나와있는 코드를 복사하여 AuthService 에 singinUser 메서드 안에 붙여넣는다.

src/app/shared/auth.service.ts

import { Injectable } from '@angular/core';
import { User } from "./user";
declare var firebase: any;

export class AuthService {
    signupUser(user: User) {
    ....
    ....
    }

    signinUser(user: User) {
        firebase.auth().signInWithEmailAndPassword(user.email, user.password)
            .catch(function (error) {
                console.log(error);
                // Handle Errors here.
                // var errorCode = error.code;
                // var errorMessage = error.message;
                // ...
            });
    }
}

SigninComponent 에 AuthService 를 Injection 하고, onSignin 메서드에서 호출한다. 이때 폼의 user 값을 넘겨준다.

src/app/unprotected/signin.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from "@angular/forms";
import { AuthService } from "../shared/auth.service";

@Component({
....
....
})
export class SigninComponent implements OnInit {
  .....
  .....

  constructor(private fb:FormBuilder,private authService:AuthService) { }

  onSignin(){
    this.authService.signinUser(this.myForm.value);
  }

  ngOnInit():any {
  .....
  .....
  }

}

로그인 확인

로그인 되었는지 확인해보자. ‘Firebase에서 사용자 관리하기‘ 페이지에서 ‘사용자 프로필 가져오기’ 섹션의 두번째 코드를 활용한다. AuthService 에 isAuthenticated 메서드를 생성한다.

src/app/shared/auth.service.ts

import { Injectable } from '@angular/core';
import { User } from "./user";
declare var firebase: any;

export class AuthService {
    signupUser(user: User) {
        .....
        .....
    }

    signinUser(user: User) {
        .....
        .....
    }

    isAuthenticated() {
        var user = firebase.auth().currentUser;

        if (user) {
            // User is signed in.
            return true;
        } else {
            // No user is signed in.
            return false;
        }
    }
}

위 isAuthenticated 메서드를 활용해서 HeaderComponent 의 Logout 버튼이 로그인 되어있을 때만 보이도록 해보자. HeaderComponent 에 isAuth 메서드를 만들어 AuthService의 isAuthenticated 메서드를 불러들인다.

src/app/shared/header.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from "./auth.service";

@Component({
....
....
                    <ul class="nav navbar-nav navbar-right" *ngIf="isAuth()">
        
                        <li><a>Logout</a></li>
                    </ul>
....
....
})
export class HeaderComponent implements OnInit {

  constructor(private authService:AuthService) { }

  ngOnInit() {
  }

  isAuth() {
    return this.authService.isAuthenticated();
  }

}

결과적으로 로그인을 해보면 오른쪽 상단의 Logout 버튼이 보인다. 로그인 되었다는 표시다.

로그아웃 기능 구현

로그아웃 버튼이 로그인 되었을 때만 보이는 기능을 구현해보았다. 이번에는 로그아웃 버튼을 눌렀을 때, 로그아웃이 되도록 해보자.

AuthService 에 logout 메서드를 추가하자.

src/app/shared/auth.service.ts

import { Injectable } from '@angular/core';
import { User } from "./user";
declare var firebase: any;

export class AuthService {
    signupUser(user: User) {
        ....
        ....
    }

    signinUser(user: User) {
        ....
        ....
    }

    logout(){
        firebase.auth().signOut();
    }

    isAuthenticated() {
        ....
        ....
    }
}

HeaderComponent 에서 로그아웃 버튼에 클릭 리스너 및 해당 메서드(onLogout)를 추가해주어야한다.

src/app/shared/header.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from "./auth.service";

@Component({
....
....
                    <ul class="nav navbar-nav navbar-right" *ngIf="isAuth()">
        
                        <li><a (click)="onLogout()" style="cursor:pointer;">Logout</a></li>
                    </ul>
....
....
})
export class HeaderComponent implements OnInit {

  constructor(private authService:AuthService) { }

  ngOnInit() {
  }

  isAuth() {
    return this.authService.isAuthenticated();
  }

  onLogout(){
      this.authService.logout();
  }

}

로그인 후에 로그아웃 버튼을 누르면, 로그아웃 버튼이 사라지는 것을 확인할 수 있다. 로그아웃 기능이 구현되었다.

비회원 접근 막기

비회원 접근을 막기 위해서 Router 기능인 canActivate 를 사용한다. protected 버튼을 눌렀을 때, protected 페이지를 로그인한 경우만 보여주도록 해보자.

먼저 guard 파일을 생성한다. 다음과 같이 하면, auth.guard.ts 파일이 생성된다.

$ cd src/app/shared
$ ng generated guard auth

app.module.ts 의 provider 항목에 AuthGurard 를 추가해준다.

....
....
import { AuthGuard } from "./shared/auth.guard";

@NgModule({
....
....
  providers: [AuthService,AuthGuard],
....
....
})
export class AppModule { }

app.routes.ts 에 ‘protected’ route에 canActivate를 추가해준다.

.....
.....
.....
import { AuthGuard } from "./shared/auth.guard";

export const APP_ROUTES: Routes = [
    { path: '', redirectTo: '/signup', pathMatch: 'full' },
    { path: 'signup', component: SignupComponent },
    { path: 'signin', component: SigninComponent },
    { path: 'protected', component: ProtectedComponent, canActivate:[AuthGuard] }
];
.....

이렇게 하면 기본 작업 준비 완료. 다음은 자동 생성된 auth.guard.ts 필요에 맞게 고쳐줄 차례다.

src/app/shared/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from "./auth.service";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) { }
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isAuthenticated();
  }
}

 

ng serve  명령으로 실행해본다.

http://localhost:4200

로그아웃된 상태에서는 Protected 버튼을 아무리 눌러도 해당 페이지가 열리지 않는 것을 확인할 수 있다.

마무리

마지막으로 로그아웃을 했을 때, protected 페이지가 닫히고 초기화면으로 돌아가도록 설정해보자. Router 의 navigate 메서드를 사용해서, logout 메서드가 실행될 때, 초기페이지로 자동전환되게 한다. AuthService에 Router를 import하고 injection 하자.

src/app/shared/auth.service.ts

import { Injectable } from '@angular/core';
import { User } from "./user";
import { Router } from "@angular/router";
declare var firebase: any;

export class AuthService {
    constructor(private router:Router){}

    signupUser(user: User) {
        .....
        .....
    }

    signinUser(user: User) {
        .....
        .....
    }

    logout(){
        firebase.auth().signOut();
        this.router.navigate(['/signin']);
    }

    isAuthenticated() {
        .....
    }
}

이렇게 하면 로그아웃 후에는 protected 페이지가 닫히고 열리지 않는다.

댓글 남기기