作者都是各自领域经过审查的专家,并撰写他们有经验的主题. All of our content is peer reviewed and validated by Toptal experts in the same field.
Joaquin Cid
Verified Expert in Engineering

Joaquin是一名全栈开发人员,在WebMD和Getty Images等公司拥有超过12年的工作经验.

PREVIOUSLY AT

Velocity Partners
Share

In this tutorial, we’ll build a Node.js app that leverages the Firebase Auth REST API to manage users and roles. In addition, 我们将看到如何使用API来授权(或不授权)哪些用户可以访问特定的资源.

Introduction

几乎每个应用程序都需要某种程度的授权系统. 在某些情况下,使用 Users table is enough, but often, 我们需要一个更细粒度的权限模型,以允许某些用户访问某些资源,并限制他们访问其他资源. Building a system to support the latter is not trivial and can be very time consuming. In this tutorial, we’ll learn how to build a role-based auth API using Firebase, 这将帮助我们快速启动和运行.

Role-based Auth

In this authorization model, access is granted to roles, instead of specific users, and a user can have one or more depending on how you design your permission model. Resources, on the other hand, require certain roles to allow a user to execute it.

Role-based Auth with illustrated

Firebase

Firebase Authentication

In a nutshell, Firebase身份验证是一个可扩展的基于令牌的身份验证系统,并提供了与最常见的提供商(如Google)的开箱即用集成, Facebook, and Twitter, among others.

它使我们能够使用自定义声明,我们将利用它来构建灵活的基于角色的API. 这些声明可以被认为是Firebase用户角色,将直接映射到我们的应用程序所支持的角色.

我们可以在声明中设置任何JSON值(例如.g., { role: 'admin' } or { role: 'manager' }).

Once set, custom claims will be included in the generated Firebase token, 我们可以读这个值来控制访问.

它还提供了非常慷慨的免费配额,在大多数情况下都绰绰有余.

Firebase Functions

Functions是一个完全托管的无服务器平台服务. We just need to write our code in Node.js and deploy it. Firebase负责按需扩展基础设施、服务器配置等. In our case, we’ll use it to build our API and expose it via HTTP to the web.

Firebase allows us to set express.js 应用程序作为不同路径的处理程序——例如,你可以创建一个Express应用程序并将其挂接到 /mypath, and all requests coming to this route will be handled by the app configured.

From within the context of a function, 你可以访问整个Firebase身份验证API, using the Admin SDK.

This is how we’ll create the user API.

What We’ll Build

So before we get started, let’s take a look at what we’ll build. We are going to create a REST API with the following endpoints:

Http VerbPathDescriptionAuthorization
GET/usersLists all usersOnly admins and managers have access
POST/usersCreates new userOnly admins and managers have access
GET/users/:idGets the :id user管理员、管理员和与:id相同的用户都可以访问
PATCH/users/:idUpdates the :id user管理员、管理员和与:id相同的用户都可以访问
DELETE/users/:idDeletes the :id user管理员、管理员和与:id相同的用户都可以访问

每个端点都将处理身份验证, validate authorization, perform the correspondent operation, and finally return a meaningful HTTP code.

我们将创建验证令牌所需的身份验证和授权函数,并检查声明是否包含执行操作所需的角色.

Building the API

In order to build the API, we’ll need:

  • A Firebase project
  • firebase-tools installed

First, log in to Firebase:

firebase login

Next, initialize a Functions project:

firebase init

? Which Firebase CLI features do you want to set up for this folder? ...
(O)功能:配置和部署云功能

? Select a default Firebase project for this directory: {your-project}

? 你想用什么语言来写云函数? TypeScript

? Do you want to use TSLint to catch probable bugs and enforce style? Yes

? 你想现在用npm安装依赖吗? Yes

此时,您将拥有一个Functions文件夹,它具有创建Firebase Functions所需的最小设置.

At src/index.ts there’s a helloWorld example, which you can uncomment to validate that your Functions works. Then you can cd functions and run npm run serve. This command will transpile the code and start the local server.

You can check the results at http://localhost:5000/{your-project}/us-central1/helloWorld

A fresh Firebase app

Notice the function is exposed on the path defined as the name of it at 'index.ts: 'helloWorld'.

Creating a Firebase HTTP Function

Now let’s code our API. We are going to create an http Firebase function and hook it on /api path.

First, install npm install express.

