在 15 分钟内在您的 Angular 应用中实现 Google 登录(OAuth)🚀KittyGramAuth

2025-05-25

在 15 分钟内在您的 Angular 应用中实现 Google 登录(OAuth)🚀

KittyGramAuth

序言:我和我亲爱的朋友 Martina(她也是 Angular 和 Web 技术的 GDE)都非常喜欢猫。😻 有一天,我们正在讨论合作的事情,突然冒出一个非常疯狂(但很有趣)的想法。那就是创建KittyGram:一个超极简的 Instagram 克隆版,只允许上传猫咪🐱的照片。💡 很可爱,不是吗?

这是我们将创建KittyGram 的系列文章中的一篇文章,我们将使用世界上最好的前端 Web 框架(咳咳 Angular)来实现 KittyGram。

就本文的范围而言,我们将主要实现 Google 登录和路由。

本文要求您对 Angular、Angular Material 和 Firebase 有基本的了解。

实际上,我有一个YouTube 播放列表,您可以参考有关 Firebase 的内容,以防万一。

话虽如此,本文面向的是经验水平参差不齐的读者。为了以防万一你只对某个特定部分感兴趣,我还是在下面添加了一个TL;DR; 👇🏻。

TL;DR;

不喜欢读书?😵

啊!你跟我一样。🤗 我也觉得光看文字很难理解。别担心!我为你准备了一个演示这个应用的视频,并附有说明。

这段视频来自@PairAngular,这是一个非常棒的 YouTube 频道,我和@Martina会在这个频道上制作更多类似上面的结对编程视频。喜欢吗?订阅PairAngular YouTube 频道,以便随时了解未来更多类似的课程。🙂

项目概述🔭

由于这是一个极简版 Instagram 克隆版,我们不会真正关注 Instagram 的具体方面/功能。我们只需要让用户能够:

  • 通过 Google 登录并注销。
  • 发布一张猫的图片 - 创建。
  • 查看其他用户发布的猫图像并对其做出反应(任意次数) - FEED。

由于在本文中,我们仅关注实现 Google 登录,因此我们只会使用占位符图像来实现 CREATE 和 FEED 功能。

一旦这部分实现,它应该看起来并做出如下反应:

从上面的视频中您可以清楚地看到,该应用程序有不同的方面。

从 UI 角度来看,我们需要一个CreateComponent、一个FeedComponent、一个HeaderComponent和一个ProfileCardComponent。对于大多数这些组件,我们也将在这里使用 Angular Material。

从路由的角度来看,我们有一个/create路由,另一个/feed路由。我们也不想让未经授权的用户访问该/create路由。所以我们需要保护该/create路由。

你可能会问:“我们该如何实现 Google Sign-In 的想法?”处理 OAuth 流程、令牌生命周期以及与 Google API 的集成可能相当复杂。

思维!!!

但您不必担心独自管理所有这些事情。Google 已将所有这些复杂性隐藏起来,并将其公开为一项服务,我们只需付出很少的努力即可利用。这项服务名为Firebase Authentication,我们将在本应用中使用它。

好了!现在我们对整个应用程序及其各个部分有了大致的了解,让我们开始从头开始构建它吧。

设置 Firebase 项目🔥

要使用 Firebase 身份验证,我们需要设置一个 Firebase 项目。您需要一个 Firebase 帐户,只需使用您的 Google 帐户登录Firebase 控制台即可创建。

按照这个 1 分钟左右的短视频来设置 Firebase 项目:

复制并粘贴我们找到的这个配置。我们稍后会用到它。

太棒了!现在我们有了项目设置,让我们快速启用身份验证。请按照以下视频操作:

太棒了!现在我们已经完全设置好 Firebase 项目,并在其中启用了 Google 登录。现在让我们继续设置我们的 Angular 应用。

设置 Angular 应用

让我们从创建一个新的 Angular 应用开始。请确保您使用的是最新版本的 Angular CLI。

使用以下命令安装最新版本的 Angular CLI:

npm i -g @angular/cli@latest

安装完成后,您可以通过运行以下命令来验证您的版本ng --version

检查计算机上的 Angular CLI 版本

太棒了!现在让我们运行以下命令创建一个新的 Angular 项目:ng new KittyGram

使用 Angular CLI 创建新的 Angular 应用

