尝试使用Angular和Firebase进行用户认证

这篇文章是Hands Lab 2017年的第10天文章。

大家好,这是第二次登场的手部实验室圣诞日历,我是大木志操。
这次我要介绍一下Firebase的实践主题。
当谈及Firebase时,大家可能都知道它作为实时数据库的知名度很高,但其实它还可以轻松构建用户认证基础设施。
在本文中,我将借助Angular官方的Firebase库AngularFire,为Angular应用实现用户认证功能。

这次要做的东西

在本文中,我們將使用Angular + AngularFire來創建下一個畫面。

    • ログイン画面

 

    • サインアップ画面

 

    トップページ(ユーザー情報)

另外,由于Firebase Authentication没有设置用户属性的功能,我希望同时在Cloud Firestore中进行用户信息管理。

前提 tí) – premise/condition/requisite

Note: The given word “前提” is already a Chinese word, so there is no need for paraphrasing.

    Firebaseのアカウントは取得済み(無料枠で可)

本次使用的环境

$ ng -v
...
Angular CLI: 1.5.4
Node: 9.2.0
OS: darwin x64
Angular: 5.0.3
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

@angular/cli: 1.5.4
@angular/flex-layout: 2.0.0-beta.10-4905443
@angular-devkit/build-optimizer: 0.0.33
@angular-devkit/core: 0.0.21
@angular-devkit/schematics: 0.0.37
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.8.4
@schematics/angular: 0.1.7
typescript: 2.4.2
webpack: 3.8.1

步骤

创建 Firebase 项目。

在Firebase控制台的首页上,点击“添加项目”,然后添加项目。

01.png

Firebase项目配置

身份验证

接下来,从左侧的“开发”菜单中选择“身份验证”,再从“用户”选项卡的登录方式设置按钮中进行登录提供商的设置。

02.png

本次我们尝试启用了通过邮件/密码登录和Google账号登录这两个选项。

03.png

最后,在屏幕右上方点击”网页设置”,复制并保存以下展示的代码片段的这一部分。

    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>'

数据库(云 Firestore)

接下来,从屏幕左侧的「DEVELOP」菜单中选择数据库,然后点击「试用FIRESTORE Beta版本」。

04.png

在显示选择安全规则的界面上,勾选“以测试模式启动”并点击“启用”。

05.png

以上就是Firebase的设置完成了。

使用Angular CLI创建模板

使用angular-cli创建应用程序的模板。
之后,计划添加一个根据是否经过身份验证来进行路由处理的功能,所以要使用–routing选项来运行。

$ ng new angular-firebase-auth --routing

当命令执行完毕后,将项目目录切换到当前位置。

$ cd angular-firebase-auth

安装所需的软件包。

我会安装Firebase和AngularFire。

$ npm install firebase angularfire2 --save

这次使用的是下一个版本。

    • angularfire2@5.0.0-rc.4

 

    firebase@4.6.2

项目的设置

我将进行配置,以便在项目中使用AngularFire。

将Firebase的设置添加到/src/environments/environment.ts文件中,该文件定义了环境变量。
apiKey、authDomain、ProjectId是从Firebase项目创建过程中复制的内容。

export const environment = {
  production: false,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    messagingSenderId: '<your-messaging-sender-id>'
  }
};

接下来,我们将编辑/src/app/app.module.ts文件如下。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

// 以下追加した項目
import { ReactiveFormsModule } from '@angular/forms';
import { AngularFireModule } from 'angularfire2';
import { AngularFireAuthModule } from 'angularfire2/auth';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { environment } from './../environments/environment';
import { AuthService } from './services/auth.service';
import { AuthGuard } from './guard/auth.guard';
import { UserLoginComponent } from './pages/user-login/user-login.component';
import { UserSignupComponent } from './pages/user-signup/user-signup.component';
import { UserInfoComponent } from './pages/user-info/user-info.component';

