Neste tutorial, construiremos uma API REST para gerenciar usuários e funções usando Firebase e Node.js. Além disso, veremos como usar a API para autorizar (ou não) quais usuários podem acessar recursos específicos.
Quase todo aplicativo requer algum nível de sistema de autorização. Em alguns casos, validar um nome de usuário / senha definido com nossa tabela de Usuários é suficiente, mas frequentemente, precisamos de um modelo de permissões mais refinado para permitir que certos usuários acessem certos recursos e os restrinja de outros. Construir um sistema para suportar o último não é trivial e pode consumir muito tempo. Neste tutorial, aprenderemos como construir uma API de autenticação baseada em papéis usando o Firebase, o que nos ajudará a começar a funcionar rapidamente.
Neste modelo de autorização, o acesso é concedido a funções, em vez de usuários específicos, e um usuário pode ter uma ou mais dependendo de como você projeta seu modelo de permissão. Os recursos, por outro lado, requerem certas funções para permitir que um usuário os execute.
Em suma, o Firebase Authentication é um sistema de autenticação baseado em token extensível e fornece integrações prontas para uso com os provedores mais comuns, como Google, Facebook e Twitter, entre outros.
Ele nos permite usar declarações personalizadas que usaremos para construir uma API flexível baseada em funções.
Podemos definir qualquer valor JSON nas declarações (por exemplo, { role: 'admin' }
ou { role: 'manager' }
).
Depois de definidas, as declarações personalizadas serão incluídas no token que o Firebase gera, e podemos ler o valor para controlar o acesso.
Ele também vem com uma cota gratuita muito generosa, que na maioria dos casos será mais do que suficiente.
As funções são um serviço de plataforma sem servidor totalmente gerenciado. Precisamos apenas escrever nosso código em Node.js e implantá-lo. O Firebase cuida do dimensionamento da infraestrutura sob demanda, configuração do servidor e muito mais. Em nosso caso, vamos usá-lo para construir nossa API e expô-lo via HTTP para a web.
O Firebase nos permite definir express.js
aplicativos como manipuladores para caminhos diferentes - por exemplo, você pode criar um aplicativo Express e conectá-lo a /mypath
, e todas as solicitações que chegam a esta rota serão manipuladas pelo app
configurado.
No contexto de uma função, você tem acesso a toda a API Firebase Authentication, usando o SDK Admin.
É assim que vamos criar a API do usuário.
Então, antes de começar, vamos dar uma olhada no que iremos construir. Vamos criar uma API REST com os seguintes endpoints:
Http Verb | Caminho | Descrição | Autorização |
---|---|---|---|
OBTER | /Comercial | Lista todos os usuários | Apenas administradores e gerentes têm acesso |
POSTAR | /Comercial | Cria novo usuário | Apenas administradores e gerentes têm acesso |
OBTER | / users /: id | Obtém o usuário: id | Administradores, gerentes e o mesmo usuário que: id têm acesso |
FRAGMENTO | / users /: id | Atualiza o usuário: id | Administradores, gerentes e o mesmo usuário que: id têm acesso |
EXCLUIR | / users /: id | Exclui o usuário: id | Administradores, gerentes e o mesmo usuário que: id têm acesso |
Cada um desses endpoints tratará da autenticação, validará a autorização, executará a operação correspondente e, finalmente, retornará um código HTTP significativo.
Criaremos as funções de autenticação e autorização necessárias para validar o token e verificar se as declarações contêm a função necessária para executar a operação.
Para construir a API, precisamos:
firebase-tools
instaladoPrimeiro, faça login no Firebase:
firebase login
Em seguida, inicialize um projeto do 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
Neste ponto, você terá uma pasta de funções, com configuração mínima para criar funções do Firebase.
Em src/index.ts
há um helloWorld
exemplo, que você pode descomentar para validar que suas funções funcionam. Então você pode cd functions
e execute npm run serve
. Este comando irá transpilar o código e iniciar o servidor local.
Você pode verificar os resultados em http: // localhost: 5000 / {seu-projeto} / us-central1 / helloWorld
como é um vazamento de memória
Observe que a função é exposta no caminho definido como o nome dela em 'index.ts: 'helloWorld'
.
Agora vamos codificar nossa API. Vamos criar uma função http Firebase e conectá-la a /api
caminho.
Primeiro, instale npm install express
.
No src/index.ts
vamos:
admin.initializeApp();
api
endpoint httpsimport * 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);
Agora, todas as solicitações indo para /api
será tratado pelo app
instância.
A próxima coisa que faremos é configurar o app
instância para suportar CORS e adicionar middleware de analisador de corpo JSON. Dessa forma, podemos fazer solicitações de qualquer URL e analisar solicitações formatadas em JSON.
Instalaremos primeiro as dependências necessárias.
npm install --save cors body-parser
npm install --save-dev @types/cors
E depois:
//... 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);
Por fim, configuraremos as rotas que app
vai lidar com.
//... import { routesConfig } from './users/routes-config'; //… app.use(cors({ origin: true })); routesConfig(app) export const api = functions.https.onRequest(app);
O Firebase Functions permite definir um aplicativo Express como o manipulador e qualquer caminho após aquele que você configurou em functions.https.onRequest(app);
—neste caso, api
—também será tratado pelo app
. Isso nos permite escrever pontos de extremidade específicos, como api/users
e definir um manipulador para cada verbo HTTP, o que faremos a seguir.
Vamos criar o arquivo src/users/routes-config.ts
Aqui, definiremos um create
manipulador em POST '/users'
import { Application } from 'express'; import { create} from './controller'; export function routesConfig(app: Application) { app.post('/users', create ); }
Agora, vamos criar o src/users/controller.ts
Arquivo.
Nessa função, primeiro validamos se todos os campos estão na solicitação de corpo e, em seguida, criamos o usuário e definimos as declarações personalizadas.
Estamos apenas passando { role }
no setCustomUserClaims
—os outros campos já estão definidos pelo Firebase.
Se nenhum erro ocorrer, retornamos um código 201 com uid
do usuário criado.
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}` }); }
Agora, vamos proteger o manipulador adicionando autorização. Para fazer isso, adicionaremos alguns manipuladores ao nosso create
ponto final. Com express.js
, você pode definir uma cadeia de manipuladores que serão executados em ordem. Em um manipulador, você pode executar o código e passá-lo para o next()
manipulador ou retornar uma resposta. O que vamos fazer é primeiro autenticar o usuário e depois validar se ele está autorizado a executar.
No arquivo 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 ); }
Vamos criar os arquivos src/auth/authenticated.ts
.
Nesta função, validaremos a presença de authorization
token do portador no cabeçalho da solicitação. Então iremos decodificá-lo com admin.auth().verifyidToken()
e persistir o usuário uid
, role
e email
no res.locals
variável, que usaremos posteriormente para validar a autorização.
Caso o token seja inválido, retornamos uma resposta 401 ao cliente:
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' }); } }
Agora, vamos criar um src/auth/authorized.ts
Arquivo.
Neste manipulador, extraímos as informações do usuário de res.locals
definimos previamente e validamos se ele tem a função necessária para executar a operação ou no caso de a operação permitir que o mesmo usuário execute, validamos se o ID nos parâmetros de solicitação é o mesmo do token de autenticação. Se o usuário não tiver a função exigida, retornaremos um 403.
import { Request, Response } from 'express'; export function isAuthorized(opts: { hasRole: Array, 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(); } }
Com esses dois métodos, seremos capazes de autenticar solicitações e autorizá-las de acordo com o role
no token de entrada. Isso é ótimo, mas como o Firebase não nos permite definir reivindicações personalizadas do projeto console , não poderemos executar nenhum desses endpoints. Para contornar isso, podemos criar um usuário root no Firebase Authentication Console
E defina uma comparação de e-mail no código. Agora, ao disparar solicitações deste usuário, poderemos executar todas as operações.
//... const { role, email, uid } = res.locals const { id } = req.params if (email === ' [email protected] ') return next(); //...
Agora, vamos adicionar o resto das operações CRUD a src/users/routes-config.ts
.
Para operações de obter ou atualizar um único usuário, onde :id
param for enviado, também permitimos que o mesmo usuário execute a operação.
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 ]); }
E em src/users/controller.ts
. Nessas operações, aproveitamos o SDK do administrador para interagir com o Firebase Authentication e realizar as respectivas operações. Como fizemos anteriormente em create
operação, retornamos um código HTTP significativo em cada operação.
Para a operação de atualização, validamos todos os campos presentes e substituímos customClaims
com aqueles enviados na solicitação:
//.. 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 } 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) } } //...
Agora podemos executar a função localmente. Para fazer isso, primeiro você precisa configurar a chave da conta para poder se conectar com a API de autenticação localmente. Então corra:
npm run serve
Ótimo! Agora que escrevemos a API baseada em funções, podemos implantá-la na web e começar a usá-la. A implantação com o Firebase é super fácil, só precisamos executar firebase deploy
. Assim que a implantação for concluída, podemos acessar nossa API no URL publicado.
Você pode verificar o URL da API em https://console.firebase.google.com/u/0/project/[your-project concepts/functions/list .
No meu caso é https://us-central1-joaq-lab.cloudfunctions.net/api .
Depois que nossa API for implantada, temos várias maneiras de usá-la - neste tutorial, vou cobrir como usá-la via Postman ou de um aplicativo Angular.
Se inserirmos o URL Listar todos os usuários (/api/users
) em qualquer navegador, obteremos o seguinte:
A razão para isso é que, ao enviar a solicitação de um navegador, estamos executando uma solicitação GET sem cabeçalhos de autenticação. Isso significa que nossa API está realmente funcionando conforme o esperado!
Nossa API é protegida por tokens - para gerar tal token, precisamos chamar o SDK do cliente do Firebase e fazer login com uma credencial de usuário / senha válida. Quando for bem-sucedido, o Firebase enviará um token de volta na resposta que podemos adicionar ao cabeçalho de qualquer solicitação a seguir que quisermos realizar.
Neste tutorial, examinarei apenas as partes importantes para consumir a API de um aplicativo Angular. O repositório completo pode ser acessado Aqui , e se você precisar de um tutorial passo a passo sobre como criar um aplicativo Angular e configurar @ angular / fire para usar, você pode verificar isto postar .
Então, voltando ao login, teremos um SignInComponent
com um permite que o usuário insira um nome de usuário e uma senha.
//... Email address Password //...
E na aula, nós signInWithEmailAndPassword
usando AngularFireAuth
serviço.
//... 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) } } //..
Neste ponto, podemos fazer login em nosso projeto Firebase.
E quando inspecionamos as solicitações de rede no DevTools, podemos ver que o Firebase retorna um token após verificar nosso usuário e senha.
Esse token é o que usaremos para enviar a solicitação de nosso cabeçalho à API que criamos. Uma maneira de adicionar o token a todas as solicitações é usando um HttpInterceptor
.
Este arquivo mostra como obter o token de AngularFireAuth
e adicione-o à solicitação do cabeçalho. Em seguida, fornecemos o arquivo interceptor no 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) }) ) } } export const AuthTokenHttpInterceptorProvider = { provide: HTTP_INTERCEPTORS, useClass: AuthTokenHttpInterceptor, multi: true }
app.module.ts
@NgModule({ //.. providers: [ AuthTokenHttpInterceptorProvider ] //... }) export class AppModule { }
Assim que o interceptor estiver definido, podemos fazer solicitações à nossa API de httpClient
. Por exemplo, aqui está um UsersService
onde chamamos a lista de todos os usuários, obtemos o usuário por seu ID, criamos um usuário e atualizamos um usuário.
//… 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 { return this.http.get(`${this.baseUrl}`).pipe( map(result => { return result.users }) ) } user$(id: string): Observable { return this.http.get(`${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) } }
Agora, podemos chamar a API para obter o usuário conectado por seu ID e listar todos os usuários de um componente como este:
//... 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)) ) } //...
E aqui está o resultado.
Observe que se entrarmos com um usuário com role=user
, apenas a seção Eu será renderizada.
E obteremos um 403 no inspetor de rede. Isso se deve à restrição que definimos anteriormente na API para permitir apenas que “Administradores” listem todos os usuários.
Agora, vamos adicionar as funcionalidades “criar usuário” e “editar usuário”. Para fazer isso, vamos criar primeiro um UserFormComponent
e um UserFormService
.
{ async}
Email address Password Display Name Role Admin Manager User Cancel Save
@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; 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) ); } }
De volta ao componente principal, vamos adicionar os botões para chamar essas ações. Nesse caso, “Editar usuário” estará disponível apenas para o usuário conectado. Você pode ir em frente e adicionar a funcionalidade para editar outros usuários, se necessário!
//... Me
Edit Profile //... All Users
New User //...
//... 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 é uma ferramenta para construir e fazer solicitações a APIs. Desta forma, podemos simular que estamos chamando nossa API de qualquer aplicativo cliente ou serviço diferente.
O que vamos demonstrar é como enviar uma solicitação para listar todos os usuários.
Depois de abrir a ferramenta, definimos o URL https: // us-central1- {your-project} .cloudfunctions.net / api / users :
A seguir, na autorização da aba, escolhemos Bearer Token e configuramos o valor que extraímos das Ferramentas de Desenvolvimento anteriormente.
como fazer um token
Parabéns! Você concluiu todo o tutorial e agora aprendeu a criar uma API baseada na função do usuário no Firebase.
Também abordamos como consumi-lo em um aplicativo Angular e no Postman.
Vamos recapitular as coisas mais importantes:
Você pode ler mais sobre o Firebase auth Aqui . E se você quiser alavancar as funções que definimos, você pode usar @ angular / fire ajudantes .
Firebase Auth é um serviço que permite que seu aplicativo se inscreva e autentique um usuário em vários provedores, como (Google, Facebook, Twitter, GitHub e mais). O Firebase Auth fornece SDKs com os quais você pode se integrar facilmente com a Web, Android e iOS. Firebase Auth também pode ser consumido como uma API REST
O Firebase é um pacote de produtos em nuvem que ajuda a criar um aplicativo móvel ou web sem servidor muito rapidamente. Ele fornece a maioria dos serviços comuns envolvidos em cada aplicativo (banco de dados, autorização, armazenamento, hospedagem).
Você pode criar um projeto com sua conta do Google em firebase.google.com. Depois de criar o projeto, você pode ativar o Firebase Auth e começar a usá-lo no seu aplicativo.
O Firebase é um produto apoiado pelo Google, e um dos quais o Google está tentando expandir e adicionar mais e mais recursos. O AWS Amplify é um produto semelhante, voltado principalmente para aplicativos móveis. Ambos são ótimos produtos, com o Firebase sendo um produto mais antigo com mais recursos.
O Firebase é um serviço totalmente gerenciado com o qual você pode começar com muita facilidade e não se preocupar com a infraestrutura quando precisar escalonar. Há uma grande quantidade de documentação e postagens de blog com exemplos para aprender rapidamente como funciona.
O Firebase tem dois bancos de dados: Realtime Database e Firestore. Ambos são bancos de dados NoSQL com recursos semelhantes e modelos de preços diferentes. O Firestore oferece suporte a melhores recursos de consulta e ambos os bancos de dados são projetados para que a latência de consulta não seja afetada pelo tamanho do banco de dados.