让我们导航到刚刚创建的项目文件夹:(cd KittyGram在 Windows 上)

好了!现在,我们可以从项目概述部分的视频中清楚地看到,我们将使用Angular Material。因此,让我们设置我们的 Angular 应用以使用 Angular Material。

设置 AngularMaterial

设置@angular/material使用:

ng add @angular/material

它可能会询问你一些问题,例如你想使用的主题,是否设置字体和动画。只需按照下面的截图回答即可。

设置 Angular Material

完成后,我会稍微重构一下代码,以适应我通常喜欢的风格。顺便说一句,这只是我的个人意见,你其实不必这么做。

将主题路径从迁移angular.jsonstyles.scss

原因:考虑到我们已经有一个文件,我不希望angular.json被 CSS 文件污染styles.scss。所以我们可以直接导入这些 CSS 文件。

在 中搜索 deeppurple-amber.cssangular.json并删除其中的 URL。删除以下 URL:

"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",

重构之前,您应该能够找到两次此 URL 的出现。重构之后,styles您的数组angular.json应该如下所示:

{
  "...": "...",
  "projects": {
    "KittyGram": {
      "...": "...",
      "architect": {
        "build": {
          "...": "...",
          "options": {
            ...
            "styles": [
              "src/styles.scss"
            ],
            ...
          },
          ...
        },
        ...
        "test": {
          ...
          "options": {
            ...
            "styles": [
              "src/styles.scss"
            ],
            ...
          }
        },
        ...
      }
    }
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

现在将此 URL 移动到styles.scss。更改后,您的 URLstyles.scss应该如下所示:

/* You can add global styles to this file, and also import other style files */

html,
body {
  height: 100%;
}
body {
  margin: 0;
  font-family: Roboto, "Helvetica Neue", sans-serif;
}

@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Enter fullscreen mode Exit fullscreen mode

创建一个AppMaterialModule

我们已经在之前的一篇文章中讨论过这个问题了。所以我就不多说了。我只需要从BrowserAnimationsModule这里导出即可。

因此我们的AppMaterialModule看起来是这样的:

app-material.module.ts

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { NgModule } from '@angular/core';

@NgModule({
  exports: [
    BrowserAnimationsModule,
    MatButtonModule,
    MatDividerModule,
    MatIconModule,
    MatMenuModule,
    MatToolbarModule,
    MatTooltipModule,
    MatSnackBarModule,
  ],
})
export class AppMaterialModule {}
Enter fullscreen mode Exit fullscreen mode

下一步是设置AngularFire

设置 AngularFire

我们需要在 Angular 应用中使用 Firebase 来启用登录功能。Angular 提供了一个名为 的官方 Firebase SDK,@angular/fire我们可以使用它来实现这一点。接下来我们来操作一下。@angular/fire使用以下命令进行设置:

ng add @angular/fire

它可能会询问您一些问题,例如是否需要授予您收集用于分析、cli 使用情况和错误报告信息等的使用数据的权限。除此之外,它还可能会要求您粘贴授权码,然后它会打开一个弹出屏幕,让您登录到您用于注册 Firebase 的 Gmail 帐户。

然后,根据您是否能够成功粘贴授权码,它可能会要求您运行firebase login --reauth以为您执行此操作。

在成功的情况下,它可能会要求你从 Firebase 控制台上创建的 Firebase 项目列表中选择你的 Firebase 应用。像这样:

设置 Angular Fire

我们还需要firebaseJavaScript SDK。因此,让我们也使用以下命令安装它:

npm i firebase

完成后,我们会添加一些东西,让它符合我通常喜欢的风格。我们首先创建一个AppFirebaseModule

创建一个AppFirebaseModule

我将运行ng g m app-firebase并生成此模块。它将被创建在一个单独的文件夹中。因此,我将把它移出该文件夹并删除空文件夹。

现在,当我们设置 Firebase 项目时,我们也将配置复制到了某个地方。我们现在需要它。让我们将该配置存储在名为firebase和 文件的environments.ts对象中environments.prod.ts

environment.ts

export const environment = {
  production: false,
  firebase: {
    apiKey: 'YOUR apiKey HERE',
    authDomain: 'YOUR authDomain HERE',
    databaseURL: 'YOUR databaseURL HERE',
    projectId: 'YOUR projectId HERE',
    storageBucket: 'YOUR storageBucket HERE',
    messagingSenderId: 'YOUR messagingSenderId HERE',
    appId: 'YOUR appId HERE',
    measurementId: 'YOUR measurementId HERE',
  },
};
Enter fullscreen mode Exit fullscreen mode

注意:此配置应保密,不应公开共享。

现在AppFirebaseModule,让我们设置AngularFireGoogle 登录所需的模块。

因此,我们需要AngularFireModule用它来初始化 Angular 应用中的 Firebase 项目。此外,我们还需要AngularFireAuthModule包含登录和注销所需的所有辅助函数的。

我们调用initializeAppour 上的方法AngularFireModule,并将存储在 const 中的配置传递给它environment。然后我们可以从这里暴露AngularFireModuleAngularFireAuthModule在代码中,它看起来像这样:

app-firebase.module.ts

import { AngularFireAuthModule } from '@angular/fire/auth';
import { AngularFireModule } from '@angular/fire';
import { NgModule } from '@angular/core';

import { environment } from '../environments/environment';

@NgModule({
  imports: [AngularFireModule.initializeApp(environment.firebase)],
  exports: [AngularFireModule, AngularFireAuthModule],
})
export class AppFirebaseModule {}
Enter fullscreen mode Exit fullscreen mode

AngularMaterial现在我们已经完成了和的设置AngularFire,我们现在可以在我们的中导入AppFirebaseModuleAppMaterialModule模块AppModule

...
import { AppFirebaseModule } from './app-firebase.module';
import { AppMaterialModule } from './app-material.module';
...

@NgModule({
  ...
  imports: [
    ...
    AppFirebaseModule,
    AppMaterialModule,
    ...
  ],
  ...
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

实现 Angular 应用

实现 Angular 应用非常简单。就本文而言,我们需要四个组件:

  • HeaderComponent将是我们的主导航栏。它将包含登录、创建帖子和显示用户个人资料卡片的按钮。这些按钮将根据用户是否登录有条件地显示。
  • 个人资料卡组件,其中包含有关用户的一些详细信息和一个注销按钮。
  • 针对FeedCompoent路线的和针对路线的/feed类似CreateComponent/feed

让我们使用以下命令创建它们:

ng g c components/create --module=app && ng g c components/feed --module=app && ng g c components/profile-card --module=app && ng g c components/header --module=app

此命令将在名为的文件夹中为我们创建这 4 个组件components

我们只需要CreateComponent和 的占位符图片FeedComponent。我从Katerina LimpitsouniUndraw下载了这些图片。我们可以将它们存储在文件夹中,并在以下组件模板中链接它们:assets

create.component.html

<img alt="Create Post" class="placeholder-image" src="/assets/create.png" />
Enter fullscreen mode Exit fullscreen mode

feed.component.html

<img alt="Feed" class="placeholder-image" src="/assets/feed.png">
Enter fullscreen mode Exit fullscreen mode

在实现剩下的两个组件之前,我们需要一种方式来实现用户的登录和注销。AngularFire让这一切变得非常简单。它有一个AngularFireAuthModule模块,可以暴露AngularFireAuth我们可以作为依赖注入的服务。然后,我们可以调用它的方法来实现登录和注销。

除此之外,此服务还公开了一个authStateObservable,其中包含与当前登录用户相关的状态数据。因此,我们可以实现一个服务,它公开一些方法来帮助登录和注销,以及user$Observable。就像这样简单:

auth.service.ts

import { AngularFireAuth } from '@angular/fire/auth';
import { auth } from 'firebase/app';
import { BehaviorSubject, Observable, from } from 'rxjs';
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private user: BehaviorSubject<
    Observable<firebase.User>
  > = new BehaviorSubject<Observable<firebase.User>>(null);
  user$ = this.user
    .asObservable()
    .pipe(switchMap((user: Observable<firebase.User>) => user));

  constructor(private afAuth: AngularFireAuth) {
    this.user.next(this.afAuth.authState);
  }

  loginViaGoogle(): Observable<auth.UserCredential> {
    return from(this.afAuth.signInWithPopup(new auth.GoogleAuthProvider()));
  }

  logout(): Observable<void> {
    return from(this.afAuth.signOut());
  }
}
Enter fullscreen mode Exit fullscreen mode

这里的实现非常简单。如果有什么不明白的地方,欢迎在下面留言。

太棒了。现在我们有了一个可以作为依赖项注入到我们的 中的服务HeaderComponent。 它将HeaderComponent利用这些方法和user$Observable 在导航栏上显示相应的选项。我们还将利用从和方法subscribe返回的 Observable,将一些警报显示为 Snackbar。loginViaGooglelogout

既然我们要这么做,我们将使用take操作员来完成,这样我们就不必unsubscribe手动操作了。

header.component.ts

import { catchError, take } from 'rxjs/operators';
import { Component } from '@angular/core';
import { EMPTY, Observable, of } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';

import { AuthService } from '../../services/auth/auth.service';
import { FEED } from './../../consts/routes.const';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
  user$: Observable<firebase.User> = this.auth.user$;

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

  login() {
    this.auth
      .loginViaGoogle()
      .pipe(
        take(1),
        catchError((error) => {
          this.snackBar.open(`${error.message} 😢`, 'Close', {
            duration: 4000,
          });
          return EMPTY;
        }),
      )
      .subscribe(
        (response) =>
          response &&
          this.snackBar.open(
            `Oh! You're here. I demand that you feed me, Hooman. 😾`,
            'Close',
            {
              duration: 4000,
            },
          ),
      );
  }

  logout() {
    this.auth
      .logout()
      .pipe(take(1))
      .subscribe((response) => {
        this.router.navigate([`/${FEED}`]);
        this.snackBar.open('Come back soon with treats! 😿', 'Close', {
          duration: 4000,
        });
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

/feed实现也相当简单。用户退出后,我们还会明确地将用户导航到该路由。

注意:在实际项目中,我还会将小吃店消息移动到 const 文件中。

对于模板,如果用户已登录,我们将显示ProfileCardComponent和“创建”图标。否则,我们将向用户显示“登录”图标。

header.component.html

<mat-toolbar color="primary">
  <mat-toolbar-row>
    <button 
      mat-button 
      routerLink="/feed"
      matTooltip="🐱Gram Home">
      🐱Gram
    </button>
    <span class="spacer"></span>
    <ng-container *ngIf="user$ | async as user; else loginIcon">

      <button 
        mat-icon-button
        routerLink="/create"
        matTooltip="Post a cute 🐱"
        >
        <mat-icon
          aria-hidden="false"
          aria-label="Post a cute 🐱"
          >
          cloud_upload
        </mat-icon>
      </button>

      <app-profile-card 
        [user]="user"
        (logoutClick)="logout()">
      </app-profile-card>

    </ng-container>
    <ng-template #loginIcon>
      <button 
        mat-icon-button
        (click)="login()"
        matTooltip="Login"
        >
        <mat-icon
          aria-hidden="false"
          aria-label="Login"
          >
          fingerprint
        </mat-icon>
      </button>
    </ng-template>
  </mat-toolbar-row>
</mat-toolbar>
Enter fullscreen mode Exit fullscreen mode

正如您所见,我们使用了ProfileCardComponent作为子组件。这是一个展示组件,它接受user作为@Input属性,并在用户点击注销按钮时发出事件logoutClick @Output

所以我们的ProfileCardComponent样式看起来是这样的:

profile-card.component.ts

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-profile-card',
  templateUrl: './profile-card.component.html',
  styleUrls: ['./profile-card.component.scss'],
})
export class ProfileCardComponent {
  @Input() user: firebase.User;
  @Output() logoutClick: EventEmitter<null> = new EventEmitter<null>();

  logout() {
    this.logoutClick.emit();
  }
}
Enter fullscreen mode Exit fullscreen mode

模板看起来是这样的:

profile-card.component.html

<button
  mat-mini-fab
  color="primary"
  class="avatar-button"
  [matMenuTriggerFor]="beforeMenu"
>
  <img 
    [alt]="user.displayName"
    [src]="user.photoURL"
    class="avatar" />
</button>
<mat-menu #beforeMenu="matMenu" xPosition="before">
  <div class="profile-card">
    <img 
      [alt]="user.displayName"
      [src]="user.photoURL" 
      class="big-avatar" />
    <h4>{{ user.displayName }}</h4>
    <p>{{ user.email }}</p>
    <mat-divider></mat-divider>
    <button mat-stroked-button (click)="logout()">
      Sign Out
    </button>
    <mat-divider></mat-divider>
    <p class="profile-footer">
      Made with 😻 by <a href="https://twitter.com/SiddAjmera">@SiddAjmera</a>
    </p>
  </div>
</mat-menu>
Enter fullscreen mode Exit fullscreen mode

现在我们已经准备好了所有模块、组件和服务。让我们通过路由将它们整合在一起。

通过路由将所有内容整合在一起

为此,我们需要通过配置来设置路由AppRoutingModule。我们已经知道有两条路线:

  • /feed路线将引导用户到FeedComponent
  • /create路线将引导用户到CreateComponent

但是该/create路由不应该被未经授权的用户访问。如果没有AngularFire,我们会实现一个CanActivateGuard 来实现这一点。但多亏了@angular/fire,我们有一个AngularFireAuthGuard可以使用redirectUnauthorizedTo其辅助函数进行配置的守卫。配置它使我们能够告诉 Angular 将未经授权的用户导航到哪里。

所有这些在代码中看起来是这样的:

app-routing.module.ts

import {
  AngularFireAuthGuard,
  redirectUnauthorizedTo,
} from '@angular/fire/auth-guard';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { BASE, CREATE, FEED } from './consts/routes.const';
import { CreateComponent } from './components/create/create.component';
import { FeedComponent } from './components/feed/feed.component';

const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo([FEED]);

const routes: Routes = [
  {
    path: BASE,
    redirectTo: `/${FEED}`,
    pathMatch: 'full',
  },
  {
    path: FEED,
    component: FeedComponent,
  },
  {
    path: CREATE,
    component: CreateComponent,
    canActivate: [AngularFireAuthGuard],
    data: { authGuardPipe: redirectUnauthorizedToLogin },
  },
];

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

就这样!我们的 Angular 应用现在已经准备好支持 Google 登录和路由了。🎉✨

您可以在这里找到该项目的源代码:

GitHub 徽标 SiddAjmera / KittyGramAuth

这个存储库演示了我们在 KittyGram 中的身份验证和授权

KittyGramAuth

该项目是使用Angular CLI版本 9.0.5生成的

开发服务器

运行ng serve开发服务器。导航到http://localhost:4200/。如果您更改任何源文件,应用程序将自动重新加载。

代码脚手架

运行ng generate component component-name生成新组件。您也可以使用ng generate directive|pipe|service|class|guard|interface|enum|module

建造

运行ng build以构建项目。构建产物将存储在dist/目录中。请使用该--prod标志进行生产构建。

运行单元测试

运行以通过Karmang test执行单元测试

运行端到端测试

运行以通过Protractorng e2e执行端到端测试

进一步帮助

要获取有关 Angular CLI 的更多帮助,请使用ng help或查看Angular CLI README






下一步👣

KittyGram功能丰富,而这只是其中的一小部分。所有这些伟大的功能都源于一个不起眼的起点,就像我们刚刚构建的这个一样。在下一篇文章中,我们将实现CreateComponent一个Reactive Form表单。我们还将在其中实现Firebase 存储,以便将图片上传到 Firebase 存储桶。等不及了?就在这儿:

结束语🎉

这篇文章就到此结束了。感谢你的阅读。希望你喜欢。

非常感谢Martina Kraus的校对以及与我合作完成这个项目。我也非常感谢AkhilRajat 的校对,以及他们提供的建设性反馈,使文章更加完善​​。

希望本文能让你学到一些关于 Angular 和 Firebase 的新知识。如果觉得有用,请点击🧡/🦄图标,并将其添加到你的阅读列表 (🔖)。也欢迎分享这篇文章给你刚接触 Angular 并想实现类似目标的朋友。

另外,别忘了在这里关注 Martina:

那么下次再见。👋🏻

图标来源:AngularIO Press Kit |登录由Noun Project 的lastspark 提供

文章来源:https://dev.to/angular/implement-google-sign-in-oauth-in-your-angular-app-in-under-15-minutes-1ebo
PREV
Taiga UI:开源一年 开源 有什么新东西?即将推出什么?
NEXT
构建你的 Pokédex:第 1 部分 - NgRX 简介简介初始配置伪后端 NgRX 安装 Angular Material Pokemon 服务 NgRX 页面/视图题外话:样式结论更多,更多,更多……