作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Joaquin是一名全栈开发人员,在WebMD和Getty Images等公司拥有超过12年的工作经验.
在本教程中,我们将构建一个Node.使用Firebase认证REST API来管理用户和角色. In addition, 我们将看到如何使用API来授权(或不授权)哪些用户可以访问特定的资源.
几乎每个应用程序都需要某种程度的授权系统. 在某些情况下,使用 Users
table is enough, but often, 我们需要一个更细粒度的权限模型,以允许某些用户访问某些资源,并限制他们访问其他资源. 构建一个支持后者的系统并不简单,而且可能非常耗时. 在本教程中, 我们将学习如何使用Firebase构建基于角色的身份验证API, 这将帮助我们快速启动和运行.
在这个授权模型中, 将访问权限授予角色, 而不是特定的用户, 用户可以有一个或多个,这取决于您如何设计权限模型. 另一方面,资源需要特定的角色来允许用户执行它.
In a nutshell, Firebase身份验证是一个可扩展的基于令牌的身份验证系统,并提供了与最常见的提供商(如Google)的开箱即用集成, Facebook, and Twitter, among others.
它使我们能够使用自定义声明,我们将利用它来构建灵活的基于角色的API. 这些声明可以被认为是Firebase用户角色,将直接映射到我们的应用程序所支持的角色.
我们可以在声明中设置任何JSON值(例如.g., {角色:'admin'}
or {角色:'经理'}
).
Once set, 自定义声明将包含在生成的Firebase令牌中, 我们可以读这个值来控制访问.
它还提供了非常慷慨的免费配额,在大多数情况下都绰绰有余.
Functions是一个完全托管的无服务器平台服务. 我们只需要在Node中编写代码.Js并部署它. Firebase负责按需扩展基础设施、服务器配置等. 在我们的例子中,我们将使用它来构建我们的API,并通过HTTP向web公开它.
Firebase允许我们设置 express.js
应用程序作为不同路径的处理程序——例如,你可以创建一个Express应用程序并将其挂接到 /mypath
,并且所有到达此路由的请求都将由 app
configured.
从函数的上下文中, 你可以访问整个Firebase身份验证API, 使用管理SDK.
这就是我们创建用户API的方式.
所以在我们开始之前,让我们看看我们将构建什么. 我们将创建一个带有以下端点的REST API:
Http Verb | Path | Description | Authorization |
---|---|---|---|
GET | /users | 列出所有用户 | 只有管理员和管理员可以访问 |
POST | /users | 创建新用户 | 只有管理员和管理员可以访问 |
GET | /users/:id | 获取:id用户 | 管理员、管理员和与:id相同的用户都可以访问 |
PATCH | /users/:id | 更新:id用户 | 管理员、管理员和与:id相同的用户都可以访问 |
DELETE | /users/:id | 删除:id用户 | 管理员、管理员和与:id相同的用户都可以访问 |
每个端点都将处理身份验证, 验证授权, 执行相应的操作, 最后返回一个有意义的HTTP代码.
我们将创建验证令牌所需的身份验证和授权函数,并检查声明是否包含执行操作所需的角色.
为了构建API,我们需要:
firebase-tools
installed首先,登录Firebase:
firebase login
接下来,初始化一个Functions项目:
firebase init
? 您想为此文件夹设置哪些Firebase CLI特性? ...
(O)功能:配置和部署云功能
? 为此目录选择默认Firebase项目:{your-project}
? 你想用什么语言来写云函数? TypeScript
? 你想使用TSLint来捕获可能的错误并强制执行样式吗? Yes
? 你想现在用npm安装依赖吗? Yes
此时,您将拥有一个Functions文件夹,它具有创建重火力点功能所需的最小设置.
At src/index.ts
there’s a helloWorld
示例,您可以取消注释以验证函数是否正常工作. Then you can cd functions
and run npm run serve
. 该命令将编译代码并启动本地服务器.
你可以在 http://localhost:5000/{your-project}/us-central1/helloWorld
注意,函数在定义为它的名称的路径上公开 'index.ts:“helloWorld”
.
现在让我们编写API. 我们将创建一个http Firebase函数并将其挂接 /api
path.
First, install NPM install express
.
On the src/index.ts
we will:
admin.initializeApp ();
api
http endpoint从'firebase-functions'中导入* as函数;
从'firebase-admin'中导入*作为admin;
从“express”中导入* as express;
admin.initializeApp ();
Const app = express();
导出const API =函数.http.onRequest(应用);
现在,所有的请求都要 /api
将由 app
instance.
接下来我们要做的是配置 app
实例来支持CORS并添加JSON主体解析器中间件. 这样我们就可以从任何URL发出请求并解析JSON格式的请求.
我们将首先安装所需的依赖项.
NPM install——save cors body-parser
NPM install——save-dev @types/cors
And then:
//...
从'cors'中导入* as cors;
从'body-parser'中导入*作为bodyParser;
//...
Const app = express();
app.use(bodyParser.json());
app.使用(cors({origin: true}));
导出const API =函数.http.onRequest(应用);
最后,我们将配置路由 app
will handle.
//...
import {routesConfig} from./用户/ routes-config”;
//…
app.使用(cors({origin: true}));
routesConfig(应用)
导出const API =函数.http.onRequest(应用);
重火力点功能允许我们将Express应用设置为处理程序, 在你设置的路径之后的任何路径 functions.http.onRequest(应用);
—in this case, api
-也将由 app
. 这允许我们编写特定的端点,例如 api/users
并为每个HTTP动词设置处理程序,这是我们接下来要做的.
让我们创建这个文件 src /用户/ routes-config.ts
这里,我们设置a create
handler at POST '/users'
从“express”中导入{Application};
导入{create}./controller";
导出routesConfig(app: Application) {
app.post('/users',
create
);
}
现在,我们将创建 src /用户/控制器.ts
file.
在这个函数中, 我们首先验证所有字段都在请求体中, and next, 我们创建用户并设置自定义声明.
我们只是路过 { role }
in the setCustomUserClaims
-其他字段已经由Firebase设置.
如果没有错误发生,则返回201代码和 uid
所创建用户的.
import {Request, Response} from“express”;
从firebase-admin中导入* as 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} =等待admin.auth().createUser({
displayName,
password,
email
})
await admin.auth().setCustomUserClaims(uid, {role})
return res.status(201).send({ uid })
} catch (err) {
返回handleError(res, err)
}
}
函数handleError(res: Response, err: any) {
return res.status(500).发送({消息:' ${错误.code} - ${err.message}` });
}
现在,让我们通过添加授权来保护处理程序. 要做到这一点,我们将向我们的 create
endpoint. With express.js
,您可以设置将按顺序执行的处理程序链. 在处理程序中,您可以执行代码并将其传递给 next()
处理程序或返回响应. 我们要做的是首先验证用户,然后验证它是否被授权执行.
On file src /用户/ routes-config.ts
:
//...
导入{isAuthenticated}../身份验证/认证”;
导入{isAuthorized}../认证/授权”;
导出routesConfig(app: Application) {
app.post('/users',
isAuthenticated,
isAuthorized({hasRole: ['admin', 'manager']}),
create
);
}
让我们创建这些文件 src /身份验证/认证.ts
.
在这个函数上,我们将验证 authorization
请求头中的承载令牌. 然后我们用 admin.auth().verifyidToken ()
并坚持用户的 uid
, role
, and email
in the res.locals
变量,稍后我们将使用它来验证授权.
在令牌无效的情况下,我们向客户端返回401响应:
import {Request, Response} from“express”;
从firebase-admin中导入* as admin
导出异步函数isAuthenticated(req: Request, res: Response, next: function) {
Const {authorization} =权限.headers
if (!authorization)
return res.status(401).send({message: 'Unauthorized'});
if (!authorization.startsWith(持票人))
return res.status(401).send({message: 'Unauthorized'});
Const split = authorization.split(持票人)
if (split.length !== 2)
return res.status(401).send({message: 'Unauthorized'});
Const token = split[1]
try {
const decodedToken: admin.auth.DecodedIdToken =等待admin.auth().verifyIdToken(令牌);
console.日志(“decodedToken JSON.stringify (decodedToken))
res.locals = { ...res.uid: decodedToken.uid, role: decodedToken.角色,邮箱:decodedToken.email }
return next();
}
catch (err) {
console.error(`${err.code} - ${err.message}`)
return res.status(401).send({message: 'Unauthorized'});
}
}
现在,我们来创建一个 src /认证/授权.ts
file.
在这个处理程序中,我们从中提取用户信息 res.locals
我们设置先前并验证它是否具有执行操作所需的角色,或者在操作允许同一用户执行的情况下, 我们验证请求参数上的ID与auth令牌中的ID是否相同. 如果用户没有所需的角色,我们将返回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.包括(角色))
return next();
return res.status(403).send();
}
}
使用这两个方法,我们将能够对请求进行身份验证并在给定的情况下对它们进行授权 role
在传入令牌中. 这很棒,但由于Firebase不允许我们从项目中设置自定义声明 console,我们将无法执行这些端点. 为了绕过这个问题,我们可以从Firebase身份验证控制台创建一个根用户
并在代码中设置一个电子邮件比较. 现在,当触发来自该用户的请求时,我们将能够执行所有操作.
//...
Const {role, email, uid} = res.locals
Const {id} = req.params
If (email === 'your-root-user-email@domain . '.com')
return next();
//...
现在,让我们将其余的CRUD操作添加到 src /用户/ routes-config.ts
.
用于获取或更新单个用户的操作 :id
参数被发送,我们也允许相同的用户执行操作.
导出routesConfig(app: Application) {
//..
//列出所有用户
app.get(' /用户的,
isAuthenticated,
isAuthorized({hasRole: ['admin', 'manager']}),
all
]);
//获取:id user
app.get(“/用户/:id”,
isAuthenticated,
isAuthorized({hasRole: ['admin', 'manager'], allowSameUser: true}),
get
]);
//更新:id user
app.补丁(“/用户/:id”,
isAuthenticated,
isAuthorized({hasRole: ['admin', 'manager'], allowSameUser: true}),
patch
]);
//删除:id user
app.删除(“/用户/:id”,
isAuthenticated,
isAuthorized({hasRole: ['admin', 'manager']}),
remove
]);
}
And on src /用户/控制器.ts
. 在这些操作中, 我们利用管理SDK与Firebase身份验证进行交互并执行相应的操作. 就像我们之前做的那样 create
操作时,对每个操作返回一个有意义的HTTP代码.
对于更新操作,我们验证所有存在的字段并覆盖它们 customClaims
在请求中发送的那些:
//..
export async function all(req: Request, res: Response) {
try {
const listUsers =等待admin.auth().listUsers()
const users = listUsers.users.map(mapUser)
return res.status(200).发送({users})
} catch (err) {
返回handleError(res, err)
}
}
function mapUser(user: admin).auth.UserRecord) {
const customClaims =(用户.customClaims || {role: "})作为{role?: string }
const role = customClaims.role ? customClaims.role : ''
return {
uid: user.uid,
email: user.email || '',
displayName:用户.displayName || ",
role,
lastSignInTime:用户.metadata.lastSignInTime,
creationTime:用户.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) {
返回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) {
返回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) {
返回handleError(res, err)
}
}
//...
现在我们可以在本地运行这个函数了. 要做到这一点,首先你需要 设置帐号密钥 以便能够在本地连接认证API. Then run:
npm run serve
Great! 现在我们已经使用Firebase的基于角色的身份验证API编写了应用程序, 我们可以将它部署到网络上并开始使用它. 使用Firebase进行部署非常简单,我们只需要运行 firebase deploy
. 一旦部署完成,我们就可以通过发布的URL访问我们的API.
您可以在这里查看API URL http://console.firebase.google.com/u/0/project/{项目}/功能/列表.
就我而言,它是[http://us-central1-joaq-lab].cloudfunctions.net/api].
一旦我们的API被部署, 在本教程中,我们有几种方法来使用它, 我将介绍如何通过Postman或从Angular应用中使用它.
如果我们输入List All Users URL (/api/users
),我们将得到以下结果:
这样做的原因是从浏览器发送请求时, 我们正在执行一个没有验证头的GET请求. 这意味着我们的API实际上是按预期工作的!
为了生成这样的令牌,我们的API是通过令牌来保护的, 我们需要调用Firebase的客户端SDK并使用有效的用户/密码凭证登录. When successful, Firebase将在响应中发送一个令牌,然后我们可以将其添加到我们想要执行的任何后续请求的标头中.
在本教程中,我将介绍从Angular应用中使用API的重要部分. 可以访问完整的存储库 here, 如果你需要一个循序渐进的教程,教你如何创建Angular应用并配置@angular/fire来使用, 如果你能检查一下 post.
回到签到,我们会有 SignInComponent
with a 让用户输入用户名和密码.
//...
//...
在课堂上,我们 signInWithEmailAndPassword
using the AngularFireAuth
service.
//...
form: FormGroup = new FormGroup({
email: new FormControl("),
password: new FormControl(")
})
constructor(
私有auth: AngularFireAuth
) { }
async signIn() {
try {
Const {email, password} = this.form.value
await this.afAuth.auth.signInWithEmailAndPassword(电子邮件、密码)
} catch (err) {
console.log(err)
}
}
//..
此时,我们可以登录到Firebase项目.
当我们在DevTools中检查网络请求时, 我们可以看到Firebase在验证我们的用户和密码后返回一个令牌.
我们将使用这个令牌将我们的头请求发送到我们构建的API. 将令牌添加到所有请求的一种方法是使用 HttpInterceptor
.
这个文件显示了如何从 AngularFireAuth
并将其添加到header的请求中. 然后,我们在AppModule中提供拦截器文件.
http-interceptors /鉴定标识.interceptor.ts
@Injectable({providedIn:“根”})
导出类authtokenttpinterceptor实现HttpInterceptor {
constructor(
private auth: AngularFireAuth
) {
}
intercept(req: HttpRequest, next: HttpHandler): Observable> {
return this.auth.idToken.pipe(
take(1),
switchMap(idToken => {
让clone = req.clone()
if (idToken) {
clone = clone.克隆({headers: req.headers.set('Authorization', 'Bearer ' + idToken)});
}
return next.handle(clone)
})
)
}
}
导出const authtokenttpinterceptorprovider = {
提供:HTTP_INTERCEPTORS,
useClass: AuthTokenHttpInterceptor,
multi: true
}
app.module.ts
@NgModule({
//..
providers: [
AuthTokenHttpInterceptorProvider
]
//...
})
导出类AppModule {}
一旦设置了拦截器,我们就可以从 httpClient
. 例如,这是a UsersService
其中我们调用列表all users,根据用户ID获取用户,创建用户,并更新用户.
//…
导出类型CreateUserRequest = {displayName:字符串, 密码:字符串, email: string, role: string }
导出类型UpdateUserRequest = {uid: string} & CreateUserRequest
@Injectable({
providedIn:“根”
})
导出类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
})
)
}
创建(用户:CreateUserRequest) {
return this.http.post(`${this.baseUrl}”,用户)
}
编辑(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;
私有auth: 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上设置的限制,只允许“Admins”列出所有用户.
现在,让我们添加“创建用户”和“编辑用户”功能. 为了做到这一点,我们先创建a UserFormComponent
and a UserFormService
.
{{ title$ | async}}
@Component({
选择器:“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("),
角色: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(用户);
} else {
this.form.reset({});
}
})
);
}
dismiss() {
this.modal.把(“模态驳回”);
}
save() {
const {displayName, email, role, password, uid} = this.form.value;
this.modal.close({displayName, email, role, password, uid});
}
}
从“@angular/core”中导入{Injectable};
从'rxjs'中导入{BehaviorSubject};
从'rxjs/operators'中导入{map};
@Injectable({
providedIn:“根”
})
导出类UserFormService {
_BS = new BehaviorSubject({title: ", user: {}});
构造函数(){}
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)
);
}
}
回到主组件,让我们添加按钮来调用这些操作. 在这种情况下,“编辑用户”将仅对登录用户可用. 如果需要,可以继续添加编辑其他用户的功能!
//...
Me
//...
All Users
//...
//...
create() {
this.userForm.create();
const modalRef = this.modal.打开(UserFormComponent);
modalRef.result.then(user => {
this.userService.create(user).subscribe(_ => {
console.日志(用户创建的);
});
}).catch(err => {
});
}
编辑(userToEdit) {
this.userForm.编辑(userToEdit);
const modalRef = this.modal.打开(UserFormComponent);
modalRef.result.then(user => {
this.userService.edit(user).subscribe(_ => {
console.日志(“用户编辑”);
});
}).catch(err => {
});
}
From Postman
Postman是一个构建和向api发出请求的工具. 通过这种方式,我们可以模拟从任何客户端应用程序或不同的服务调用API.
我们将演示的是如何发送一个请求来列出所有用户.
打开工具后,设置URL http://us-central1-{your-project}.cloudfunctions.net/api/users:
Next, 关于TAB授权, 我们选择承载令牌,并设置之前从Dev Tools提取的值.
Conclusion
祝贺你! 您已经完成了整个教程,现在您已经学习了如何在Firebase上创建基于用户角色的API.
我们还介绍了如何从Angular应用和Postman中使用它.
让我们回顾一下最重要的事情:
- Firebase允许您使用企业级身份验证API快速启动和运行, 你可以稍后再扩展.
- 几乎每个项目都需要授权——如果您需要使用基于角色的模型来控制访问的话, Firebase身份验证可以让您快速入门.
- 基于角色的模型依赖于验证从具有特定角色的用户请求的资源. specific users.
- 使用快递.. js应用程序在Firebase功能, 我们可以创建一个REST API并设置处理程序来对请求进行身份验证和授权.
- 利用内置的自定义声明,您可以创建基于角色的身份验证API并保护您的应用程序.
您可以进一步了解Firebase认证 here. 如果你想利用我们已经定义的角色,你可以使用@angular/fire helpers.
Firebase Auth是一项服务,它允许你的应用程序注册和验证用户对多个提供商,如谷歌, Facebook, Twitter, GitHub和更多). Firebase Auth提供sdk,您可以使用这些sdk轻松地与web、Android和iOS集成. Firebase Auth也可以作为REST API使用
Firebase是一套云产品,可帮助您快速构建无服务器移动或web应用程序. 它提供了每个应用程序(数据库)中涉及的大多数公共服务, authorization, storage, hosting).
你可以在firebase上用你的Google帐户创建一个项目.google.com. 一旦项目被创建,你就可以打开Firebase Auth并开始在你的应用中使用它.
Firebase是谷歌支持的产品, 其中一个谷歌正在努力发展并增加越来越多的功能. AWS Amplify是一款类似的产品,主要针对移动应用. 两者都是很棒的产品,Firebase是一个更老的产品,功能更多.
Firebase是一个完全托管的服务,您可以非常轻松地开始使用它,并且在需要扩展时不必担心基础设施. 有很多很棒的文档和博客文章,其中有示例,可以快速了解它是如何工作的.
Firebase有两个数据库:Realtime Database和Firestore. 两者都是NoSQL数据库,具有相似的功能和不同的定价模型. Firestore支持更好的查询功能,而且这两个数据库的设计使得查询延迟不受数据库大小的影响.
Joaquin是一名全栈开发人员,在WebMD和Getty Images等公司拥有超过12年的工作经验.
世界级的文章,每周发一次.
世界级的文章,每周发一次.