@NgModule({
  declarations: [
    AppComponent,
    UserLoginComponent,
    UserSignupComponent,
    UserInfoComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule, // 追加
    AngularFireModule.initializeApp(environment.firebase), // 追加
    AngularFireAuthModule, // 追加
    AngularFirestoreModule // 追加
  ],
  providers: [
    AuthService,
    AuthGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

除了主要的AngularFireModule外,我們還導入了AngularFireAuthModule以使用身份驗證功能。這還包括稍後將添加的服務和組件的導入,我們將在後面詳細介紹。

用户模型的定义

在前端端定义保存到Firestore的用户信息的类型定义。
保存到Firebase Authentication的是认证信息,保存到Firestore的是用户信息,这两者通过UID关联起来。
我们决定将模型(interface)放在app/models目录中。

执行以下命令创建接口。

$ ng g interface models/user

在/src/models/user.ts中定义属 性如下。

export interface User {
  uid: string;
  email: string;
  displayName?: string;
  photoURL?: string;
  profile?: string;
}

UID和电子邮件地址是必需的项目,其他的是可选的项目。
如果使用Google帐号登录,我们将稍后进行实现,从Google注册信息中获取displayName和photoURL。

创建用户认证服务

用户认证处理将被分解为服务。我决定将这个服务放置在app/services目录中。

执行以下命令创建认证服务的模板。

ng g service services/auth

将/src/services/auth.service.ts按照以下方式进行编辑。

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

// 以下追加したもの
import { AngularFireAuth } from 'angularfire2/auth';
import { AngularFirestore, AngularFirestoreDocument } from 'angularfire2/firestore';
import * as firebase from 'firebase/app';
import { Observable } from 'rxjs/Observable';
import { switchMap } from 'rxjs/operators';
import { User } from './../models/user';

@Injectable()
export class AuthService {
  user: Observable<User | null>;

  constructor(
    private router: Router,
    private afAuth: AngularFireAuth,
    private afStore: AngularFirestore
  ) {
    this.user = this.afAuth.authState
      .switchMap(user => {
        if (user) {
          return this.afStore.doc<User>(`users/${user.uid}`).valueChanges();
        } else {
          return Observable.of(null);
        }
      });
  }

  signUp(email: string, password: string) {
    return this.afAuth.auth.createUserWithEmailAndPassword(email, password)
      .then(user => {
        return console.log(user) || this.updateUserData(user);
      })
      .catch(err => console.log(err));
  }

  login(email: string, password: string): Promise<any> {
    return this.afAuth.auth.signInWithEmailAndPassword(email, password)
      .then(user => {
        return console.log(user) || this.updateUserData(user);
      })
      .catch(err => console.log(err));
  }

  googleLogin() {
    const provider = new firebase.auth.GoogleAuthProvider();
    return this.oAuthLogin(provider);
  }

  logout() {
    this.afAuth.auth.signOut()
      .then(() => {
        this.router.navigate(['/login']);
      });
  }

  private oAuthLogin(provider) {
    return this.afAuth.auth.signInWithPopup(provider)
      .then(credential => {
        console.log(credential.user);
        return this.updateUserData(credential.user);
      })
      .catch(err => console.log(err));
  }

  private updateUserData(user: User) {
    const docUser: AngularFirestoreDocument<User> = this.afStore.doc(`users/${user.uid}`);
    const data: User = {
      uid: user.uid,
      email: user.email,
      displayName: user.displayName || '',
      photoURL: user.photoURL || '',
      profile: user.profile || ''
    };
    return docUser.set(data);
  }
}

创建路由器保护

创建一个用于判断用户登录状态的守卫。

首先,执行以下命令创建guard的模板。

$ ng g guard guard/auth 

将app/guard/auth.guard.ts进行以下修改。

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';

// 以下追加したもの
import { Router } from '@angular/router';
import { AngularFireAuth } from 'angularfire2/auth';
import { map, take, tap } from 'rxjs/operators';
import { AuthService } from './../services/auth.service';


@Injectable()
export class AuthGuard implements CanActivate {

  constructor(
    private router: Router,
    private auth: AuthService
  ) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.auth.user.pipe(
      take(1),
      map(user => !!user), // userが取得できた場合はtrueを返す
      tap(loggedIn => {
        if (!loggedIn) {
          this.router.navigate(['/loggin']);
        }
      })
    );
  }
}

在每个方法中,我们使用AngularFire提供的身份验证API来执行操作。
结果可以通过Promise来接收,因此实现也会变得相对轻松。
另外,在执行注册和登录相关操作时,我们也会将用户信息写入数据库中。

创建登录组件 de

我們將創建一個顯示登入頁面的組件。

执行以下命令以创建组件的模板。

$ ng g component pages/user-login

班级一方面

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

// 以下追加したもの
import { ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from './../../services/auth.service';

@Component({
  selector: 'app-user-login',
  templateUrl: './user-login.component.html',
  styleUrls: ['./user-login.component.css']
})
export class UserLoginComponent implements OnInit {

  loginForm: FormGroup;

  constructor(
    private router: Router,
    private fb: FormBuilder,
    private auth: AuthService
  ) {
    this.auth.user.subscribe(user => {
      if (user !== null) {
        this.router.navigate(['/']);
      }
    });
  }

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

  login() {
    const email = this.loginForm.get('email').value;
    const password = this.loginForm.get('password').value;
    this.auth.login(email, password)
      .then(() => {
        this.router.navigate(['/']);
      });
  }

  googleLogin() {
    this.auth.googleLogin()
      .then(() => {
        this.router.navigate(['/']);
      });
  }
}

模板方面

<h1>ログイン</h1>

<button (click)="googleLogin()">Googleでログイン</button>
<button [routerLink]="['/signup']">新規登録</button>

<form [formGroup]="loginForm" (ngSubmit)="login()">
  <label for="email">Email</label>
  <input type="email" class="input" formControlName="email" required >
  <label for="password">Password</label>
  <input type="password" class="input" formControlName="password" required >
  <button type="submit" class="button">ログイン</button>
</form>

创建注册组件

我們將創建一個用於用戶註冊的組件。
這與登錄相同。

$ ng g component pages/user-singup

大部分与登录相同,因此已略去部分内容。

课堂方面

...
  signupForm: FormGroup;

  constructor(
    private router: Router,
    private fb: FormBuilder,
    private auth: AuthService
  ) { }

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

  signup() {
    const email = this.signupForm.get('email').value;
    const password = this.signupForm.get('password').value;
    this.auth.signUp(email, password)
      .then((x) => {
        this.router.navigate(['/']);
      });
  }
}

模板端

<h1>登録</h1>

<form [formGroup]="signupForm" (ngSubmit)="signup()">
  <label for="email">Email</label>
  <input type="email" class="input" formControlName="email" required >
  <label for="password">Password</label>
  <input type="password" class="input" formControlName="password" required >
  <button type="submit" class="button">登録</button>
</form>

创建用户信息组件

在登录后创建一个组件来显示用户信息。
在下文中的路由设置中,将访问路由重定向到该组件。

这也是与注册和登录一样的步骤。

$ ng g component pages/user-info

课堂一方

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

// 以下追加したもの
import { AuthService } from './../../services/auth.service';

@Component({
  selector: 'app-user-info',
  templateUrl: './user-info.component.html',
  styleUrls: ['./user-info.component.css']
})
export class UserInfoComponent implements OnInit {

  constructor(public auth: AuthService) { }

  ngOnInit() {
    this.auth.user.subscribe(user => {
      console.log(user);
    });
  }

  logout() {
    this.auth.logout();
  }
}

模板方面

<div *ngIf="auth.user | async as user">
  <h1>ようこそ {{user.displayName}}</h1>
</div>
<button (click)="logout()">ログアウト</button>

<div *ngIf="auth.user | async as user">
  <h2>ユーザー情報</h2>
  <p>プロフィール画像:</p>
  <img [src]="user.photoURL" style="width: 150px">
  <p>UID: {{user.uid}}</p>
  <p>名前: {{user.displayName}}</p>
  <p>Email: {{user.email}}</p>
  <p>プロフィール: {{user.profile}}</p>
</div>

设置路由

我們將在路由中添加在前一步驟中創建的user-login和user-info組件。
可能在創建項目時使用了–routing選項,所以在app文件夾下應該已經創建了一個名為app-routing.module.ts的文件。

将src/app/app-routing.module.ts文件进行如下编辑。

import { NgModule } from '@angular/core';
import { Routes, RouterModule, CanActivate } from '@angular/router';

// 以下追加したもの
import { UserLoginComponent } from './pages/user-login/user-login.component';
import { UserSignupComponent } from './pages/user-signup/user-signup.component';
import { UserInfoComponent } from './pages/user-info/user-info.component';
import { AuthGuard } from './guard/auth.guard';

const routes: Routes = [
  { path: '', redirectTo: '/userinfo', pathMatch: 'full' },
  { path: 'userinfo', component: UserInfoComponent, canActivate: [AuthGuard] },
  { path: 'login', component: UserLoginComponent },
  { path: 'signup', component: UserSignupComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

如果有访问路径,设置重定向到/userinfo。
此外,使用Guard的canActivate进行配置,限制只有经过身份验证才能访问的设置。

确认

我們在開發伺服器上啟動Angular應用程式,並驗證我們所建立的內容。

$ ng serve

从浏览器访问 http://localhost:4200。
如果显示登录页面,请点击”通过Google登录”并选择要使用的Google帐户。

成功登录后,我想会显示用户信息如下。

06_2.png

此外,我没有准备UI,但我想确认一下是否可以获取“个人资料”的值。
我想从Firebase管理界面尝试编辑登录用户的文档中的个人资料。

07.png
08.png

请注意

本次实施仅是进行了一个初步的尝试,所以对于表单验证、从库中获取响应的错误处理以及Firebase的权限设置等方面都进行了相当粗糙的处理。我认为在实际使用时,需要重新审视这些方面。

结束

以前,我使用AWS Cognito在Angular应用程序中实现了用户身份验证功能。但是,在配置身份验证基础设施和实现Angular方面,我觉得无论是哪种方式,使用Firebase都更方便简单。

在AWS Cognito中,可以默认设置用户属性和分组等功能,但在Firebase中,需要与Firestore结合使用。这一点需要注意。

实际构建应用程序时,除了用户身份验证,还需要后端API、数据库和静态文件托管等功能。根据所需的功能进行选择,我认为使用Angular + AngularFire的组合可以轻松构建SPA。

根据“Angular&Firebaseを使ってがっつりサーバーレスなWEBサービスを開発・運用したノウハウ”这篇文章,实际上,alclimb先生提出了不需要服务器端的观点。文章利用了“Angular”和“Firebase”来实现完全无服务器的Web服务。

明天是Hands Lab 2017年圣诞日历的第11天,我们有幸再次邀请到@watarukura先生分享✨

请参考一下

1. 安装和设置 – angularfire2
通过Angular2学习Firebase入门 – HTML5 Experts.jp

bannerAds