On the src/index.ts we will:

  • 初始化firebase-admin SDK模块 admin.initializeApp();
  • Set an Express app as the handler of our api http endpoint
从'firebase-functions'中导入* as函数;
import * as admin from 'firebase-admin';
import * as express from 'express';

admin.initializeApp();

const app = express();

export const api = functions.http.onRequest(app);

Now, all requests going to /api will be handled by the app instance.

The next thing we’ll do is configure the app 实例来支持CORS并添加JSON主体解析器中间件. This way we can make requests from any URL and parse JSON formatted requests.

We’ll first install required dependencies.

npm install --save cors body-parser
npm install --save-dev @types/cors

And then:

//...
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.http.onRequest(app);

最后,我们将配置路由 app will handle.

//...
import { routesConfig } from './users/routes-config';
 
//…
app.use(cors({ origin: true }));
routesConfig(app)

export const api = functions.http.onRequest(app);

Firebase Functions allows us to set an Express app as the handler, and any path after the one you set up at functions.http.onRequest(app);—in this case, api—will also be handled by the app. 这允许我们编写特定的端点,例如 api/users 并为每个HTTP动词设置处理程序,这是我们接下来要做的.

Let’s create the file src/users/routes-config.ts

Here, we’ll set a create handler at POST '/users'

import { Application } from "express";
import { create} from "./controller";

导出routesConfig(app: Application) {
   app.post('/users',
       create
   );
}

Now, we’ll create the src/users/controller.ts file.

In this function, 我们首先验证所有字段都在请求体中, and next, 我们创建用户并设置自定义声明.

We are just passing { role } in the setCustomUserClaims-其他字段已经由Firebase设置.

如果没有错误发生,则返回201代码和 uid of the user created.

import {Request, Response} from“express”;
import * as admin from 'firebase-admin'

导出异步函数创建(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)
   }
}

函数handleError(res: Response, err: any) {
   return res.status(500).send({ message: `${err.code} - ${err.message}` });
}

现在,让我们通过添加授权来保护处理程序. 要做到这一点,我们将向我们的 create endpoint. With express.js, you can set a chain of handlers that will be executed in order. 在处理程序中,您可以执行代码并将其传递给 next() handler or return a response. 我们要做的是首先验证用户,然后验证它是否被授权执行.

On file src/users/routes-config.ts:

//...
import { isAuthenticated } from "../auth/authenticated";
import { isAuthorized } from "../auth/authorized";

导出routesConfig(app: Application) {
   app.post('/users',
       isAuthenticated,
       isAuthorized({hasRole: ['admin', 'manager']}),
       create
   );
}

Let’s create the files src/auth/authenticated.ts.

在这个函数上,我们将验证 authorization bearer token in the request header. Then we’ll decode it with admin.auth().verifyidToken() and persist the user’s uid, role, and email in the res.locals 变量,稍后我们将使用它来验证授权.

In the case the token is invalid, we return a 401 response to the client:

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' });
   }
}

Now, let’s create a src/auth/authorized.ts file.

在这个处理程序中,我们从中提取用户信息 res.locals 我们设置先前并验证它是否具有执行操作所需的角色,或者在操作允许同一用户执行的情况下, we validate that the ID on the request params is the same as the one in the auth token. If the user doesn’t have the required role, we’ll return a 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();
   }
}

使用这两个方法,我们将能够对请求进行身份验证并在给定的情况下对它们进行授权 role in the incoming token. That’s great, but since Firebase doesn’t let us set custom claims from the project console,我们将无法执行这些端点. 为了绕过这个问题,我们可以从Firebase身份验证控制台创建一个根用户

从Firebase身份验证控制台创建用户

And set an email comparison in the code. Now, when firing requests from this user, we’ll be able to execute all operations.

//...
  const { role, email, uid } = res.locals
  const { id } = req.params

  if (email === 'your-root-user-email@domain.com')
    return next();
//...

现在,让我们将其余的CRUD操作添加到 src/users/routes-config.ts.

用于获取或更新单个用户的操作 :id param is sent, we also allow the same user to execute the operation.

导出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
   ]);
}

And on src/users/controller.ts. In these operations, 我们利用管理SDK与Firebase身份验证进行交互并执行相应的操作. As we did previously on create 操作时,对每个操作返回一个有意义的HTTP代码.

For the update operation, we validate all fields present and override customClaims with those sent in the request:

//..

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
    }
}

导出异步函数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)
   }
}

