在本系列的第 1 部分我们已经介绍了如何配置云托管的 MongoDB, Node 服务器,还有 Angular 前端项目。
接下来本系列的第 2 部分将介绍身份验证、授权、功能模块规划和数据建模:
Angular:身份验证
继续第 1 部分的内容,现在添加身份验证模块,它包含:
- 登录和注销
- 用户信息和令牌管理
- 会话持久性
- 使用访问令牌对 HTTP 请求进行授权
安装 Auth0.js
首先安装 Auth0 依赖,用于和之前注册的 Auth0 账号进行交互:
1 | $ npm install auth0-js@latest --save |
动态环境配置
创建一个文件来存储关于应用程序环境的信息。我们目前在localhost:4200
上进行开发,但是最终将部署在节点服务器上,在生产环境中,它会运行在反向代理上。我们需要确保开发环境不会破坏生产环境,反之亦然。
创建src/app/core
文件夹,然后添加一个名为env.config.ts
的文件:
1 | // src/app/core/env.config.ts |
上述代码检测主机环境并设置应用程序的基础 URI 和基础 API URI。在需要检测和使用这些 uri 的地方,可以引入ENV
变量。
另一种方法是配置environments/environment.*.ts
。。
安全认证设定
创建src/app/auth/auth.config.ts
文件用于存储 Auth0 认证相关的配置信息:
1 | // src/app/auth/auth.config.ts |
这些配置信息可以在你的 Auth0 账号里找到。
身份认证服务
AuthService
将会负责前端的身份验证逻辑,用 CLI 为生成模板:
1 | $ ng g service auth/auth --spec false |
打开该文件并添加:
1 | // src/app/auth/auth.service.ts |
上面代码使用auth.config
里的配置实例化了一个WebAuth
对象,并且提供了一个 RxJS 的BehaviorSubject
身份验证状态事件流,使得我们可以在整个应用中订阅它。
构造函数在初始化时检查应用程序身份验证状态:如果用户没有从之前的会话中退出 Angular 应用程序(令牌还没有过期),将会调用renewToken()
的方法来验证他们在身份验证服务器上的 Auth0 会话是否仍然有效。如果是,我们会接收一个新的访问令牌。
login()
方法使用WebAuth
发起授权身份验证请求。Auth0 登录授权页面会显示给用户,然后用户可以进行登录。
当用户成功验证,应用的回调页面会接收到一个access_token
和令牌过期时间(expiresIn
)。handleAuth()
方法使用 Auth0 的parseHash()
回调方法来获取用户的概要文件(_getProfile()
),并且通过本地存储保存令牌,过期时间,概要文件设置会话信息(_setSession()
),同时调用setLoggedIn()
同步用户验证状态,以便应用程序中的任何组件知道用户已经登陆了。
接着,我们创建了一些通用方法(_clearExpiration
),用于从本地存储中轻松清除过期信息。
logout()
方法清除本地存储的过期信息,并通过 Auth0 的 API 注销当前会话,并且重定向到我们指定的页面(受页)。
tokenValid()
访问器,用于检查当前日期时间是否小于令牌过期日期时间。
最后,我们将实现renewToken()
方法,如果用户的身份验证会话仍然处于活动状态,则使用 Auth0 checkSession()
方法从 Auth0 请求一个新的访问令牌。如果没有会话活动,我们将不做任何事情。我们不希望在这里产生任何错误或日志,因为没有会话并不意味着出了什么问题。
AuthService 全局实例
我们需要全局注册 AuthService 的单一实例,因此将在app.module.ts
里注入依赖:
1 | // src/app/app.module.ts |
回调组件
接下来,我们将创建一个回调组件。通过验证后应用程序会被重定向到此。这个组件负责接收处理身份验证信息,然后显示一条加载消息,直到散列解析完成,Angular 应用程序重定向回主页。
还记得我们之前已经将http://localhost:4200/callback
和http://localhost:8083/callback
添加到 Auth0 允许的客户端回调地址。
1 | $ ng g component pages/callback |
AuthService
服务的handleAuth()
方法必须在该组件的构造方法里调用,以便在应用初始化时运行。
1 | <!-- src/app/pages/callback/callback.component.html --> |
然后添加回调路由:
1 | // src/app/app-routing.module.ts |
在 HeaderComponent 添加登陆和注销
在 Header 组件里添加 AuthService 服务:
1 | // src/app/header/header.component.ts |
在组件模板里添加相应元素:
1 | <!-- src/app/header/header.component.html --> |
相应样式这里就不占用篇幅了,具体请参照源码。
我们现在可以登录我们的应用程序了! 通过单击“登录”链接并进行身份验证。登录之后,可以在 Header 的右上角看到名字和退出链接。
你可以试着关闭浏览器并重新打开它,你会发现登录状态是持久的(除非令牌已经过期,或者你点击了注销)
角色授权
对于我们的应用来说,只有是admin
的用户才可以创建,更新和删除活动信息,其他普通用户只能回复活动。为了实现这些,我们需要给用户分配角色在 Node.js 的 API 和 Angular 应用里完成相应的逻辑代码。
首先来看看大概的步骤:
- 使用 Auth0 规则创建我们的用户角色,然后将它们添加到 ID(客户端用户信息)和 access (API)令牌。
- 实现 Node.js API 中间件以保证只有
admin
角色的用户可以访问相应 API。 - Angular 中利用用户角色信息对路由和功能模块进行保护。
快上车!
使用 Auth0 规则进行管理授权
所谓Rules
是 Auth0 提供的一个拓展,它实际上是一个 Javascript 方法,每次进行用户身份认证的时候都会执行。
进入我们的Auth0并选择创建一条Set roles to a user
模板的 rule:
1 | // set me as 'admin' role, and all others to 'user' |
简单起见,我们只给自己的账号分配admin
角色,其他账号都是普通user
。
注意,上面代码还检查确保用户邮件必须是已经通过验证了。
namespace
标识符可以是任何非 auth0 的 HTTP 或 HTTPS URL,并且不必指向实际的资源。Auth0 执行 OIDC 关于附加声明的建议,并且会静默排除任何没有名称空间的声明。[了解更多]
现在,你可以在我们的 RSVP 程序进行登陆,登陆成功后,可以在Auth0 的用户查看用户的Metadata
,你应该看到app_metadata
大概如下:
1 | { |
在客户端接收的 ID 和访问令牌, 会附带如下的键值对:
1 | "http://myapp.com/roles": ["admin"] |
Node API 管理员中间件
现在我们的 Auth0 身份验证已经可以提供角色支持,接下来利用它来保护需要管理员访问的 API 路由。
打开config.js
并添加我们在上面设置的namespace
:
1 | // server/config.js |
添加中间件代码来确认用户是否经过身份验证,以及是否具有访问 API 的管理员权限。
1 | // server/api.js |
express-jwt
包默认将解码后的令牌添加到req.user
。adminCheck
中间件查找这个属性,并在数组中查找 admin 的值。如果找到,则继续请求。如果没有,则返回 401 未授权状态,并显示一条简短的错误消息。
Angular 应用中的管理员授权
同样,我们需要在前端添加相应的管理员授权检测代码,我们需要修改AuthService
服务。
首先,添加同样的namespace
:
1 | // src/app/auth/auth.config.ts |
接着在auth.service.ts
检查和保存管理员授权信息:
1 | // src/app/auth/auth.service.ts |
首先我们添加了一个属性:isAdmin: boolean
,用来标识用户的管理员状态。另外,我们更新了_setSession
方法,在用户通过验证后,检查了用户的角色信息并同步isAdmin
。
至此,在后端 Node API 路由和 Angular 应用中都已经实现了权限校验。
规划功能模块
数据库、Angular 应用程序、身份验证和 Node API 基本结构已经搭建好了。现在是时候进行功能规划和数据建模了。在直接编写 API 和业务逻辑之前,规划应用程序的数据结构非常重要。
让我们从更高层次的角度思考一下 RSVP 应用程序的预期功能,然后我们将推断数据库模型应该是什么样子的。
活动事件
- 在首页显示可参加的公开活动事件列表,并且可以进行搜索。这些活动必须发生在将来,而不是已经过期的。
- 管理员可以看到所有活动事件的列表,包括公开/私有/过去/将来的活动。
- 活动详情页面,已登陆用户可以回复参与活动,并且可以查看别人的回复。
- 活动只能被管理员创建更新和删除。
- 删除一个活动会同时清除所有相关联的回复。
- 公开的活动可以显示在首页,但是私有的活动也可以直接通过链接访问。
- 活动回复和活动的 ID
活动属性
- 活动 ID (数据库自动生成)
- 活动标题
- 地点
- 开始日期和时间
- 结束日期和时间
- 活动描述
- 可见性(公开/私有)
活动回复
- 任何已认证的用户可以参与回复将要发生的活动,不管是公开还是私有。
- 用户不可以添加和更新一个已经结束的活动。
- 用户可以修改他们现有的回复,但不能删除它们。
回复的属性
- 回复 ID
- 用户 ID
- 名字
- 活动 ID
- 是否出席
- 额外出席人数(如果参加)
- 评论
用户
- 用户应该能够在他们的个人资料中查看所有的已回复列表
- 用户数据不存储在 MongoDB 中,由 Auth0 托管。
- 用户通过用户 ID 与他们的 RSVPs 相关联。
- 用户只能更新自己的回复。
- 管理员可以对活动进行增删改查。
数据建模
我们已经对应用的功能有了大致的了解,接下来需要在服务端
和客户端
建立必要的数据模型。
创建 Mongoose Schema
通过 mongoose
进行 MongoDB
对象建模。每个 mongoose
模式会映射到一个 MongoDB
集合,并定义该集合中文档对象的原型。
在server
下新建models
文件夹,添加Event.js
和Rsvp.js
:
1 | // server/models/Event.js |
MongoDB 会自动生成对象 ID。
1 | // server/models/Rsvp.js |
在 Node API 中,我们将利用它们从 MongoDB 中
1 | // server/api.js |
Angular 应用中的模型
同样在前端 Angular 应用里我们也需要定义 Event
和 RSVP
模型,用来接受从 Node API 检索回来的数据。通过 CLI 创建两个 Class
:
1 | $ ng g class core/models/event.model |
打开生成的文件并添加:
1 | // src/app/core/models/event.model.ts |
1 | // src/app/core/models/rsvp.model.ts |
在 MongoDB 里创建和初始化 Collections
为了查询数据库,我们准备在 MongoDB 里创建必要的 collection 和一些原始数据。这一切都将通过之前提到的 MongoBooster 来完成:
创建 Collection
通过 MongoBooster 连接到我们托管的 MyLab 数据库,并创建events
和rsvps
两个 collections.
添加原始数据
打开 Mongo shell:
1 | db.getCollection("events").insert([{ |
1 | db.getCollection("rsvps").insert([{ |
记得替换上面相应的数据,userId
对应 Auth0 上的已认证的用户,eventId
对应我们已经插入的原始 event 数据。
小结
在 Angular 实战系列的第 2 部分中,我们已经介绍了 MEAN 应用程序的身份验证和授权、功能规划和数据建模。在本系列教程的第 3 部分中,我们将使用 Node API 从数据库中获取数据,并使用 Angular 显示数据,完成过滤和排序。
系列索引