firebase auth
本文最初发布在Auth0.com博客上 ,并经许可在此处重新发布。
在这个分为两部分的系列教程中,我们将学习如何构建一个使用Auth0身份验证保护Node后端和Angular前端安全的应用程序。 我们的服务器和应用程序还将使用自定义令牌对Firebase Cloud Firestore数据库进行身份验证,以便用户在使用Auth0登录后可以安全方式留下实时评论。
可以在angular-firebase GitHub存储库中找到Angular应用程序代码,在firebase-auth0-nodeserver存储库中找到Node API。
使用Auth0对Firebase和Angular进行身份验证:第1部分
本教程的第1部分将介绍:
- Firebase和Auth0
- 我们将建立什么
- 角度CLI
- Auth0客户端和API
- 具有服务帐户的Firebase项目
- 节点API
- 设置Angular应用
- Angular应用架构
- 实施共享模块
- 实现路由和延迟加载模块
- 加载和错误组件
- 验证逻辑
- 核心逻辑
- 下一步
Firebase和Auth0
Firebase是一个移动和Web应用程序开发平台。 Firebase于2014年被Google收购,并将继续在Google的领导下进行开发。 Firebase提供了NoSQL数据库( RTDB或Realtime Database and Cloud Firestore,在撰写本文时为beta版),该 数据库托管在云中,并使用Web套接字进行连接以向应用程序提供实时功能。
Auth0是基于云的平台,提供身份验证和授权即服务。 作为身份验证提供程序,Auth0使开发人员可以轻松地实现和自定义其应用程序的登录和授权安全性。
选择Auth0 + Firebase身份验证
如果您已经熟悉Firebase的产品,您可能会问:为什么我们要在Firebase中使用自定义令牌实现Auth0,而不是坚持使用Firebase的内置身份验证 ?
首先,在这里有一个重要的区别。 使用Auth0保护Firebase并不意味着您不使用Firebase身份验证。 Firebase具有自定义身份验证方法 ,允许开发人员将其首选身份解决方案与 Firebase身份验证集成。 这种方法使开发人员能够实施Firebase身份验证,以便它与专有系统或其他身份验证提供程序无缝运行。
我们可能想将Auth0与Firebase身份验证集成的潜在原因有很多。 另外,在某些情况下,仅使用基本的Firebase身份验证就足够了。 让我们来探索。
如果您满足以下条件,则可以单独使用Firebase的内置身份验证 :
- 只想对Firebase RTDB或Firestore进行身份验证,而无需对其他后端进行身份验证
- 只需要少量的登录选项,不需要企业标识提供程序,与您自己的用户存储数据库集成等。
- 不需要大量的用户管理,配置文件扩充等,并且可以通过API严格地管理用户
- 无需自定义身份验证流程
- 无需遵守有关用户数据存储的合规性法规。
如果您执行以下操作,则应考虑将Auth0与自定义Firebase令牌结合使用:
- 已经实现了Auth0,并希望向您的应用添加实时功能
- 需要轻松使用发行的令牌来保护 Firebase 不提供的后端
- 需要整合社交身份提供者 ,而不仅仅是Google,Facebook,Twitter和GitHub
- 需要集成企业身份提供程序 ,例如Active Directory,LDAP,ADFS,SAMLP等。
- 需要定制的身份验证流程
- 需要通过API 和易于管理的仪表板进行强大的用户管理
- 希望能够动态丰富用户个人资料
- 想要可自定义的无密码登录 , 多因素身份验证 , 违反密码安全性 , 异常检测等功能。
- 必须遵守HIPAA,GDPR,SOC2等合规性法规 。
本质上,如果您有一个非常简单的应用程序具有基本的身份验证需求,并且仅使用Firebase数据库,则Firebase的基本身份验证提供程序就足够了。 但是,如果您还需要更多, Firebase提供了一种将其服务与其他身份验证解决方案一起使用的好方法 。 这是许多开发人员都将面对的更为现实的情况,因此我们将在此处详细探讨。
我们将建立什么
我们将构建一个使用Auth0保护的Node.js API,该API会铸造自定义Firebase令牌,并返回十种不同犬种的数据。
我们还将构建一个名为“ Popular Dogs”的Angular前端应用程序,该应用程序显示有关2016年十种最受欢迎的狗的信息,该信息由美国养犬俱乐部(AKC)按公众受欢迎程度排名。 我们的应用将通过Auth0保护,调用Node API来获取狗数据,并调用API获取Firebase令牌以授权用户使用Cloud Firestore实时添加和删除评论。 该应用程序将使用共享模块以及实现延迟加载。
要实施该应用程序,您将需要以下内容:
- 角度CLI
- 具有客户端和API配置的免费Auth0帐户
- 具有服务帐户的免费Firebase项目
让我们开始吧!
角度CLI
确保在本地计算机上安装了带有NPM的Node.js。 运行以下命令以全局安装Angular CLI :
$ npm install -g @angular/cli@latest
我们将使用CLI生成Angular应用及其几乎所有架构。
Auth0客户端和API