导出异步函数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)
   }
}

//...

Now we can run the function locally. To do that, first you need to set up the account key 以便能够在本地连接认证API. Then run:

npm run serve

Deploy the API

Great! 现在我们已经使用Firebase的基于角色的身份验证API编写了应用程序, 我们可以将它部署到网络上并开始使用它. 使用Firebase进行部署非常简单,我们只需要运行 firebase deploy. Once the deploy is completed, we can access our API at the published URL.

Running the firebase deploy command

You can check the API URL at http://console.firebase.google.com/u/0/project/{项目}/功能/列表.

The API URL at the Firebase Console

就我而言,它是[http://us-central1-joaq-lab].cloudfunctions.net/api].

Consuming the API

Once our API is deployed, 在本教程中,我们有几种方法来使用它, 我将介绍如何通过Postman或从Angular应用中使用它.

If we enter the List All Users URL (/api/users) on any browser, we’ll get the following:

Firebase Authentication API

The reason for this is when sending the request from a browser, 我们正在执行一个没有验证头的GET请求. 这意味着我们的API实际上是按预期工作的!

Our API is secured via tokens—in order to generate such a token, we need to call Firebase’s Client SDK and log in with a valid user/password credential. When successful, Firebase将在响应中发送一个令牌,然后我们可以将其添加到我们想要执行的任何后续请求的标头中.

From an Angular App

在本教程中,我将介绍从Angular应用中使用API的重要部分. The full repository can be accessed here, 如果你需要一个循序渐进的教程,教你如何创建Angular应用并配置@angular/fire来使用, it you can check this post.

So, back to signing in, we’ll have a SignInComponent with a

让用户输入用户名和密码.

//...
 

   
//...

And on the class, we signInWithEmailAndPassword using the AngularFireAuth service.

 //... 

 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(电子邮件、密码)
   } catch (err) {
     console.log(err)
   }
 }

 //..

此时,我们可以登录到Firebase项目.

Signing in through the Angular app

从Angular应用中登录时的API响应

当我们在DevTools中检查网络请求时, we can see that Firebase returns a token after verifying our user and password.

我们将使用这个令牌将我们的头请求发送到我们构建的API. 将令牌添加到所有请求的一种方法是使用 HttpInterceptor.

This file shows how to get the token from AngularFireAuth and add it to the header’s request. 然后,我们在AppModule中提供拦截器文件.

http-interceptors/auth-token.interceptor.ts

@Injectable({ providedIn: 'root' })
export class AuthTokenHttpInterceptor implements HttpInterceptor {

   constructor(
       private auth: AngularFireAuth
   ) {

   }

   intercept(req: HttpRequest, next: HttpHandler): Observable> {
       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)
           })
       )

   }
}

导出const authtokenttpinterceptorprovider = {
   provide: HTTP_INTERCEPTORS,
   useClass: AuthTokenHttpInterceptor,
   multi: true
}

app.module.ts

@NgModule({
 //..
 providers: [
   AuthTokenHttpInterceptorProvider
 ]
 //...
})
export class AppModule { }

Once the interceptor is set, we can make requests to our API from httpClient. For example, here’s a UsersService 其中我们调用列表all users,根据用户ID获取用户,创建用户,并更新用户.

//…

导出类型CreateUserRequest = {displayName:字符串, password: string, email: string, role: string }
导出类型UpdateUserRequest = {uid: string} & CreateUserRequest

@Injectable({
 providedIn: 'root'
})
export class UserService {

 private baseUrl = '{your-functions-url}/api/users'

 constructor(
   private http: HttpClient
 ) { }
  get users$(): Observable {
   return this.http.get<{ users: User[] }>(`${this.baseUrl}`).pipe(
     map(result => {
       return result.users
     })
   )
 }

 user$(id: string): Observable {
   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)
 }

}

Now, 我们可以通过调用API来获取登录用户的ID,并列出组件中的所有用户,如下所示:

//...

  

Me

  • {{user.displayName}}
    {{user.email}}
    {{user.role?.toUpperCase()}}

All Users

  • {{user.displayName}}
    {{user.email}} {{user.uid}}
    {{user.role?.toUpperCase()}}
//...
//...

 users$: Observable
 user$: Observable

 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))
   )
 }

//...

And here’s the result.

All the users in our Angular app

Notice that if we sign in with a user with role=user, only the Me section will be rendered.

