本文概述
在本教程中, 我们将构建一个REST API, 以使用Firebase和Node.js管理用户和角色。此外, 我们将介绍如何使用API授权(或不授权)哪些用户可以访问特定资源。
介绍
几乎每个应用程序都需要某种级别的授权系统。在某些情况下, 验证使用用户表设置的用户名/密码就足够了, 但是通常, 我们需要一种更细粒度的权限模型, 以允许某些用户访问某些资源并限制他们对其他资源的访问。构建支持后者的系统并非易事, 而且可能非常耗时。在本教程中, 我们将学习如何使用Firebase构建基于角色的身份验证API, 这将有助于我们快速启动并运行。
基于角色的身份验证
在此授权模型中, 权限是授予角色的, 而不是特定用户, 并且一个用户可以拥有一个或多个权限, 具体取决于你设计权限模型的方式。另一方面, 资源需要某些角色才能允许用户执行它。
Firebase
Firebase身份验证
简而言之, Firebase身份验证是一个基于令牌的可扩展身份验证系统, 可与Google, Facebook和Twitter等最常见的提供程序进行现成的集成。
它使我们能够使用自定义声明, 我们将利用它们来构建灵活的基于角色的API。
我们可以在声明中设置任何JSON值(例如, {角色:’admin’}或{角色:’manager’})。
设置后, 自定义声明将包含在Firebase生成的令牌中, 我们可以读取该值来控制访问。
它还具有非常慷慨的免费配额, 在大多数情况下, 这将绰绰有余。
Firebase功能
功能是完全托管的无服务器平台服务。我们只需要在Node.js中编写代码并进行部署即可。 Firebase负责按需扩展基础架构, 服务器配置等。在我们的案例中, 我们将使用它来构建我们的API, 并通过HTTP将其公开给网络。
Firebase允许我们将express.js应用程序设置为不同路径的处理程序-例如, 你可以创建一个Express应用程序并将其挂接到/ mypath, 所有到达此路由的请求将由配置的应用程序处理。
在功能范围内, 你可以使用Admin SDK访问整个Firebase Authentication API。
这就是我们创建用户API的方式。
我们将建立什么
因此, 在开始之前, 我们先来看一下我们要构建的内容。我们将使用以下端点创建REST API:
Http动词 | 路径 | 描述 | 授权书 |
---|---|---|---|
得到 | /用户 | 列出所有用户 | 只有管理员和管理员可以访问 |
开机自检 | /用户 | 创建新用户 | 只有管理员和管理员可以访问 |
得到 | / users /:id | 获取:id用户 | 管理员, 经理和与:id相同的用户有权访问 |
补丁 | / users /:id | 更新:id用户 | 管理员, 经理和与:id相同的用户有权访问 |
删除 | / users /:id | 删除:id用户 | 管理员, 经理和与:id相同的用户有权访问 |
这些端点中的每个端点都将处理身份验证, 验证授权, 执行相应的操作, 最后返回有意义的HTTP代码。
我们将创建验证令牌所需的身份验证和授权功能, 并检查声明中是否包含执行操作所需的角色。
构建API
为了构建API, 我们需要:
- Firebase项目
- 安装了firebase-tools
首先, 登录Firebase:
firebase login
接下来, 初始化一个Functions项目:
firebase init
? Which Firebase CLI features do you want to set up for this folder? ...
(O) Functions: Configure and deploy Cloud Functions
? Select a default Firebase project for this directory: {your-project}
? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use TSLint to catch probable bugs and enforce style? Yes
? Do you want to install dependencies with npm now? Yes
此时, 你将拥有一个Functions文件夹, 使用最少的设置即可创建Firebase Functions。
在src / index.ts上有一个helloWorld示例, 你可以取消注释以验证你的Function是否有效。然后, 你可以使用cd函数并运行npm run serve。此命令将转换代码并启动本地服务器。
你可以在http:// localhost:5000 / {your-project} / us-central1 / helloWorld中查看结果
注意, 该函数在’index.ts:’helloWorld’的定义为它的名称的路径上公开。
创建Firebase HTTP函数
现在, 让我们编写API。我们将创建一个http Firebase函数并将其挂在/ api路径上。
首先, 安装npm install express。
在src / index.ts上, 我们将:
- 使用admin.initializeApp()初始化firebase-admin SDK模块。
- 将Express应用设置为我们的api https终结点的处理程序
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as express from 'express';
admin.initializeApp();
const app = express();
export const api = functions.https.onRequest(app);
现在, 所有发送到/ api的请求都将由该应用程序实例处理。
接下来, 我们将配置应用实例以支持CORS并添加JSON主体解析器中间件。这样, 我们可以从任何URL发出请求并解析JSON格式的请求。
我们将首先安装必需的依赖项。
npm install --save cors body-parser
npm install --save-dev @types/cors
接着:
//...
import * as cors from 'cors';
import * as bodyParser from 'body-parser';
//...
const app = express();
app.use(bodyParser.json());
app.use(cors({ origin: true }));
export const api = functions.https.onRequest(app);
最后, 我们将配置应用程序要处理的路由。
//...
import { routesConfig } from './users/routes-config';
//…
app.use(cors({ origin: true }));
routesConfig(app)
export const api = functions.https.onRequest(app);
Firebase Functions允许我们将Express应用程序设置为处理程序, 并且在函数处设置的路径之后的任何路径。https.onRequest(app);(在这种情况下为api)也将由该应用程序处理。这样, 我们就可以编写特定的端点(例如api / users)并为每个HTTP动词设置处理程序, 接下来我们将进行处理。
我们来创建文件src / users / routes-config.ts
在这里, 我们将在POST’/ users’设置一个创建处理程序
import { Application } from "express";
import { create} from "./controller";
export function routesConfig(app: Application) {
app.post('/users', create
);
}
现在, 我们将创建src / users / controller.ts文件。
在此功能中, 我们首先验证所有字段都在正文请求中, 然后, 我们创建用户并设置自定义声明。
我们只是在setCustomUserClaims中传递{role}-其他字段已由Firebase设置。
如果没有错误发生, 我们将返回201代码, 其中包含用户的uid。
import { Request, Response } from "express";
import * as admin from 'firebase-admin'
export async function create(req: Request, res: Response) {
try {
const { displayName, password, email, role } = req.body
if (!displayName || !password || !email || !role) {
return res.status(400).send({ message: 'Missing fields' })
}
const { uid } = await admin.auth().createUser({
displayName, password, email
})
await admin.auth().setCustomUserClaims(uid, { role })
return res.status(201).send({ uid })
} catch (err) {
return handleError(res, err)
}
}
function handleError(res: Response, err: any) {
return res.status(500).send({ message: `${err.code} - ${err.message}` });
}
现在, 通过添加授权来保护处理程序。为此, 我们将几个处理程序添加到我们的创建端点。使用express.js, 你可以设置将按顺序执行的处理程序链。在处理程序中, 你可以执行代码并将其传递给next()处理程序或返回响应。我们要做的是首先对用户进行身份验证, 然后验证用户是否有权执行。
在文件src / users / routes-config.ts上:
//...
import { isAuthenticated } from "../auth/authenticated";
import { isAuthorized } from "../auth/authorized";
export function routesConfig(app: Application) {
app.post('/users', isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'] }), create
);
}
我们来创建文件src / auth / authenticated.ts。
在此功能上, 我们将验证请求标头中是否存在授权承载令牌。然后, 我们将使用admin.auth()。verifyidToken()对其进行解码, 并将用户的uid, 角色和电子邮件保留在res.locals变量中, 我们稍后将使用该变量来验证授权。
在令牌无效的情况下, 我们向客户端返回401响应:
import { Request, Response } from "express";
import * as admin from 'firebase-admin'
export async function isAuthenticated(req: Request, res: Response, next: Function) {
const { authorization } = req.headers
if (!authorization)
return res.status(401).send({ message: 'Unauthorized' });
if (!authorization.startsWith('Bearer'))
return res.status(401).send({ message: 'Unauthorized' });
const split = authorization.split('Bearer ')
if (split.length !== 2)
return res.status(401).send({ message: 'Unauthorized' });
const token = split[1]
try {
const decodedToken: admin.auth.DecodedIdToken = await admin.auth().verifyIdToken(token);
console.log("decodedToken", JSON.stringify(decodedToken))
res.locals = { ...res.locals, uid: decodedToken.uid, role: decodedToken.role, email: decodedToken.email }
return next();
}
catch (err) {
console.error(`${err.code} - ${err.message}`)
return res.status(401).send({ message: 'Unauthorized' });
}
}
现在, 让我们创建一个src / auth / authorized.ts文件。
在此处理程序中, 我们从先前设置的res.locals中提取用户的信息, 并验证它是否具有执行该操作所需的角色, 或者在该操作允许同一用户执行的情况下, 我们验证请求参数上的ID与auth令牌中的相同。如果用户没有所需的角色, 我们将返回403。
import { Request, Response } from "express";
export function isAuthorized(opts: { hasRole: Array<'admin' | 'manager' | 'user'>, allowSameUser?: boolean }) {
return (req: Request, res: Response, next: Function) => {
const { role, email, uid } = res.locals
const { id } = req.params
if (opts.allowSameUser && id && uid === id)
return next();
if (!role)
return res.status(403).send();
if (opts.hasRole.includes(role))
return next();
return res.status(403).send();
}
}
通过这两种方法, 我们将能够对请求进行身份验证并根据传入令牌中的角色对其进行授权。很好, 但是由于Firebase不允许我们从项目控制台设置自定义声明, 因此我们将无法执行任何这些终结点。为了绕过这个问题, 我们可以从Firebase身份验证控制台创建一个root用户
并在代码中设置电子邮件比较。现在, 当触发该用户的请求时, 我们将能够执行所有操作。
//...
const { role, email, uid } = res.locals
const { id } = req.params
if (email === '[email protected]')
return next();
//...
现在, 让我们将其余CRUD操作添加到src / users / routes-config.ts中。
对于获取或更新发送了:id参数的单个用户的操作, 我们还允许同一用户执行该操作。
export function routesConfig(app: Application) {
//..
// lists all users
app.get('/users', [
isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'] }), all
]);
// get :id user
app.get('/users/:id', [
isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'], allowSameUser: true }), get
]);
// updates :id user
app.patch('/users/:id', [
isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'], allowSameUser: true }), patch
]);
// deletes :id user
app.delete('/users/:id', [
isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'] }), remove
]);
}
并在src / users / controller.ts上。在这些操作中, 我们利用admin SDK与Firebase身份验证进行交互并执行相应的操作。就像我们之前在create操作上所做的一样, 我们在每个操作上返回有意义的HTTP代码。
对于更新操作, 我们验证所有存在的字段, 并用请求中发送的字段覆盖customClaims:
//..
export async function all(req: Request, res: Response) {
try {
const listUsers = await admin.auth().listUsers()
const users = listUsers.users.map(mapUser)
return res.status(200).send({ users })
} catch (err) {
return handleError(res, err)
}
}
function mapUser(user: admin.auth.UserRecord) {
const customClaims = (user.customClaims || { role: '' }) as { role?: string }
const role = customClaims.role ? customClaims.role : ''
return {
uid: user.uid, email: user.email || '', displayName: user.displayName || '', role, lastSignInTime: user.metadata.lastSignInTime, creationTime: user.metadata.creationTime
}
}
export async function get(req: Request, res: Response) {
try {
const { id } = req.params
const user = await admin.auth().getUser(id)
return res.status(200).send({ user: mapUser(user) })
} catch (err) {
return handleError(res, err)
}
}
export async function patch(req: Request, res: Response) {
try {
const { id } = req.params
const { displayName, password, email, role } = req.body
if (!id || !displayName || !password || !email || !role) {
return res.status(400).send({ message: 'Missing fields' })
}
await admin.auth().updateUser(id, { displayName, password, email })
await admin.auth().setCustomUserClaims(id, { role })
const user = await admin.auth().getUser(id)
return res.status(204).send({ user: mapUser(user) })
} catch (err) {
return handleError(res, err)
}
}
export async function remove(req: Request, res: Response) {
try {
const { id } = req.params
await admin.auth().deleteUser(id)
return res.status(204).send({})
} catch (err) {
return handleError(res, err)
}
}
//...
现在我们可以在本地运行该功能了。为此, 首先, 你需要设置帐户密钥, 以便能够在本地与auth API连接。然后运行:
npm run serve
部署API
大!现在, 我们已经编写了基于角色的API, 我们可以将其部署到Web上并开始使用它。使用Firebase部署非常容易, 我们只需要运行firebase deploy。部署完成后, 我们可以通过发布的URL访问我们的API。
你可以在https://console.firebase.google.com/u/0/project/{your-project}/functions/list上检查API URL。
就我而言, 它是https://us-central1-joaq-lab.cloudfunctions.net/api。
部署API
部署API后, 我们可以通过多种方式使用它-在本教程中, 我将介绍如何通过Postman或Angular应用程序使用它。
如果我们在任何浏览器上输入列出所有用户的网址(/ api / users), 则会得到以下信息:
原因是从浏览器发送请求时, 我们正在执行不带身份验证头的GET请求。这意味着我们的API实际上正在按预期运行!
我们的API是通过令牌保护的-为了生成这样的令牌, 我们需要调用Firebase的客户端SDK并使用有效的用户/密码凭据登录。成功后, Firebase将在响应中发送回令牌, 然后我们可以将其添加到我们要执行的以下任何请求的标头中。
从Angular应用程序
在本教程中, 我将介绍一些重要的内容, 以使用Angular应用程序中的API。完整的存储库可以在这里访问, 如果你需要有关如何创建Angular应用和配置@ angular / fire来使用的分步教程, 则可以查看此文章。
因此, 回到登录状态, 我们将使用一个带有<form>的SignInComponent, 让用户输入用户名和密码。
//...
<form [formGroup]="form">
<div class="form-group">
<label>Email address</label>
<input type="email"
formControlName="email"
class="form-control"
placeholder="Enter email">
</div>
<div class="form-group">
<label>Password</label>
<input type="password"
formControlName="password"
class="form-control"
placeholder="Password">
</div>
</form>
//...
在课堂上, 我们使用AngularFireAuth服务对SignInWithEmailAndPassword进行了签名。
//...
form: FormGroup = new FormGroup({
email: new FormControl(''), password: new FormControl('')
})
constructor(
private afAuth: AngularFireAuth
) { }
async signIn() {
try {
const { email, password } = this.form.value
await this.afAuth.auth.signInWithEmailAndPassword(email, password)
} catch (err) {
console.log(err)
}
}
//..
此时, 我们可以登录我们的Firebase项目。
当我们在DevTools中检查网络请求时, 可以看到Firebase在验证了用户名和密码后返回了令牌。
我们将使用此令牌将标头的请求发送到我们构建的API。将令牌添加到所有请求的一种方法是使用HttpInterceptor。
此文件显示了如何从AngularFireAuth获取令牌并将其添加到标头的请求中。然后, 我们在AppModule中提供拦截器文件。
http-interceptors / auth-token.interceptor.ts
@Injectable({ providedIn: 'root' })
export class AuthTokenHttpInterceptor implements HttpInterceptor {
constructor(
private auth: AngularFireAuth
) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.auth.idToken.pipe(
take(1), switchMap(idToken => {
let clone = req.clone()
if (idToken) {
clone = clone.clone({ headers: req.headers.set('Authorization', 'Bearer ' + idToken) });
}
return next.handle(clone)
})
)
}
}
export const AuthTokenHttpInterceptorProvider = {
provide: HTTP_INTERCEPTORS, useClass: AuthTokenHttpInterceptor, multi: true
}
app.module.ts
@NgModule({
//..
providers: [
AuthTokenHttpInterceptorProvider
]
//...
})
export class AppModule { }
设置拦截器后, 我们可以从httpClient向我们的API发出请求。例如, 在这里的UsersService中, 我们将所有用户称为列表, 通过ID来获取用户, 创建用户并更新用户。
//…
export type CreateUserRequest = { displayName: string, password: string, email: string, role: string }
export type UpdateUserRequest = { uid: string } & CreateUserRequest
@Injectable({
providedIn: 'root'
})
export class UserService {
private baseUrl = '{your-functions-url}/api/users'
constructor(
private http: HttpClient
) { }
get users$(): Observable<User[]> {
return this.http.get<{ users: User[] }>(`${this.baseUrl}`).pipe(
map(result => {
return result.users
})
)
}
user$(id: string): Observable<User> {
return this.http.get<{ user: User }>(`${this.baseUrl}/${id}`).pipe(
map(result => {
return result.user
})
)
}
create(user: CreateUserRequest) {
return this.http.post(`${this.baseUrl}`, user)
}
edit(user: UpdateUserRequest) {
return this.http.patch(`${this.baseUrl}/${user.uid}`, user)
}
}
现在, 我们可以调用API以获得ID标识的登录用户, 并列出组件中的所有用户, 如下所示:
//...
<div *ngIf="user$ | async; let user"
class="col-12">
<div class="d-flex justify-content-between my-3">
<h4> Me </h4>
</div>
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1">{{user.displayName}}</h5>
<small>{{user.email}}</small>
</div>
<span class="badge badge-primary badge-pill">{{user.role?.toUpperCase()}}</span>
</li>
</ul>
</div>
<div class="col-12">
<div class="d-flex justify-content-between my-3">
<h4> All Users </h4>
</div>
<ul *ngIf="users$ | async; let users"
class="list-group">
<li *ngFor="let user of users"
class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1">{{user.displayName}}</h5>
<small class="d-block">{{user.email}}</small>
<small class="d-block">{{user.uid}}</small>
</div>
<span class="badge badge-primary badge-pill">{{user.role?.toUpperCase()}}</span>
</li>
</ul>
//...
//...
users$: Observable<User[]>
user$: Observable<User>
constructor(
private userService: UserService, private userForm: UserFormService, private modal: NgbModal, private afAuth: AngularFireAuth
) { }
ngOnInit() {
this.users$ = this.userService.users$
this.user$ = this.afAuth.user.pipe(
filter(user => !!user), switchMap(user => this.userService.user$(user.uid))
)
}
//...
这就是结果。
请注意, 如果我们使用具有role = user的用户登录, 则仅呈现Me部分。
然后, 我们会在网络检查器上收到403。这是由于我们之前在API上设置的限制, 仅允许”管理员”列出所有用户。
现在, 让我们添加”创建用户”和”编辑用户”功能。为此, 我们首先创建一个UserFormComponent和一个UserFormService。
<ng-container *ngIf="user$ | async"></ng-container>
<div class="modal-header">
<h4 class="modal-title"
id="modal-title">{{ title$ | async}}</h4>
<button type="button"
class="close"
aria-describedby="modal-title"
(click)="dismiss()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form [formGroup]="form"
(ngSubmit)="save()">
<div class="form-group">
<label>Email address</label>
<input type="email"
formControlName="email"
class="form-control"
placeholder="Enter email">
</div>
<div class="form-group">
<label>Password</label>
<input type="password"
formControlName="password"
class="form-control"
placeholder="Password">
</div>
<div class="form-group">
<label>Display Name</label>
<input type="string"
formControlName="displayName"
class="form-control"
placeholder="Enter display name">
</div>
<div class="form-group">
<label>Role</label>
<select class="custom-select"
formControlName="role">
<option value="admin">Admin</option>
<option value="manager">Manager</option>
<option value="user">User</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-outline-danger"
(click)="dismiss()">Cancel</button>
<button type="button"
class="btn btn-primary"
(click)="save()">Save</button>
</div>
@Component({
selector: 'app-user-form', templateUrl: './user-form.component.html', styleUrls: ['./user-form.component.scss']
})
export class UserFormComponent implements OnInit {
form = new FormGroup({
uid: new FormControl(''), email: new FormControl(''), displayName: new FormControl(''), password: new FormControl(''), role: new FormControl(''), });
title$: Observable<string>;
user$: Observable<{}>;
constructor(
public modal: NgbActiveModal, private userService: UserService, private userForm: UserFormService
) { }
ngOnInit() {
this.title$ = this.userForm.title$;
this.user$ = this.userForm.user$.pipe(
tap(user => {
if (user) {
this.form.patchValue(user);
} else {
this.form.reset({});
}
})
);
}
dismiss() {
this.modal.dismiss('modal dismissed');
}
save() {
const { displayName, email, role, password, uid } = this.form.value;
this.modal.close({ displayName, email, role, password, uid });
}
}
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class UserFormService {
_BS = new BehaviorSubject({ title: '', user: {} });
constructor() { }
edit(user) {
this._BS.next({ title: 'Edit User', user });
}
create() {
this._BS.next({ title: 'Create User', user: null });
}
get title$() {
return this._BS.asObservable().pipe(
map(uf => uf.title)
);
}
get user$() {
return this._BS.asObservable().pipe(
map(uf => uf.user)
);
}
}
返回主要组件, 让我们添加按钮以调用这些操作。在这种情况下, “编辑用户”将仅对登录用户可用。如果需要, 你可以继续添加功能来编辑其他用户!
//...
<div class="d-flex justify-content-between my-3">
<h4> Me </h4>
<button class="btn btn-primary"
(click)="edit(user)">
Edit Profile
</button>
</div>
//...
<div class="d-flex justify-content-between my-3">
<h4> All Users </h4>
<button class="btn btn-primary"
(click)="create()">
New User
</button>
</div>
//...
//...
create() {
this.userForm.create();
const modalRef = this.modal.open(UserFormComponent);
modalRef.result.then(user => {
this.userService.create(user).subscribe(_ => {
console.log('user created');
});
}).catch(err => {
});
}
edit(userToEdit) {
this.userForm.edit(userToEdit);
const modalRef = this.modal.open(UserFormComponent);
modalRef.result.then(user => {
this.userService.edit(user).subscribe(_ => {
console.log('user edited');
});
}).catch(err => {
});
}
来自邮递员
Postman是一种用于构建API并向其发出请求的工具。这样, 我们可以模拟我们从任何客户端应用程序或其他服务中调用我们的API。
我们将演示的是如何发送请求以列出所有用户的信息。
打开工具后, 我们将设置URL https:// us-central1- {your-project} .cloudfunctions.net / api / users:
接下来, 在选项卡授权上, 选择Bearer Token, 并设置先前从Dev Tools提取的值。
总结
恭喜你!你已经完成了整个教程, 现在你已经学会了在Firebase上创建基于用户角色的API。
我们还介绍了如何从Angular应用程序和Postman中使用它。
让我们回顾一下最重要的事情:
- Firebase允许你使用企业级身份验证API快速启动并运行, 以后可以对其进行扩展。
- 几乎每个项目都需要授权-如果你需要使用基于角色的模型控制访问权限, 则Firebase身份验证可让你快速入门。
- 基于角色的模型依赖于验证具有特定角色的用户与特定用户的用户所请求的资源。
- 使用Firebase Function上的Express.js应用程序, 我们可以创建REST API并设置处理程序以对请求进行身份验证和授权。
- 利用内置的自定义声明, 你可以创建基于角色的身份验证API并保护你的应用程序。
你可以在此处进一步了解Firebase身份验证。而且, 如果你想利用我们定义的角色, 可以使用@ angular / fire帮助器。
评论前必须登录!
注册