免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
您需要一个Auth0帐户来管理身份验证。 您可以在此处注册一个免费帐户 。
接下来,设置Auth0客户端应用程序和API,以便Auth0可以与Angular应用程序和Node API交互。
设置Auth0客户端
- 转到您的Auth0信息中心 ,然后点击创建新客户端按钮。
- 为您的新应用命名(例如
Angular Firebase
),然后选择单页Web应用 。 - 在新的Auth0客户端应用的设置中,将
http://localhost:4200/callback
到允许的回调URL中 。 - 启用使用Auth0(而不是IdP)进行单点登录的切换。
- 在“ 设置”部分的底部,单击“显示高级设置”。 选择OAuth选项卡,并验证JsonWebToken签名算法是否设置为“ RS256”。
- 如果需要,您可以建立一些社交关系 。 然后,可以在“ 连接”选项卡下的“ 客户端”选项中为您的应用启用它们。 上面的屏幕快照中显示的示例使用用户名/密码数据库,Facebook,Google和Twitter。
注意:对于生产,请确保您设置了自己的社交密钥,并且不要将社交连接设置为使用Auth0开发密钥。
设置Auth0 API
- 转到Auth0信息中心中的API ,然后单击“创建API”按钮。 输入API的名称,例如
Firebase Dogs API
。 将标识符设置为您的API端点URL。 在本教程中,我们的API标识符为http://localhost:1337/
。 签名算法应为“ RS256”。 - 您可以在新API设置的“ 快速入门”选项卡下查阅Node.js示例。 在接下来的步骤中,我们将使用Express , express-jwt和jwks-rsa以这种方式实现Node API。
现在,我们准备在Angular客户端和Node后端API上实现Auth0身份验证。
具有服务帐户的Firebase项目
接下来,您将需要一个免费的Firebase项目。
创建Firebase项目
- 转到Firebase控制台,然后使用您的Google帐户登录。
- 单击添加项目 。
- 在弹出的对话框中,为您的项目命名(例如
Angular Firebase Auth0
)。 将根据您选择的名称生成一个项目ID。 然后,您可以选择您的国家/地区。 - 单击创建项目按钮。
生成管理员SDK密钥
为了创建自定义的Firebase令牌 ,您需要访问Firebase Admin SDK 。 要获得访问权限,您必须在新的Firebase项目中创建一个服务帐户。
单击Firebase控制台侧栏中项目概述旁边的齿轮图标,然后从出现的菜单中选择项目设置 :
在设置视图中,单击“ 服务帐户”选项卡。 将显示Firebase Admin SDK UI,其中显示配置代码段。 默认情况下,选择Node.js。 这是我们想要的技术,我们将在Node API中实现它。 单击生成新私钥按钮。
将出现一个对话框,警告您秘密存储私钥。 我们将永远不要将这个密钥签入公共存储库。 单击Generate Key按钮,将密钥下载为.json
文件。 我们将很快将此文件添加到我们的Node API。
节点API
可以在firebase-auth0-nodeserver GitHub存储库中找到本教程的完整Node.js API。 让我们学习如何构建此API。
节点API文件结构
我们将要设置以下文件结构:
firebase-auth0-nodeserver/
|--firebase/
|--.gitignore
|--<your-firebase-admin-sdk-key>.json
|--.gitignore
|--config.js
|--dogs.json
|--package.json
|--routes.js
|--server.js
您可以使用命令行生成必要的文件夹和文件,如下所示:
$ mkdir firebase-auth0-nodeserver
$ cd firebase-auth0-nodeserver
$ mkdir firebase
$ touch firebase/.gitignore
$ touch .gitignore
$ touch config.js
$ touch dogs.json
$ touch package.json
$ touch routes.js
$ touch server.js
Firebase Admin SDK密钥和Git忽略
现在,将您先前下载的Firebase Admin SDK .json
密钥文件移动到firebase
文件夹中。 我们会确保文件夹已签入,但绝对不会使用firebase/.gitignore
将其内容推送到存储库中,如下所示:
# firebase/.gitignore
*
*/
!.gitignore
此.gitignore
配置可确保Git忽略firebase
目录中的所有文件和文件夹, 除了 .gitignore
文件本身。 这使我们可以提交(基本上)空的文件夹。 我们的.json
Firebase Admin SDK密钥可以存在于此文件夹中,我们不必担心通过filename忽略它。
注意:如果我们将项目拉到多台计算机上并且生成了不同的密钥(具有不同的文件名),这将特别有用。
接下来,我们为根目录的.gitignore
添加代码:
# .gitignore
config.js
node_modules
狗的JSON数据
接下来,我们将添加十个犬种的数据。 为简便起见,您可以简单地将此数据复制并粘贴到dogs.json
文件中。
依存关系
让我们像这样添加我们的package.json
文件:
{
"name": "firebase-auth0-nodeserver",
"version": "0.1.0",
"description": "Node.js server that authenticates with an Auth0 access token and returns a Firebase auth token.",
"repository": "https://github.com/auth0-blog/firebase-auth0-nodeserver",
"main": "server.js",
"scripts": {
"start": "node server"
},
"author": "Auth0",
"license": "MIT",
"dependencies": {},
"devDependencies": {}
}
我们将使用命令行安装依赖项,最新版本将自动保存到package.json
文件中:
$ npm install --save body-parser cors express express-jwt jwks-rsa firebase-admin
我们需要body-parser
, cors
和express
来提供我们的API端点。 身份验证将依赖于express-jwt
和jwks-rsa
,而Firebase令牌铸造是通过firebase-admin
SDK(我们将使用生成的密钥进行访问)实现的。
组态
在config.js
文件中,添加以下代码,并将占位符值替换为您自己的设置:
// config.js
module.exports = {
AUTH0_DOMAIN: '<Auth0 Domain>', // e.g., you.auth0.com
AUTH0_API_AUDIENCE: '<Auth0 API Audience>', // e.g., http://localhost:1337/
FIREBASE_KEY: './firebase/<Firebase JSON>', // e.g., your-project-firebase-adminsdk-xxxxx-xxxxxxxxxx.json
FIREBASE_DB: '<Firebase Database URL>' // e.g., https://your-project.firebaseio.com
};
服务器
有了我们的数据,配置和依赖关系,我们现在就可以实现我们的Node服务器。 打开server.js
文件并添加:
// server.js
// Modules
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
// App
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());
// Set port
const port = process.env.PORT || '1337';
app.set('port', port);
// Routes
require('./routes')(app);
// Server
app.listen(port, () => console.log(`Server running on localhost:${port}`));
这将使用http://localhost:1337/
Express启动我们的Node服务器。
注意:请注意,这是我们在Auth0中设置的API标识符。
API路由
接下来打开routes.js
文件。 这是我们定义API端点,保护它们和铸造自定义Firebase令牌的地方。 添加以下代码:
// routes.js
// Dependencies
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
// Config
const config = require('./config');
module.exports = function(app) {
// Auth0 athentication middleware
const jwtCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
audience: config.AUTH0_API_AUDIENCE,
issuer: `https://${config.AUTH0_DOMAIN}/`,
algorithm: 'RS256'
});
// Initialize Firebase Admin with service account
const serviceAccount = require(config.FIREBASE_KEY);
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(serviceAccount),
databaseURL: config.FIREBASE_DB
});
// GET object containing Firebase custom token
app.get('/auth/firebase', jwtCheck, (req, res) => {
// Create UID from authenticated Auth0 user
const uid = req.user.sub;
// Mint token using Firebase Admin SDK
firebaseAdmin.auth().createCustomToken(uid)
.then(customToken =>
// Response must be an object or Firebase errors
res.json({firebaseToken: customToken})
)
.catch(err =>
res.status(500).send({
message: 'Something went wrong acquiring a Firebase token.',
error: err
})
);
});
// Set up dogs JSON data for API
const dogs = require('./dogs.json');
const getDogsBasic = () => {
const dogsBasicArr = dogs.map(dog => {
return {
rank: dog.rank,
breed: dog.breed,
image: dog.image
}
});
return dogsBasicArr;
}
// GET dogs (public)
app.get('/api/dogs', (req, res) => {
res.send(getDogsBasic());
});
// GET dog details by rank (private)
app.get('/api/dog/:rank', jwtCheck, (req, res) => {
const rank = req.params.rank * 1;
const thisDog = dogs.find(dog => dog.rank === rank);
res.send(thisDog);
});
};
在较高级别,我们的路由文件执行以下操作:
- 设置身份验证检查,以确保只有登录用户才能使用
jwtCheck
中间件访问路由 - 使用从Firebase项目服务帐户生成的私钥初始化Firebase Admin SDK。
- 提供返回自定义Firebase令牌的安全
GET
端点 - 提供一个公共
GET
*端点,该端点返回dogs数据的简短版本 - 提供一个安全的
GET
*端点,该端点返回按等级要求的特定狗的详细数据。
*端点使用相同基本数据集的变体来模拟更复杂的API。
您可以阅读代码注释以获取更多详细信息。
提供API
您可以通过运行以下命令来提供Node API:
$ node server
然后,该API将在http:// localhost:1337可用。
注意:如果尝试在浏览器中访问安全路由,则应收到401 Unauthorized
错误。
这就是我们的服务器! 保持API保持运行状态,以便Angular应用可以访问它,我们将在下一步中对其进行设置。
设置Angular应用
现在是时候创建我们的Angular应用程序并设置一些其他依赖项了。
创建新的Angular应用
您应该早先已经安装了Angular CLI 。 现在,我们可以使用CLI生成我们的项目及其架构。 要创建一个新应用,请选择一个包含文件夹,然后运行以下命令:
$ ng new angular-firebase --routing --skip-tests
--routing
标志生成具有路由模块的应用程序,而--skip-tests
生成不具有.spec.ts
文件的根组件。
注意:为简便起见,本文将不涉及测试。 如果您想了解有关在Angular中进行测试的更多信息,请查看教程的结论以获取更多资源。
安装前端依赖项
现在,让我们安装前端依赖项:
$ cd angular-firebase
$ npm install --save auth0-js@latest firebase@latest angularfire2@latest
我们将需要auth0-js
库在Angular应用中实现Auth0身份验证。 我们还需要在firebase
JS SDK和angularfire2
角火力地堡库来实现我们的火力地堡的实时评论。
添加Bootstrap CSS
为了简化样式,我们将Bootstrap CSS CDN链接添加到index.html
文件的<head>
,如下所示:
<!-- src/index.html -->
...
<head>
...
<title>Top 10 Dogs</title>
...
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
</head>
...
服务角度应用
您可以使用以下命令为Angular应用提供服务:
$ ng serve
该应用程序将在浏览器中的http:// localhost:4200上运行 。
Angular应用架构
我们将使用Angular CLI为我们的应用程序预先生成完整的架构。 这样,我们可以在实现逻辑和模板之前确保模块正常运行。
我们的应用将使用延迟加载的模块化方法 。 本教程中的示例应用程序很小,但是我们希望以可扩展的,真实的方式构建它。
根模块
使用ng new
命令生成Angular应用时,已经创建了根模块。 根模块位于src/app/app.module.ts
。 我们在Angular应用程序中生成的任何未指定其他模块子目录的组件都将自动导入并在我们的根模块中声明。
现在,使用CLI生成一个组件:
# create CallbackComponent:
$ ng g component callback --is --it --flat --no-spec
该命令由以下内容组成:
-
ng g component
:生成具有以下内容的callback
组件文件: -
--is
内联样式 -
--it
内联模板 -
--flat
不包含文件夹 -
--no-spec
否.spec
测试文件
用户登录到我们的应用程序后,我们将使用回调组件来处理重定向。 这是一个非常简单的组件。
注意: g
是generate
的快捷方式。 我们还可以使用c
作为component
的快捷方式,使此命令成为ng gc
。 但是,为了清楚起见,本教程将不对生成的文件类型使用快捷方式。
核心模块架构
接下来,我们将创建CoreModule
及其组件和服务。 这是一个共享模块。 在Angular项目文件夹的根目录中,运行以下CLI命令。 请确保您运行ng g module core
的第一个命令,就像这样:
# create Core module:
$ ng g module core
# create API service with no .spec file:
$ ng g service core/api --no-spec
# create HeaderComponent with inline styles, no .spec file, and export in module:
$ ng g component core/header --is --no-spec --export=true
# create LoadingComponent with inline styles, inline template, no folder, no .spec file, and export in module:
$ ng g component core/loading --is --it --flat --no-spec --export=true
# create ErrorComponent with inline styles, inline template, no folder, no .spec file, and export in module:
$ ng g component core/error --is --it --flat --no-spec --export=true
# create Dog type interface:
$ ng g interface core/dog
# create DogDetail type interface:
$ ng g interface core/dog-detail
首先创建模块可确保在该模块的文件夹中创建的组件随后将被导入并自动在该父模块(而不是应用程序的根模块)中声明。
注意:如果要在另一个模块中使用共享模块的组件,则需要export
组件并声明它们。 我们可以使用--export=true
标志通过CLI自动执行此操作。
这是我们的应用程序需要访问的共享核心服务,组件和模型的基本架构。
身份验证模块架构
接下来,我们将创建AuthModule
。 执行以下CLI命令(同样,请确保首先生成模块):
# create Auth module:
$ ng g module auth
# create AuthService with no .spec file:
$ ng g service auth/auth --no-spec
# create Auth route guard with no .spec file:
$ ng g guard auth/auth --no-spec
我们的Auth
模块提供了管理身份验证所需的服务和路由防护,但没有任何组件。 这也是一个共享模块。
狗模块架构
我们的应用程序的主页将由DogsModule
提供。 根据AKC的排名,这将是2016年十只最受欢迎的狗的名单。 使用以下CLI命令来为该延迟加载的页面模块生成结构:
# create Dogs module:
$ ng g module dogs
# create DogsComponent with inline styles and no .spec file:
$ ng g component dogs/dogs --is --no-spec
狗模块架构
我们的应用程序还将为Dogs组件中列出的每条狗提供详细页面,以便用户可以了解有关每种品种的更多信息。 使用以下CLI命令来为延迟加载的DogModule
生成结构:
# create Dog module:
$ ng g module dog
# create DogComponent with inline styles and no .spec file:
$ ng g component dog/dog --is --no-spec
注释模块架构
最后,我们需要实现Firebase实时注释所需的架构。 使用下面的CLI命令生成的结构CommentsModule
:
# create Comments module:
$ ng g module comments
# create Comment model class:
$ ng g class comments/comment
# create CommentsComponent with no .spec file:
$ ng g component comments/comments --no-spec --export=true
# create CommentFormComponent with inline styles and no .spec file:
$ ng g component comments/comments/comment-form --is --no-spec
环境配置
让我们将Auth0和Firebase的配置信息添加到Angular前端。 打开environment.ts
文件并添加:
// src/environments/environment.ts
const FB_PROJECT_ID = '<FIREBASE_PROJECT_ID>';
export const environment = {
production: false,
auth: {
clientId: '<AUTH0_CLIENT_ID>',
clientDomain: '<AUTH0_DOMAIN>', // e.g., you.auth0.com
audience: '<AUTH0_API_AUDIENCE>', // e.g., http://localhost:1337/
redirect: 'http://localhost:4200/callback',
scope: 'openid profile email'
},
firebase: {
apiKey: '<FIREBASE_API_KEY>',
authDomain: `${FB_PROJECT_ID}.firebaseapp.com`,
databaseURL: `https://${FB_PROJECT_ID}.firebaseio.com`,
projectId: FB_PROJECT_ID,
storageBucket: `${FB_PROJECT_ID}.appspot.com`,
messagingSenderId: '<FIREBASE_MESSAGING_SENDER_ID>'
},
apiRoot: '<API URL>' // e.g., http://localhost:1337/ (DO include trailing slash)
};
用适当的Auth0,Firebase和API信息替换<angle brackets>
占位符。
您可以在您为本教程创建的客户端和API的设置的Auth0仪表板中找到Auth0配置。
单击标有“ 将Firebase添加到您的Web应用程序”的大图标后,您可以在Firebase控制台项目概述中找到Firebase配置,如下所示:
添加加载图片
开始在Angular应用中实现功能之前,我们要做的最后一件事是添加加载图像。 创建以下文件夹: src/assets/images
。
然后将此加载的SVG图像保存到该文件夹中:
实施共享模块
让我们设置我们的模块。 我们将在根AppModule
导入共享模块( CoreModule
和AuthModule
)。
核心模块
首先,我们将实现我们的CoreModule
。 打开core.module.ts
文件并更新为以下代码:
// src/app/core/core.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { DatePipe } from '@angular/common';
import { HeaderComponent } from './header/header.component';
import { ApiService } from './api.service';
import { LoadingComponent } from './loading.component';
import { ErrorComponent } from './error.component';
@NgModule({
imports: [
CommonModule,
RouterModule,
HttpClientModule, // AuthModule is a sibling and can use this without us exporting it
FormsModule
],
declarations: [
HeaderComponent,
LoadingComponent,
ErrorComponent
],
exports: [
FormsModule, // Export FormsModule so CommentsModule can use it
HeaderComponent,
LoadingComponent,
ErrorComponent
]
})
export class CoreModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
Title,
DatePipe,
ApiService
]
};
}
}
由于这是一个共享模块,因此我们将导入在整个应用程序中需要访问的其他模块,服务和组件。
注意: CommonModule
导入到 不是根模块的所有模块中 。
在我们的imports
数组中,我们将在CoreModule
添加服务或组件可能需要的任何模块,或者将这些模块提供给应用程序中的其他模块。 CLI应该已经自动将所有生成的组件添加到了declarations
数组。 exports
数组应包含我们要提供给其他模块的任何模块或组件。
请注意,我们已从@angular/core
导入ModuleWithProviders
。 使用此模块,我们可以创建一个forRoot()
方法,当导入CoreModule
时,可以在根app.module.ts
中的导入时调用该方法。 这样,我们可以确保添加到由forRoot()
方法返回的providers
数组中的所有服务在我们的应用程序中保持单例 。 这样,如果我们应用中的其他模块也需要导入CoreModule
,我们可以避免意外的多个实例。
验证模块
接下来让我们添加一些代码,我们AuthModule
在auth.module.ts
文件:
// src/app/auth/auth.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
import { AngularFireAuthModule } from 'angularfire2/auth';
@NgModule({
imports: [
CommonModule,
AngularFireAuthModule
]
})
export class AuthModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: AuthModule,
providers: [
AuthService,
AuthGuard
]
};
}
}
我们将导入ModuleWithProviders
以实现与CoreModule
的forRoot()
方法。 然后,我们将导入AuthService
和AuthGuard
。 我们还需要从angularfire2/auth
导入AngularFireAuthModule
,以便我们可以在AuthService
保护Firebase连接。 然后,应在forRoot()
方法的providers
数组中返回服务和防护。
评论模块
打开comments.module.ts
文件以实现CommentsModule
如下所示:
// src/app/comments/comments.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '../core/core.module';
import { environment } from './../../environments/environment';
import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { CommentsComponent } from './comments/comments.component';
import { CommentFormComponent } from './comments/comment-form/comment-form.component';
@NgModule({
imports: [
CommonModule,
CoreModule, // Access FormsModule, Loading, and Error components
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule
],
declarations: [
CommentsComponent,
CommentFormComponent
],
exports: [
CommentsComponent
]
})
export class CommentsModule { }
我们需要导入CoreModule
以便可以利用其导出的FormsModule
, LoadingComponent
和ErrorComponent
。 我们还需要从environment.ts
文件访问我们的配置。 评论使用火力地堡的云数据库的FireStore,让我们导入AngularFireModule
和AngularFirestoreModule
以及我们的两个组成部分: CommentsComponent
和CommentFormComponent
。
当我们将AngularFireModule
添加到@NgModule的imports
数组时,我们将调用其initializeApp()
方法,并传入Firebase配置。 我们的两个组件都应该已经在declarations
数组中,并且CommentsComponent
应该已经添加到了exports
数组中,以便其他模块中的其他组件可以使用它。
注意:我们不需要导出CommentsFormComponent
因为它是CommentsComponent
的子级。
CommentsModule
不提供任何服务,因此无需实现forRoot()
方法。
应用模块
现在,我们的CoreModule
, AuthModule
和CommentsModule
已经实现,我们需要导入我们的根模块中,上述AppModule
地处app.module.ts
文件:
// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { CoreModule } from './core/core.module';
import { AuthModule } from './auth/auth.module';
import { CommentsModule } from './comments/comments.module';
import { AppComponent } from './app.component';
import { CallbackComponent } from './callback.component';
@NgModule({
declarations: [
AppComponent,
CallbackComponent
],
imports: [
BrowserModule,
AppRoutingModule,
CoreModule.forRoot(),
AuthModule.forRoot(),
CommentsModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
CLI已自动添加了AppComponent
和CallbackComponent
。 当我们将CoreModule
和AuthModule
添加到imports
数组时,我们将调用forRoot()
方法以确保没有为其服务创建额外的实例。 CommentsModule
不提供任何服务,因此该模块无需担心。
实现路由和延迟加载模块
我们有两个需要路由的模块: DogsModule
用于狗的主要列表)和DogModule
,其中包含显示犬种详细信息页面的组件。
应用程序路由
首先,让我们实现应用程序的路由。 打开app-routing.module.ts
文件并添加以下代码:
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CallbackComponent } from './callback.component';
import { AuthGuard } from './auth/auth.guard';
const routes: Routes = [
{
path: '',
loadChildren: './dogs/dogs.module#DogsModule',
pathMatch: 'full'
},
{
path: 'dog',
loadChildren: './dog/dog.module#DogModule',
canActivate: [
AuthGuard
]
},
{
path: 'callback',
component: CallbackComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
我们将导入CallbackComponent
和AuthGuard
。 其余路由将是对模块的字符串引用 ,而不是使用loadChildren
属性导入的组件。
我们将设置默认的''
路径来从DogsModule
加载路由DogsModule
,而'dog'
路径来从DogModule
加载路由DogModule
。 'dog'
路径也应受到AuthGuard
保护,我们使用canActivate
属性对其进行了声明。 如果我们需要不止一个,这可以容纳一系列的路由卫士。 最后, 'callback'
路由应仅指向CallbackComponent
。
狗模块
让我们向dogs.module.ts
文件添加一些代码:
// src/app/dogs/dogs.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { CoreModule } from '../core/core.module';
import { CommentsModule } from '../comments/comments.module';
import { DogsComponent } from './dogs/dogs.component';
const DOGS_ROUTES: Routes = [
{
path: '',
component: DogsComponent
}
];
@NgModule({
imports: [
CommonModule,
CoreModule,
RouterModule.forChild(DOGS_ROUTES),
CommentsModule
],
declarations: [
DogsComponent
]
})
export class DogsModule { }
除了CoreModule
和CommentsModule
外,我们RouterModule
导入Routes
和RouterModule
(注释将显示在主要的dogs列表页面上)。
这个模块有一个子路由,因此我们将创建一个常量,该常量包含一个数组来保存我们的路由对象。 独生子女的路线,我们需要继承''
从路径app-routing.module.ts
,所以它的路径也应该是''
。 它将加载DogsComponent
。 在imports
数组中,我们将DOGS_ROUTES
常量传递给RouterModule
的forChild()
方法。
狗模块
该DogModule
的工作方式与同样DogsModule
以上。 打开dog.module.ts
并添加以下内容:
// src/app/dog/dog.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { CoreModule } from '../core/core.module';
import { DogComponent } from './dog/dog.component';
const DOG_ROUTES: Routes = [
{
path: ':rank',
component: DogComponent
}
];
@NgModule({
imports: [
CommonModule,
CoreModule,
RouterModule.forChild(DOG_ROUTES)
],
declarations: [
DogComponent
]
})
export class DogModule { }
该模块与DogsModule
之间的DogsModule
是我们的DOG_ROUTES
的路径为:rank
。 这样,任何特定狗的详细信息的路由都会作为URL段传递,该URL段与我们在十大犬种列表中的犬排名相匹配,如下所示:
http://localhost:4200/dog/3
另一个区别是,我们将不会导入CommentsModule
。 但是,如果需要,我们将来可以在狗的详细信息中添加评论。
我们的应用程序的架构和路由现已完成! 该应用程序应成功编译并显示在浏览器中,并且延迟加载功能可以正确加载共享代码和请求的特定路由的代码。
现在,我们准备实现应用程序的逻辑。
加载和错误组件
加载和错误组件是基本的核心UI元素,可以在我们的应用程序中的许多不同位置使用。 现在设置它们。
加载组件
LoadingComponent
应该只显示一个加载图像。 (回想一下,我们已经保存一个,当我们建立我们的应用程序的体系结构)。但是,它应该是能够显示图像大和中心, 或小和内联。
打开loading.component.ts
文件并添加:
// src/app/core/loading.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-loading',
template: `
<div [ngClass]="{'inline': inline, 'text-center': !inline, 'py-2': !inline }">
<img src="/assets/images/loading.svg">
</div>
`,
styles: [`
.inline {
display: inline-block;
}
img {
height: 80px;
width: 80px;
}
.inline img {
height: 24px;
width: 24px;
}
`]
})
export class LoadingComponent {
@Input() inline: boolean;
}
使用@Input()
装饰器 ,我们可以将信息从其父级传递到组件中,告诉它是否应该内联显示该组件。 我们将在模板中使用NgClass指令 ( [ngClass]
)有条件地为我们想要的显示添加适当的样式。 在另一个模板中显示该组件将如下所示:
<!-- Large, full width, centered: -->
<app-loading></app-loading>
<!-- Inline: -->
<app-loading inline="true"></app-loading>
错误成分
接下来,让我们快速实现我们的ErrorComponent
。 如果显示该组件,将显示一条简单的错误消息。 打开error.component.ts
文件并添加:
// src/app/core/error.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-error',
template: `
<p class="alert alert-danger">
<strong>Error:</strong> There was an error retrieving data.
</p>
`
})
export class ErrorComponent {
}
验证逻辑
现在,让我们实现使AuthModule
的功能正常工作所需的代码。 为了在CoreModule
构建标头,我们将需要身份验证服务,因此从这里开始是有意义的。 我们已经安装了必要的依赖项(Auth0和FirebaseAuth),所以让我们开始吧。
认证服务
在编写任何代码之前,我们将确定该服务的要求。 我们要:
- 创建一个
login()
方法,该方法将允许用户使用Auth0进行身份验证 - 如果提示用户通过尝试访问受保护的路由来登录,请确保在成功通过身份验证后将其重定向到该路由
- 获取用户的个人资料信息并设置他们的会话
- 建立让应用知道用户是否已登录的方法
- 通过Auth0访问令牌的授权,从API请求Firebase自定义令牌
- 如果成功获取Firebase令牌,请使用返回的令牌登录Firebase,并为应用建立一种方法来了解用户是否已登录Firebase
- Firebase铸造的自定义令牌会在一小时后过期,因此我们应该设置一种方法来自动续订过期的令牌
- 创建一个
logout()
方法以清除会话并logout()
Firebase。
打开我们之前生成的auth.service.ts
文件。
为了简化教程,请在GitHub repo的auth.service.ts
文件中查看完整代码。
发生了很多事情,所以让我们逐步进行一下。
首先,与往常一样,我们将导入依赖项。 这包括我们environment
配置之前设置为广大Auth0,火力地堡和API设置,以及auth0
和firebase
库, AngularFireAuth
, HttpClient
调用API来获取自定义火力地堡的令牌,以及必要的RxJS进口。
您可以参考代码注释,以获取有关AuthService
类的私有和公共成员的AuthService
。
接下来是构造函数,在该函数中,我们可以在类中使用Router
, AngularFireAuth
和HttpClient
。
login()
方法如下所示:
login(redirect?: string) {
// Set redirect after login
const _redirect = redirect ? redirect : this.router.url;
localStorage.setItem('auth_redirect', _redirect);
// Auth0 authorize request
this._auth0.authorize();
}
如果将redirect
URL段传递到方法中,我们会将其保存在本地存储中。 如果没有传递重定向,我们将只存储当前URL。 然后,我们将使用在成员中创建的_auth0
实例,并调用Auth0的authorize()
方法转到Auth0登录页面,以便我们的用户可以进行身份验证。
接下来的三个方法是handleLoginCallback()
, getUserInfo()
和_setSession()
:
handleLoginCallback() {
this.loading = true;
// When Auth0 hash parsed, get profile
this._auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken) {
window.location.hash = '';
// Store access token
this.accessToken = authResult.accessToken;
// Get user info: set up session, get Firebase token
this.getUserInfo(authResult);
} else if (err) {
this.router.navigate(['/']);
this.loading = false;
console.error(`Error authenticating: ${err.error}`);
}
});
}
getUserInfo(authResult) {
// Use access token to retrieve user's profile and set session
this._auth0.client.userInfo(this.accessToken, (err, profile) => {
if (profile) {
this._setSession(authResult, profile);
} else if (err) {
console.warn(`Error retrieving profile: ${err.error}`);
}
});
}
private _setSession(authResult, profile) {
// Set tokens and expiration in localStorage
const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + Date.now());
localStorage.setItem('expires_at', expiresAt);
this.userProfile = profile;
// Session set; set loggedIn and loading
this.loggedIn = true;
this.loading = false;
// Get Firebase token
this._getFirebaseToken();
// Redirect to desired route
this.router.navigateByUrl(localStorage.getItem('auth_redirect'));
这些方法是不言自明的:它们使用Auth0方法parseHash()
和userInfo()
来提取身份验证结果并获取用户的个人资料 。 我们还将设置服务的属性以存储必要的状态(例如,用户的身份验证状态是否正在加载以及是否已登录),处理错误,将数据保存到我们的服务和本地存储中,以及重定向到适当的位置路线。
我们还将使用身份验证结果的访问令牌来向我们的API授权HTTP请求以获取Firebase令牌。 这是通过_getFirebaseToken()
和_firebaseAuth()
方法完成的:
private _getFirebaseToken() {
// Prompt for login if no access token
if (!this.accessToken) {
this.login();
}
const getToken$ = () => {
return this.http
.get(`${environment.apiRoot}auth/firebase`, {
headers: new HttpHeaders().set('Authorization', `Bearer ${this.accessToken}`)
});
};
this.firebaseSub = getToken$().subscribe(
res => this._firebaseAuth(res),
err => console.error(`An error occurred fetching Firebase token: ${err.message}`)
);
}
private _firebaseAuth(tokenObj) {
this.afAuth.auth.signInWithCustomToken(tokenObj.firebaseToken)
.then(res => {
this.loggedInFirebase = true;
// Schedule token renewal
this.scheduleFirebaseRenewal();
console.log('Successfully authenticated with Firebase!');
})
.catch(err => {
const errorCode = err.code;
const errorMessage = err.message;
console.error(`${errorCode} Could not log into Firebase: ${errorMessage}`);
this.loggedInFirebase = false;
});
}
我们将创建一个从GET
请求到我们的API的/auth/firebase
端点的可观察到的getToken$
并订阅它。 如果成功,我们会将带有自定义Firebase令牌的返回对象传递给_firebaseAuth()
方法,该方法将使用Firebase的signInWithCustomToken()
方法向Firebase进行身份验证。 此方法返回一个Promise,当Promise被解决后,我们可以告诉我们的应用程序Firebase登录成功。 我们还可以安排Firebase令牌续订(我们稍后会介绍)。 我们将适当地处理所有错误。
我们的自定义Firebase令牌将在3600
秒(1小时)内到期。 这仅是我们默认的Auth0访问令牌生存期(即7200
秒或2个小时)的一半 。 为避免用户在会话过程中意外失去对Firebase的访问权限,我们将使用以下两种方法设置Firebase令牌自动续订: scheduleFirebaseRenewal()
和unscheduleFirebaseRenewal()
。
注意:您也可以使用checkSession()
方法以类似的方式使用checkSession()
实现自动会话更新。 此外,如果用户离开应用程序,然后稍后返回,则可以使用checkSession()
在构造函数中还原未到期的身份验证会话。 在本教程中,我们不会涉及到这一点,但是您应该自己尝试一下!
scheduleFirebaseRenewal() {
// If user isn't authenticated, check for Firebase subscription
// and unsubscribe, then return (don't schedule renewal)
if (!this.loggedInFirebase) {
if (this.firebaseSub) {
this.firebaseSub.unsubscribe();
}
return;
}
// Unsubscribe from previous expiration observable
this.unscheduleFirebaseRenewal();
// Create and subscribe to expiration observable
// Custom Firebase tokens minted by Firebase
// expire after 3600 seconds (1 hour)
const expiresAt = new Date().getTime() + (3600 * 1000);
const expiresIn$ = Observable.of(expiresAt)
.pipe(
mergeMap(
expires => {
const now = Date.now();
// Use timer to track delay until expiration
// to run the refresh at the proper time
return Observable.timer(Math.max(1, expires - now));
}
)
);
this.refreshFirebaseSub = expiresIn$
.subscribe(
() => {
console.log('Firebase token expired; fetching a new one');
this._getFirebaseToken();
}
);
}
unscheduleFirebaseRenewal() {
if (this.refreshFirebaseSub) {
this.refreshFirebaseSub.unsubscribe();
}
}
为了安排自动令牌更新,我们将创建一个可观察的计时器,该计时器可以倒计时到令牌的到期时间。 我们可以订阅expiresIn$
observable,然后再次调用_getFirebaseToken()
方法以获取新令牌。 signInWithCustomToken()
angularfire2 auth方法返回一个Promise。 当承诺解决后, scheduleFirebaseRenewal()
调用scheduleFirebaseRenewal()
,这又确保了只要用户登录到我们的应用程序,令牌就将继续更新。
我们还需要能够取消订阅令牌续订,因此我们还将为此创建一个方法。
最后,我们的身份验证服务中的最后两个方法是logout()
和tokenValid()
:
logout() {
// Ensure all auth items removed
localStorage.removeItem('expires_at');
localStorage.removeItem('auth_redirect');
this.accessToken = undefined;
this.userProfile = undefined;
this.loggedIn = false;
// Sign out of Firebase
this.loggedInFirebase = false;
this.afAuth.auth.signOut();
// Return to homepage
this.router.navigate(['/']);
}
get tokenValid(): boolean {
// Check if current time is past access token's expiration
const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return Date.now() < expiresAt;
}
logout()
方法从本地存储和我们的服务中删除所有会话信息,退出Firebase Auth,然后将用户重定向回首页(我们应用程序中唯一的公共路由)。
tokenValid
访问器方法通过将Auth0访问令牌的到期时间与当前日期时间进行比较来检查Auth0访问令牌是否到期。 这对于确定用户是否需要新的访问令牌很有用。 我们在本教程中不会对此进行介绍,但是您可能希望自己进一步探索Auth0会话续订。
这就是我们的AuthService
!
回调组件
回想一下,我们在根模块中创建了一个CallbackComponent
。 此外,我们将environment
的Auth0 redirect
设置为回调组件的路由。 这意味着,当用户使用Auth0登录时,他们将通过/callback
路由返回我们的应用,并将身份验证哈希附加到URI。
我们使用方法来处理身份验证和设置会话,从而创建了AuthService
,但是目前尚未从任何地方调用这些方法。 回调组件是执行此代码的适当位置。
打开callback.component.ts
文件并添加:
// src/app/callback.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth/auth.service';
@Component({
selector: 'app-callback',
template: `
<app-loading></app-loading>
`
})
export class CallbackComponent implements OnInit {
constructor(private auth: AuthService) { }
ngOnInit() {
this.auth.handleLoginCallback();
}
}
我们所有的回调组件所需要做的就是在AuthService
的handleAuth()
方法执行时显示LoadingComponent
。 handleLoginCallback()
方法将解析身份验证哈希,获取用户的个人资料信息,设置其会话,然后重定向到应用程序中的适当路由。
验证卫士
现在,我们已经实现了身份验证服务,我们可以访问在整个Angular应用程序中有效使用身份验证状态所需的属性和方法。 让我们使用此逻辑来实现AuthGuard
以保护路由。
使用Angular CLI应该会生成一些有用的样板代码,并且我们只需要进行一些小的更改即可确保只有经过身份验证的用户才能访问我们的受保护路由。
注意:必须注意,路由防护器 本身不能提供足够的安全性。 就像我们在本教程中所做的那样,您应该始终保护自己的API端点,并且永远不要仅依赖客户端来授权对受保护数据的访问。
打开auth.guard.ts
文件并进行以下更改:
// src/app/auth/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 auth: AuthService) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
if (this.auth.loggedIn) {
return true;
} else {
// Send guarded route to redirect after logging in
this.auth.login(state.url);
return false;
}
}
}
我们将导入AuthService
添加一个constructor()
函数,以使该服务在我们的路由保护器中可用。 如果满足条件以授予对路由的访问权,则canActivate()
方法应返回true
否则返回false
。 在我们的情况下,如果用户经过身份验证,则应该能够访问该受保护的路由。 该loggedIn
从我们的财产AuthService
提供此信息。
如果用户没有有效的令牌,我们将提示他们登录。我们希望他们在通过身份验证后被重定向回受保护的路由,因此,我们将调用login()
方法并传递受保护的路由( state.url
)作为重定向参数。
注意:请记住,我们之前设置了整个应用程序的体系结构和路由。 我们已经在我们的狗详细信息路由中添加了AuthGuard
,因此,既然我们已经实现了警卫功能,则应该对其进行保护。
核心逻辑
The last thing we'll do in this section of our tutorial is build out the remaining components and services that belong to our CoreModule
. We've already taken care of the LoadingComponent
and ErrorComponent
, so let's move on to the header.
Header Component
The header will use methods and logic from our authentication service to show login and logout buttons as well as display the user's name and picture if they're authenticated. Open the header.component.ts
file and add:
// src/app/core/header/header.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../../auth/auth.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styles: [`
img {
border-radius: 100px;
width: 30px;
}
.loading { line-height: 31px; }
.home-link { color: #212529; }
.home-link:hover { text-decoration: none; }
`]
})
export class HeaderComponent {
constructor(public auth: AuthService) {}
}
We'll add a few simple styles and import our AuthService
to make its members publicly available to our header component's template.
Next open the header.component.html
file and add:
<!-- src/app/core/header/header.component.html -->
<nav class="nav justify-content-between mt-2 mx-2 mb-3">
<div class="d-flex align-items-center">
<strong class="mr-1"><a routerLink="/" class="home-link">Popular Dogs ❤</a></strong>
</div>
<div class="ml-3">
<small *ngIf="auth.loading" class="loading">
Logging in...
</small>
<ng-template [ngIf]="!auth.loading">
<button
*ngIf="!auth.loggedIn"
class="btn btn-primary btn-sm"
(click)="auth.login()">Log In</button>
<span *ngIf="auth.loggedIn">
<img [src]="auth.userProfile.picture">
<small>{{ auth.userProfile.name }}</small>
<button
class="btn btn-danger btn-sm"
(click)="auth.logout()">Log Out</button>
</span>
</ng-template>
</div>
</nav>
The header now shows:
- The name of our app (“Popular Dogs”) with a link to the
/
route - A login button if the user is not authenticated
- A “Logging in…” message if the user is currently authenticating
- The user's picture, name, and a logout button if the user is authenticated
Now that we have our header component built, we need to display it in our app.
Open the app.component.html
file and add:
<!-- src/app/app.component.html -->
<app-header></app-header>
<div class="container">
<router-outlet></router-outlet>
</div>
The header component will now be displayed in our app with the current routed component showing beneath it. Check it out in the browser and try logging in!
Dog and DogDetail Models
Let's implement our dog.ts
and dog-detail.ts
interfaces . These are models that specify types for the shape of values that we'll use in our app. Using models ensures that our data has the structure that we expect.
We'll start with the dog.ts
interface:
// src/app/core/dog.ts
export interface Dog {
breed: string;
rank: number;
image: string;
}
Next let's implement the dog-detail.ts
interface:
// src/app/core/dog-detail.ts
export interface DogDetail {
breed: string;
rank: number;
description: string;
personality: string;
energy: string;
group: string;
image: string;
link: string;
}
API Service
With our Node API and models in place, we're ready to implement the service that will call our API in the Angular front end.
Open the api.service.ts
file and add this code:
// src/app/core/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { environment } from './../../environments/environment';
import { AuthService } from './../auth/auth.service';
import { Observable } from 'rxjs/Observable';
import { catchError } from 'rxjs/operators';
import 'rxjs/add/observable/throw';
import { Dog } from './../core/dog';
import { DogDetail } from './../core/dog-detail';
@Injectable()
export class ApiService {
private _API = `${environment.apiRoot}api`;
constructor(
private http: HttpClient,
private auth: AuthService) { }
getDogs$(): Observable<Dog[]> {
return this.http
.get(`${this._API}/dogs`)
.pipe(
catchError((err, caught) => this._onError(err, caught))
);
}
getDogByRank$(rank: number): Observable<DogDetail> {
return this.http
.get(`${this._API}/dog/${rank}`, {
headers: new HttpHeaders().set('Authorization', `Bearer ${this.auth.accessToken}`)
})
.pipe(
catchError((err, caught) => this._onError(err, caught))
);
}
private _onError(err, caught) {
let errorMsg = 'Error: Unable to complete request.';
if (err instanceof HttpErrorResponse) {
errorMsg = err.message;
if (err.status === 401 || errorMsg.indexOf('No JWT') > -1 || errorMsg.indexOf('Unauthorized') > -1) {
this.auth.login();
}
}
return Observable.throw(errorMsg);
}
}
We'll add the necessary imports to handle HTTP in Angular along with the environment configuration, AuthService
, RxJS imports, and Dog
and DogDetail
models we just created. We'll set up private members for the _API
and to store the _accessToken
, then make the HttpClient
and AuthService
available privately to our API service.
Our API methods will return observables that emit one value when the API is either called successfully or an error is thrown. The getDogs$()
stream returns an observable with an array of objects that are Dog
-shaped. The getDogByRank$(rank)
stream requires a numeric rank to be passed in, and will then call the API to retrieve the requested Dog
's data. This API call will send an Authorization
header containing the authenticated user's access token.
Finally, we'll create an error handler that checks for errors and assesses if the user is not authenticated and prompts for login if so. The observable will then terminate with an error.
Note: We are using arrow functions to pass parameters to our handler functions for RxJS pipeable operators (such as catchError
). This is done to preserve the scope of the this
keyword (see the “No separate this
” section of the MDN arrow functions documentation ).
下一步
We've already accomplished a lot in the first part of our tutorial series. In the next part, we'll finish our Popular Dogs application. In the meantime, here are some additional resources that you may want to check out:
Angular Testing Resources
If you're interested in learning more about testing in Angular, which we did not cover in this tutorial, please check out some of the following resources:
- Angular – Testing
- Angular Testing In Depth: Services
- Angular Testing In Depth: HTTP Services
- Angular Testing In Depth: Components
- How to correctly test Angular 4 application with Auth0 integration
其他资源
You can find more resources on Firebase, Auth0, and Angular here:
- Firebase documentation
- Cloud Firestore documentation
- angularfire2 documentation
- Auth0 documentation
- Auth0 pricing and features
- Angular documentation
- Angular CLI
- Angular Cheatsheet
In the next installment of our Auth0 + Firebase + Angular tutorial, we'll display data from our dogs API and learn how to set up and implement realtime comments with Firebase ! Check out Authenticating Firebase and Angular with Auth0: Part 2 now.
翻译自: https://www.sitepoint.com/authenticating-firebase-angular-auth0-1/
firebase auth