The view of the user resource that the user with the role user has access to

我们会在网络检查员那里查到403. 这是由于我们之前在API上设置的限制,只允许“Admins”列出所有用户.

A 403 error in the network inspector

Now, let’s add the “create user” and “edit user” functionality. In order to do that, let’s create first a UserFormComponent and a UserFormService.





@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
  styleUrls: ['./user-form.component.scss']
})
导出类UserFormComponent实现OnInit {

  form = new FormGroup({
    uid: new FormControl(''),
    email: new FormControl(''),
    displayName: new FormControl(''),
    password: new FormControl(''),
    role: new FormControl(''),
  });
  title$: Observable;
  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});
  }

}
从“@angular/core”中导入{Injectable};
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)
    );
  }
}

Back in the main component, let’s add the buttons to call those actions. In this case, “Edit User” will only be available for the logged-in user. You can go ahead and add the functionality to edit other users if you need to!

//...

    

Me

//...

All Users

//...
//...
  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 => {

    });

  }

From Postman

Postman是一个构建和向api发出请求的工具. 通过这种方式,我们可以模拟从任何客户端应用程序或不同的服务调用API.

我们将演示的是如何发送一个请求来列出所有用户.

Once we open the tool, we set the URL http://us-central1-{your-project}.cloudfunctions.net/api/users:

The API URL loaded in the Postman field ready to fire as a GET request

Next, on the tab authorization, we choose Bearer Token and we set the value we extracted from Dev Tools previously.

Setting the bearer token in Postman

The body of the response we receive

Conclusion

Congratulations! 您已经完成了整个教程,现在您已经学习了如何在Firebase上创建基于用户角色的API.

We’ve also covered how to consume it from an Angular app and Postman.

Let’s recap the most important things:

  1. Firebase allows you to get quickly up and running with an enterprise-level auth API, which you can extend later on.
  2. 几乎每个项目都需要授权——如果您需要使用基于角色的模型来控制访问的话, Firebase身份验证可以让您快速入门.
  3. 基于角色的模型依赖于验证从具有特定角色的用户请求的资源. specific users.
  4. Using an Express.js app on Firebase Function, we can create a REST API and set handlers to authenticate and authorize requests.
  5. 利用内置的自定义声明,您可以创建基于角色的身份验证API并保护您的应用程序.

You can read further about Firebase auth here. And if you want to leverage the roles we have defined, you can use @angular/fire helpers.

Understanding the basics

  • Is Firebase Auth a REST API?

    Firebase Auth是一项服务,它允许你的应用程序注册和验证用户对多个提供商,如谷歌, Facebook, Twitter, GitHub and more). Firebase Auth提供sdk,您可以使用这些sdk轻松地与web、Android和iOS集成. Firebase Auth也可以作为REST API使用

  • What is Firebase used for?

    Firebase是一套云产品,可帮助您快速构建无服务器移动或web应用程序. It provides most of the common services involved on every app (database, authorization, storage, hosting).

  • How do I get Firebase Auth API?

    You can create a project with your Google account at firebase.google.com. 一旦项目被创建,你就可以打开Firebase Auth并开始在你的应用中使用它.

  • Which is better, Firebase or AWS?

    Firebase is Google-backed product, and one of which Google is trying to grow and add more and more features. AWS Amplify is a similar product, mostly targeted to mobile apps. Both are great products, with Firebase being an older product with more features.

  • Is Firebase easy to use?

    Firebase是一个完全托管的服务,您可以非常轻松地开始使用它,并且在需要扩展时不必担心基础设施. 有很多很棒的文档和博客文章,其中有示例,可以快速了解它是如何工作的.

  • Is Firebase good for large databases?

    Firebase有两个数据库:Realtime Database和Firestore. Both are NoSQL databases with similar features and different pricing models. Firestore支持更好的查询功能,而且这两个数据库的设计使得查询延迟不受数据库大小的影响.

Hire a Toptal expert on this topic.
Hire Now
Joaquin Cid

Joaquin Cid

Verified Expert in Engineering

Rosario, Santa Fe Province, Argentina

Member since May 2, 2018

About the author

Joaquin是一名全栈开发人员,在WebMD和Getty Images等公司拥有超过12年的工作经验.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Velocity Partners

World-class articles, delivered weekly.

输入您的电子邮件,即表示您同意我们的 privacy policy.

World-class articles, delivered weekly.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal Developers

Join the Toptal® community.