简介
在这篇文章中,我们将创建一个小项目,将Firebase认证整合到NestJS应用程序中。
认证是任何应用程序的一个重要组成部分,但从头开始设置可能会有很大的压力。这是Firebase通过其认证产品解决的一个问题。
Firebase包括一系列的产品和解决方案,使应用开发更容易。Firebase提供的一些服务包括数据库、认证、分析和托管,等等。Firebase可以通过firebase-adminnpm模块被集成到NodeJS应用中。
NestJS帮助你使用TypeScript创建服务器端的NodeJS应用程序。该框架在npm上每周有超过60万次的下载,在GitHub上有35K颗星,是一个非常受欢迎的框架。它有一个Angular类型的架构,具有控制器和模块等功能。NestJS在引擎盖下使用Express,尽管它也可以被配置为使用Fastify。
该项目
我们将创建一个简单的应用程序,只允许认证的用户访问一个资源。用户可以通过Firebase客户端的登录和注册来进行认证。在认证时,会向用户提供一个JSON Web Token(JWT),然后将其与随后对受限资源的请求一起发送。所提供的JWT会在服务器端使用firebase-admin
SDK进行验证,并根据JWT的有效性允许或拒绝访问。
开始使用
首先,让我们创建一个Firebase应用程序。这将为我们提供一些配置,我们将在以后的NestJS应用程序中使用。你可以通过Firebase控制台做到这一点。点击添加项目,然后命名你的项目。我们在这个项目中不需要谷歌分析,所以你不必启用它。然后你可以点击创建项目。
一旦你的应用程序被创建,点击项目概览旁边的设置图标,选择项目 设置。在服务账户标签下,生成一个新的私钥。这应该会下载一个带有一些证书的JSON文件,我们会用它来在服务器(NestJS)端初始化我们的Firebase Admin SDK。
在同一个项目设置菜单中,在常规标签下,滚动到你的应用程序,向Firebase注册你的应用程序(如果你已经在Firebase注册了一个应用程序,点击添加应用程序按钮)。
我们的应用程序是基于网络的,所以选择 **</>
**图标。接下来,给你的应用程序一个昵称。你不需要选择Firebase托管,除非你打算这样做。
你会得到一些脚本的链接以及Firebase的配置,这些配置是你的应用程序正常运行所需要的。把这些内容复制到一个你可以轻松访问的地方,因为以后会需要它。
之后,点击认证(位于Build侧边栏下),在登录方式菜单下,启用电子邮件/密码。我们将用用户的电子邮件和密码进行认证。
初始化你的NestJS应用程序
接下来,我们将全局安装Nest CLI包。这将为我们提供一些命令,其中之一是nest
命令,我们可以用它来启动一个新的NestJS应用程序。
npm i -g @nestjs/cli //install nest cli package globally
nest new firebase-auth-project //create a new nestjs project in a folder named firebase-auth-project
复制代码
创建一个新项目的安装过程可能需要一点时间,因为所有需要的依赖都需要安装。新项目应该有git初始化,一些文件夹自动添加到.gitignore
。将*/**/firebase.config.json
添加到.gitignore
。
使用npm run start:dev
命令在开发中启动你的应用程序。NestJS默认在3000端口运行,当文件被保存时,服务器会自动重新启动。每当你启动应用程序时,你的TypeScript文件会被编译成dist
文件夹中的纯JavaScript。
我们将使用服务器上的Handlebars文件。要做到这一点,我们需要hbs
模块,可以用以下命令来安装。
npm i hbs
npm i @types/hbs
复制代码
Handlebars是一个模板引擎,帮助我们编写可重复使用的动态HTML。你可以在这里阅读更多关于模板引擎的信息。
你现在可以修改你的main.ts
文件,看起来像这样。
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
import * as hbs from 'hbs';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const logger = new Logger('App');
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
hbs.registerPartials(join(__dirname, '..', 'views/partials'));
app.setViewEngine('hbs');
app.set('view options', { layout: 'main' });
await app.listen(3000);
logger.log('Application started on port 3000');
}
bootstrap();
复制代码
你的文件中每一行的末尾都可能有一个
Delete`␍`
的错误,特别是如果你运行的是Windows。这是因为在Windows中,行末序列由CR(carriage-return character)
和换行符,或LF(linefeed character)
表示,而git只使用换行符LF
。运行npm run lint
应该可以解决这个问题,或者你可以在你的代码编辑器中手动设置行结束序列为LF
。
app.set('view options', { layout: 'main' });
表示一个main.hbs
文件将作为我们hbs
文件的布局。
在这个项目中,我们将使用几个软件包,所以在进一步讨论之前,让我们把它们全部安装好。
npm i @nestjs/passport class-transformer firebase-admin passport passport-firebase-jwt
复制代码
Passport是一个易于使用且非常流行的NodeJS认证库,并通过@nestjs/passport模块与NestJS很好地合作,提供一个强大的认证系统。
创建路由和hbs
文件
让我们来创建我们的第一个路由。在app.controller.ts
文件中,添加以下代码。
import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';
@Controller('')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('login')
@Render('login')
login() {
return;
}
@Get('signup')
@Render('signup')
signup() {
return;
}
}
复制代码
这表明,当我们向/login
路由发送一个GET
请求时,login.hbs
文件应该为我们呈现,同时也是注册路由。现在让我们来创建这些hbs
文件。
在你项目的根目录下,创建public
和views
文件夹。你的文件夹结构应该看起来有点像这样。
├──-public
├──-src
├───test
├───views
复制代码
记住,我们已经指出main.hbs
是我们的布局文件,所以在视图文件夹内,创建main.hbs
文件并添加以下代码。
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://juejin.cn/styles/style.css">
</head>
<body>
<nav class="navbar navbar-dark bg-primary navbar-expand">
<div class="container"><a class="navbar-brand" href="">Nest Auth</a>
</div>
</nav>
{{{body}}}
<div id="quotes" class="d-none">
</div>
<script src="https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.3.1/firebase-auth.js"></script>
<script src='https://juejin.cn/scripts/main.js'></script>
</html>
复制代码
注意文件底部的前两个脚本。这些是在网络上使用Firebase功能的脚本。第一个是核心的FirebaseJS SDK,而第二个是用于Firebase认证的。你需要为你的应用程序中需要的Firebase功能添加脚本。
在视图文件夹中创建一个login.hbs
和signup.hbs
文件,并添加以下代码。
login.hbs
。
<div class='container'>
<form id='login-form' class='mt-3'>
<div class='form-group'>
<label htmlFor='email'>Email address</label>
<input type='email' class='form-control' id='email' placeholder='Enter email' required />
</div>
<div class='form-group'>
<label htmlFor='password'>Password</label>
<input type='password' class='form-control' id='password' placeholder='Password' required />
</div>
<p id="error" class="text-white text-small bg-danger"></p>
<button type='submit' class='btn btn-primary pull-left'>
Login
</button>
</form>
</div>
<script src='https://juejin.cn/scripts/login.js'></script>
复制代码
signup.hbs
。
<div class='container'>
<form id='signup-form' class='mt-3'>
<div class='form-group'>
<label htmlFor='email'>Email address</label>
<input type='email' class='form-control' id='email' placeholder='Enter email' required />
</div>
<div class='form-group'>
<label htmlFor='password'>Password</label>
<input type='password' class='form-control' id='password' placeholder='Password' required />
</div>
<p id="error" class="text-white text-small bg-danger"></p>
<button type='submit' class='btn btn-primary'>
Signup
</button>
</form>
</div>
<script src="https://juejin.cn/scripts/signup.js"></script>
>
复制代码
现在是样式和脚本。在public
文件夹中,添加脚本和样式子文件夹。在样式子文件夹中,添加一个style.css
文件。
style.css
。
blockquote {
position: relative;
text-align: left;
padding: 1.2em 0 2em 38px;
border: none;
margin: 20px auto 20px;
max-width: 800px;
width: 100%;
display: block;
}
blockquote:after {
content: '';
display: block;
width: 2px;
height: 100%;
position: absolute;
left: 0;
color: #66cc66;
top: 0;
background: -moz-linear-gradient(
top,
#66cc66 0%,
#66cc66 60%,
rgba(255, 255, 255, 0) 100%
);
background: -webkit-linear-gradient(
top,
#66cc66 0%,
#66cc66 60%,
rgba(255, 255, 255, 0) 100%
);
}
blockquote:before {
content: '\f10d';
font-family: 'fontawesome';
font-size: 20px;
display: block;
margin-bottom: 0.8em;
font-weight: 400;
color: #66cc66;
}
blockquote > cite,
blockquote > p > cite {
display: block;
font-size: 16px;
line-height: 1.3em;
font-weight: 700;
font-style: normal;
margin-top: 1.1em;
letter-spacing: 0;
font-style: italic;
}
复制代码
在scripts文件夹中,创建以下文件:main.js
,login.js
, 和signup.js
。你可以暂时让它们空着,我们会回来找它们。你应该访问/login
和/signup
路线,以确保你的文件被正确呈现。
创建我们的资源
我们清单上的下一个项目是创建我们的受限资源。在这种情况下,它将是一个引言及其作者的列表。要创建一个新的resources
文件夹(模块、控制器和服务都已设置),请运行。
nest g resource resources
复制代码
选择REST API作为传输层,在 “你是否愿意生成CRUD入口点?”的答案中选择 “否“。
一旦完成,在resources.service.ts
文件中,添加以下代码。
import { Injectable } from '@nestjs/common';
@Injectable()
export class ResourcesService {
private readonly resources: any[];
constructor() {
this.resources = [
{
quote: 'They taste like...burning.',
character: 'Ralph Wiggum',
},
{
quote: 'My eyes! The goggles do nothing!',
character: 'Rainier Wolfcastle',
},
{
quote:
"Hello, Simpson. I'm riding the bus today becuase Mother hid my car keys to punish me for talking to a woman on the phone. She was right to do it.",
character: 'Principal Skinner',
},
{
quote:
'I live in a single room above a bowling alley...and below another bowling alley.',
character: 'Frank Grimes',
},
{
quote:
"All I'm gonna use this bed for is sleeping, eating and maybe building a little fort.",
character: 'Homer Simpson',
},
{
quote: 'In theory, Communism works! In theory.',
character: 'Homer Simpson',
},
{
quote: "Oh, wow, windows. I don't think I could afford this place.",
character: 'Otto',
},
];
}
getAll() {
return this.resources;
}
}
复制代码
在那里你可以看到我们的引号(来自电视节目 “辛普森一家”)和一个方法,getAll()
,它可以返回所有的引号。
将此添加到resources.controller.ts
文件中。
import { Controller, Get } from '@nestjs/common';
import { ResourcesService } from './resources.service';
@Controller('resources')
export class ResourcesController {
constructor(private readonly resourcesService: ResourcesService) {}
@Get('')
getAll() {
return this.resourcesService.getAll();
}
}
复制代码
@Controller()
装饰器表明,以/resources
开始的路由将被引导到这个端点。我们有一个GET
端点,使用getAll()
方法返回我们所有的报价,在resources.service.ts
。为了测试你的应用程序,向GET
发送一个请求到 [http://localhost:3000/resources](http://localhost:3000/resources)
应该会返回所有的报价。
这个端点目前是公开的,现在是时候处理我们应用程序的认证部分了。
Firebase客户端
为了用Firebase从客户端认证用户,首先我们用你在Firebase控制台创建新应用时提供的Firebase网络配置初始化我们的应用。你可以在项目设置菜单的常规选项卡中得到这个。
这样把设置添加到你的公共文件夹中的main.js
文件。
const quotes = document.getElementById('quotes');
const error = document.getElementById('error');
var firebaseConfig = {
apiKey: 'AIzaSyB7oEYDje93lJI5bA1VKNPX9NVqqcubP1Q',
authDomain: 'fir-auth-dcb9f.firebaseapp.com',
projectId: 'fir-auth-dcb9f',
storageBucket: 'fir-auth-dcb9f.appspot.com',
messagingSenderId: '793102669717',
appId: '1:793102669717:web:ff4c646e5b2242f518c89c',
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
const displayQuotes = (allQuotes) => {
let html = '';
for (const quote of allQuotes) {
html += `<blockquote class="wp-block-quote">
<p>${quote.quote}. </p><cite>${quote.character}</cite>
</blockquote>`;
}
return html;
};
复制代码
quotes
,error
, 和displayQuotes
是将被login.js
和signup.js
脚本使用的变量,所以你的main.js
文件在其他两个文件之前被导入是很重要的。反过来,main.js
可以访问firebase
变量,因为 Firebase 脚本首先被包含在main.hbs
文件中。
现在,为了处理用户的注册,在signup.js
中添加这个。
const signupForm = document.getElementById('signup-form');
const emailField = document.getElementById('email');
const passwordField = document.getElementById('password');
signupForm.addEventListener('submit', (e) => {
e.preventDefault();
const email = emailField.value;
const password = passwordField.value;
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(({ user }) => {
return user.getIdToken().then((idToken) => {
return fetch('/resources', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${idToken}`,
},
})
.then((resp) => resp.json())
.then((resp) => {
const html = displayQuotes(resp);
quotes.innerHTML = html;
document.title = 'quotes';
window.history.pushState(
{ html, pageTitle: 'quotes' },
'',
'/resources',
);
signupForm.style.display = 'none';
quotes.classList.remove('d-none');
})
.catch((err) => {
console.error(err.message);
error.innerHTML = err.message;
});
});
})
.catch((err) => {
console.error(err.message);
error.innerHTML = err.message;
});
});
复制代码
并在login.js
中登录。
const loginForm = document.getElementById('login-form');
const emailField = document.getElementById('email');
const passwordField = document.getElementById('password');
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const email = emailField.value;
const password = passwordField.value;
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(({ user }) => {
return user.getIdToken().then((idToken) => {
return fetch('/resources', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${idToken}`,
},
})
.then((resp) => resp.json())
.then((resp) => {
const html = displayQuotes(resp);
quotes.innerHTML = html;
document.title = 'quotes';
window.history.pushState(
{ html, pageTitle: 'quotes' },
'',
'/resources',
);
loginForm.style.display = 'none';
quotes.classList.remove('d-none');
})
.catch((err) => {
console.error(err.message);
error.innerHTML = err.message;
});
});
})
.catch((err) => {
console.error(err.message);
error.innerHTML = err.message;
});
});
复制代码
Firebase-admin
虽然用户现在可以注册并登录到我们的应用程序,但我们的resources
路径仍然是开放的,任何人都可以访问。记住,我们在我们的NestJS应用程序中安装了firebase-admin
。正如我前面提到的,这个包将帮助验证从客户端发送的JWT令牌,然后允许或拒绝用户访问路由。
在src
文件夹中,创建一个名为firebase
的文件夹。这将包含我们所有的Firebase设置。在firebase
文件夹中,创建一个名为firebase.config.json
的文件。这将包含你在服务账户标签下生成私钥时下载的JSON文件的值。
{
"type": "service_account",
"project_id": "",
"private_key_id": "",
"private_key": "",
"client_email": "",
"client_id": "",
"auth_uri": "",
"token_uri": "",
"auth_provider_x509_cert_url": "",
"client_x509_cert_url": ""
}
复制代码
保持这些值的私密性是很重要的,因为其中有些值是非常敏感的。
接下来,我们要为Firebase创建一个Passport策略。策略是Passport中特定服务(这里是指Firebase)的认证机制。在firebase
文件夹中创建一个firebase-auth.strategy.ts
文件,并添加以下代码。
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Strategy, ExtractJwt } from 'passport-firebase-jwt';
import * as firebaseConfig from './firebase.config.json';
import * as firebase from 'firebase-admin';
const firebase_params = {
type: firebaseConfig.type,
projectId: firebaseConfig.project_id,
privateKeyId: firebaseConfig.private_key_id,
privateKey: firebaseConfig.private_key,
clientEmail: firebaseConfig.client_email,
clientId: firebaseConfig.client_id,
authUri: firebaseConfig.auth_uri,
tokenUri: firebaseConfig.token_uri,
authProviderX509CertUrl: firebaseConfig.auth_provider_x509_cert_url,
clientC509CertUrl: firebaseConfig.client_x509_cert_url,
};
@Injectable()
export class FirebaseAuthStrategy extends PassportStrategy(
Strategy,
'firebase-auth',
) {
private defaultApp: any;
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
this.defaultApp = firebase.initializeApp({
credential: firebase.credential.cert(firebase_params),
});
}
async validate(token: string) {
const firebaseUser: any = await this.defaultApp
.auth()
.verifyIdToken(token, true)
.catch((err) => {
console.log(err);
throw new UnauthorizedException(err.message);
});
if (!firebaseUser) {
throw new UnauthorizedException();
}
return firebaseUser;
}
}
复制代码
这里发生了什么?JWT被作为承载令牌从请求头中提取出来,我们的Firebase应用程序被用来验证该令牌。如果令牌有效,就会返回结果,否则就会拒绝用户的请求,并抛出一个未经授权的异常。
如果你在导入Firebase配置时遇到ESLint错误,请在你的
tsconfig.json
文件中添加这个:"resolveJsonModule": true
。
整合策略
现在,我们的认证策略是一个独立的函数,这并没有什么帮助。我们可以让它成为中间件,并将其集成到需要认证的端点中,但NestJS有一种更简单、更好的处理认证的方式,叫做Guards。我们将创建一个卫士来利用我们的Firebase策略,并通过一个简单的装饰器,将其包裹在需要认证的路由中。
创建一个名为firebase-auth.guard.ts
的文件,并在其中添加以下代码。
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
@Injectable()
export class FirebaseAuthGuard extends AuthGuard('firebase-auth') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>('public', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
复制代码
接下来,更新你的resources.controller.ts
文件,看起来像这样。
import { Controller, Get, UseGuards } from '@nestjs/common';
import { FirebaseAuthGuard } from 'src/firebase/firebase-auth.guard';
import { ResourcesService } from './resources.service';
@Controller('resources')
export class ResourcesController {
constructor(private readonly resourcesService: ResourcesService) {}
@Get('')
@UseGuards(FirebaseAuthGuard)
getAll() {
return this.resourcesService.getAll();
}
}
复制代码
你还需要更新你的app.module.ts
文件,把FirebaseAuthStrategy
添加到提供者的列表中。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { FirebaseAuthStrategy } from './firebase/firebase-auth.strategy';
import { ResourcesModule } from './resources/resources.module';
@Module({
imports: [ResourcesModule],
controllers: [AppController],
providers: [AppService, FirebaseAuthStrategy],
})
export class AppModule {}
复制代码
你可以再次测试你的应用程序,你会发现我们的资源路由现在得到了很好的保护。
总结
虽然这是一个基本的应用,但你可以在这些知识的基础上创建更大的应用,使用Firebase认证。你也可以通过调用firebase.auth().signOut()
,轻松地从Firebase客户端注销一个用户。这个资源库可以在Github上找到。
The postUsing Firebase Authentication in NestJS appsappeared first onLogRocket Blog.