安全性
认证
无认证
如何开发简单的应用程序时禁用身份认证
要完全禁用deepstream服务器的身份认证,请在服务器的配置文件中将type 设置为none。
Authentication
auth: type: none
或使用--disable-auth命令行参数。
./deepstream start --disable-auth
在deepstream启动日志中可以确认已禁用了身份认证。
请注意,即使身份认证类型为none,用户仍然可以通过{username: 'johndoe'}在登录时发送(未验证的)用户名。
client = new DeepstreamClient('localhost:6020') client.login({ username: 'johndoe' })
如果未提供用户名,则deepstream默认用“OPEN”作用户名。
文件认证
一种从文件读取凭据和用户数据的deepstream身份认证机制
基于文件的身份认证让您可以将用户名,密码或密码哈希以及可选的元数据存储在一个文件中,该文件将用于认证传入的连接。 对于需要认证的连接数量较少的场景,例如:具有少量数据提供者进程的公共可读实时仪表板,基于文件的验证是一个不错的选择。
要用存储在本地文件中的用户凭据对deepstream 服务器进行身份认证,请在服务器配置文件中将密钥type设置为fileauth
auth: type: file options: path: ./users.yml # Path to the user file. Can be json, js or yml hash: 'md5' # the name of a HMAC digest algorithm iterations: 100 # the number of times the algorithm should be applied keyLength: 32 # the length of the resulting key
- path 项中的路径是相对于配置文件的,path指定的文件包含了你的用户名和密码。默认情况下是deepstream随附的users.yml文件,但名称或位置实际取决于您。
- hash 项中指定了密码的哈希算法,例如,使用md5(或操作系统支持的任何其他算法)。
- iterations 项指定了对用户密码取哈希的次数
- keyLength 项是生成密码的长度。
上面这些项配置了您的密码哈希策略。在users.yml 文件中,创建用户及其哈希密码的列表(您可以使用deepstreams hash命令显示哈希值)。
您还可以指定:
- clientData – 成功登录后发送给客户端的用户数据。
- serverData – 发送给服务器进行权限管理的用户数据。
chris: password: tsA+ks76hGGSGHF8**/JHGusy78=75KQ2Mzm clientData: favorite color: blue serverData: department: admin
fred: password: jhdGHJ7&0-9)GGSGHF8**/JHGusy78sjHJ&78 clientData: favorite color: red serverData: department: finance
启动deepstream服务器,您会看到身份认证类型是否被确认。
在您的应用程序代码中,可以连接到deepstream服务器并尝试登录用户。
const { DeepstreamClient } = require('@deepstream/client') const client = deepstream('localhost:6021')
client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL })
如果成功,deepstream控制台将显示
AUTH_SUCCESSFUL
如果失败,deepstream控制台将显示
INVALID_AUTH_DATA
然后,您可以在JavaScript代码中处理登录请求的结果,例如:
// ES5 const { DeepstreamClient } = require('@deepstream/client') const client = new DeepstreamClient('localhost:6020');
client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL }, (success, clientData) => { if (success) { // Do stuff now your authenticated } else { // Unhappy path of an unsuccesful login } })
// ES6 import deepstream from '@deepstream/client' const client = new DeepstreamClient('localhost:6020')
try { const clientData = await client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL }) // Do stuff now your authenticated } catch (error) { // Unhappy path of an unsuccesful login }
存储介质认证
通过存储适配器读取凭据和用户数据的deepstream身份认证机制 在v5版本中添加
基于存储介质的身份认证允许您将用户名,密码哈希和可选的元数据存储在数据库中的表中,该表将用于对传入连接进行身份认证。
使用基于存储介质的身份认证
要使用存储在数据库中的用户凭据对deepstream服务器进行身份认证,请在服务器配置文件中将type字段设置为storage
auth: type: storage options: table: 'User' # The table to store the user data in tableSplitChar: string # the split character used for tables (defaults to /) createUser: true # automatically create users if they don't exist in the database hash: 'md5' # the name of a HMAC digest algorithm iterations: 100 # the number of times the algorithm should be applied keyLength: 32 # the length of the resulting key *hash 项中指定了密码的哈希算法,例如,使用md5(或操作系统支持的任何其他算法)。 *iterations 项指定了对用户密码取哈希的次数 *keyLength 项是生成密码的长度。 上面这些项配置了您的密码哈希策略。
启动Deepstream服务器,您应该看到确认的身份验证类型。在您的应用程序代码中,您可以连接到deepstream服务器并尝试登录用户。
client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL })
如果成功,deepstream控制台将显示
AUTH_SUCCESSFUL
如果失败,deepstream控制台将显示
INVALID_AUTH_DATA
然后,您可以在JavaScript代码中处理登录请求的结果,例如:
// ES5 const { DeepstreamClient } = require('@deepstream/client') const client = new DeepstreamClient('localhost:6020');
client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL }, (success, clientData) => { if (success) { // Do stuff now your authenticated } else { // Unhappy path of an unsuccesful login } })
// ES6 import deepstream from '@deepstream/client' const client = new DeepstreamClient('localhost:6020')
try { const clientData = await client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL }) // Do stuff now your authenticated } catch (error) { // Unhappy path of an unsuccesful login }
用户自动注册
如果设置createUser项为true,则deepstream将自动为您创建一个用户。这适用于用户无需调用API注册帐户的工作流。
- 创建新用户
如果在首次登录时创建了用户,则应注意以下几点:
- ServerData为空
- ClientData包含两个字段
- 创建用户的时间戳
- 用户ID
deepstream中的用户名实际上是用户id而不是用户名。为每个用户分配唯一的ID非常重要,原因有两个:
- 它在presence 中使用,所以每当你调用client.presence.getAll() 或者client.presence.subscribe() 时你将得到用户id,而不是用户名
- 它在权限管理中使用,可以通过user.id 访问用户id
那么,为什么要使用id而不用用户名呢?因为用户名是人们可能想要更改的名称,但uuid永远存在。这样,您的应用程序就无需考虑更新数据库中用户引用的事了。
不要忘记申请权限
很重要,最好别忘!由于默认情况下数据库是放开的,这意味着任何用户都可以请求或更新数据,这可能导致巨大的安全问题。
添加权限最简单的方法是拒绝在Valve中访问该表。
record: 'YourUserTable/.*': create: false write: false read: false delete: false listen: false notify: false
那么为什么不施展魔法,在内部自动应用该规则呢?因为如果正确设置了权限,您将可以做一些非常酷的事情。例如,从管理UI修改用户数据或删除用户。
record: "YourUserTable/.*": write: "user.data.role === 'admin'" read: "user.data.role === 'admin'" delete: "user.data.role === 'admin'"
最终用户本身不应访问UserTable,要更改他们的任何数据或密码时应通过RPC调用来做。
import { DeepstreamClient } = from '@deepstream/client' const client = new DeepstreamClient('localhost:6020')
async { await client.login({ username: 'a user with an admin role', password: '1234' })
client.rpc.provide('change-user-details', async (data, response) => { const user = client.record.snapshot(`User/${data.username}`) // update user data await client.record.setData(`User/${data.username}`, user) response.send('Done') }) }
HTTP认证
如何将您自己的HTTP服务器注册为Webhook以进行用户身份认证
Http身份认证使您可以将自己的HTTP服务器URL注册为Webhook。每次用户尝试登录时,deepstream都会通过POST请求将其凭据发送到您指定的服务器URL。根据您服务器的响应,将拒绝或授予用户的登录操作。
Http身份认证是最灵活的身份认证类型,因为完全由服务器来实现身份认证机制。您可以查询数据库,联系oAuth提供商,验证WebToken或使用任何您喜欢的方式。
要启用HTTP认证,在服务器配置文件的auth 段中把type项设置为http。
- type: http options: endpointUrl: https://someurl.com/auth-user permittedStatusCodes: [ 200 ] requestTimeout: 2000 retryStatusCodes: [ 404, 504 ] retryAttempts: 3 retryInterval: 5000
在options设置项中
- endpointUrl:deepstream发送POST请求进行身份认证的服务器URL
- permittedStatusCodes:身份认证成功时的HTTP代码列表
- requestTimeout:超时设置(以毫秒为单位)
除非您的deepstream服务器和身份认证服务器位于同一专用网络中,否则应使用安全连接(https)。
以下数据会随POST身份认证请求一起发送。
{ "connectionData": {...}, "authData": {...} }
处理身份认证URL的服务如何构建取决于您和您的应用程序栈,但它应返回相应的http响应代码和用户名字符串或以下JSON:
{ "id":"chris", "clientData": {}, "serverData": {} }
clientData和serverData的内容由您决定,但应对deepstream流程有用。clientData会成为客户端client.login()的回调,而serverData会发送给权限处理模块。
启动Deepstream服务器,您应该看到确认的身份验证类型。下面是个node服务器的简单示例,在传递某个用户名时返回http代码200,如果是其他用户名则返回404:
const express = require('express') const bodyParser = require('body-parser') const app = express()
app.use(bodyParser.json())
app.post('/auth-user', (req, res) => { if (req.body.authData.username === 'chris') { res.json({ id: 'chris', clientData: { themeColor: 'pink' }, serverData: { role: 'admin' } }) } else { res.status(403).send('Invalid Credentials') } })
app.listen(3000)
接下来,在您的应用程序代中可以连接到deepstream服务器并尝试登录。尝试将用户名的值更改为除“chris”之外的其他值,查看会发生什么。
// ES5 const { DeepstreamClient } = require('@deepstream/client') const client = new DeepstreamClient('localhost:6020');
client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL }, (success, clientData) => { if (success) { // Do stuff now your authenticated } else { // Unhappy path of an unsuccesful login } })
// ES6 import deepstream from '@deepstream/client' const client = new DeepstreamClient('localhost:6020')
try { const clientData = await client.login({ username: 'chris', password: 'password' // NEEDS TO BE REAL }) // Do stuff now your authenticated } catch (error) { // Unhappy path of an unsuccesful login }
如果成功,deepstream控制台将显示
AUTH_SUCCESSFUL
如果失败,deepstream控制台将显示
INVALID_AUTH_DATA
使用JWT进行身份认证
如何使用JSON Web Token向deepstream进行身份认证
身份认证对于大多数应用程序至关重要,近年来身份验证的实现方式已发生了巨大变化。当今最流行的概念之一是称为JSON Web Token 或JWT 的标准,该标准使您可以将加密的信息存储在可验证的令牌中。
这些令牌是紧凑且自洽的已编码的JSON对象,其中包含重要信息,这些信息在不同方(大多数时候都是客户端/服务器)之间传输。紧凑的特性使它们可以通过请求头交换,而自洽的特性则体现了JWT存储身份认证有效负载的能力,这使得JWT不仅对身份认证有用,而且在信息交换中也非常方便。
JWT包含三个部分:标头,有效负载和签名:
- 标头:哈希算法和类型(一般是jwt)
- 有效负载:通常由身份验证数据组成,即Claims
- 签名:用标头的哈希算法和密钥对标头和有效负载创建签名。签名过程就是验证令牌的过程。
deepstream可以使用多种策略来验证传入的连接。对于JWT 我们将使用HTTP-Webhook(一个可配置的URL)deepstream会将登录和连接数据发送到该URL以进行验证。
您是否应该在deepstream中使用JWT?
看情况。传统令牌用作会话数据的主键,这意味着它们帮助后端从数据库或缓存中检索与用户会话相关的数据。而JWT 本身就是实际的会话数据 -- cookie本身包含有效负载,从而使后端不必频繁查找会话数据。这对于HTTP工作流非常有用,在HTTP工作流中,客户端发出许多与同一用户关联的单个请求。
但deepstream使用持久连接,该持久连接仅在客户端连接时建立一次(好吧,如果连接断也许会再建立一次)。所有会话数据都与该连接关联,而不是与通过该连接进行的请求和订阅关联。所以,deepstream 消息比同等的HTTP 消息小得多且速度更快。
这确实意味着使用JWT不会使deepstream本身受益匪浅。但也并不太糟,deepstream与传统的HTTP端点结合使用时,JWT 仍然很有用。
deepstream的Webhook认证方式
在开始使用JWT进行身份认证之前要注意的是,deepstream允许您注册一个HTTP URL,每当客户端或后端进程尝试登录时,连接数据就会作为POST请求转发到该URL。
下面的HTTP认证指南介绍如何在您的项目中设置Webhook工作流程。
使用JWT进行deepstream HTTP身份认证
JWT允许我们使用编码的JSON字符串将身份声明从服务器安全地传输到客户端,反之亦然。只要令牌有效(不被篡改且未过期),该令牌将保留在客户端上并用于发出授权请求。
回顾上述流程,需要将JWT放在下图合适的地方上。为此,有两种选择:
- 简单但安全性较低
在此场景中,deepstream客户端将用户的身份声明发送到deepstream服务器,然后将其转发到已配置的HTTP服务器。
HTTP服务器创建JWT令牌并将其通过deepstream服务器传递回客户端,客户端将其存储在localStorage中。
对于后续请求,令牌已经在localStorage中,并且将由客户端发送不再需要身份声明。
为什么这样不太安全?
使用javascript存储在localStorage或cookie中的令牌可以被页面上的所有脚本读取。这使它可以被用来做跨站点脚本攻击(XSS),从而可能劫持会话。而且此方法要求Web应用程序本身及其所有资源都是公开可读的。下面介绍的方法允许您将所有未经身份认证的请求重定向到登录页面上。
- 复杂但安全性高
推荐的工作流如下图所示:
用户在静态登录页面中提供身份凭据,这些凭据通过HTTP POST请求发送到身份验证服务器。
如果提供的凭据有效,则服务器将生成JWT,并返回301把客户端重定向到将令牌存储为cookie的Web应用程序页面。
deepstream客户端将建立与deepstream服务器的连接,并通过调用ds.login(null, callback)进行身份验证。cookie中存储的JWT也会发送给deepstream服务器。
- deepstream将cookie转发到身份验证服务器并等待其回复。身份验证服务器可以选择分析cookie,并将其中包含的数据返回给deepstream用于权限管理。如果身份认证服务器返回了正常响应(例如HTTP代码200),则连接通过认证。
理论听起来很复杂,实际上是这样工作的:
我们的应用程序将提供以下URL:
- / 实际包含deepstream客户端脚本的Web应用程序。服务器仅允许提供有效JWT的连接访问该路由
- /login 公开的登录页面(静态,不含deepstream客户端脚本)
- /handle-login 登录表单会POST到此URL
- /check-token deepstream会将传入连接的身份验证数据转发到此URL
登陆页
我们将从创建带有简单登录表单的静态HTML页面开始。
仅两个输入:用户名和密码。用户单击登录后,身份凭据将发送到/handle-login路由。在装有Express的Node服务器上,可用以下方法处理路由
// . . . const jwt = require('jsonwebtoken');
app.post('/handle-login', function(req, res, next) {
const users = { wolfram: { username: 'wolfram', password: 'password' }, chris: { username: 'chris', password: 'password' } // . . . }
const user = users[req.body.username];
if (!user) { res.status(403).send('Invalid User') } else { // check if password and username matches if (user.username != req.body.username || user.password != req.body.password) { res.status(403).send('Invalid Password') } else {
// if user is found and password is right
// create a token
const token = jwt.sign(user, 'abrakadabra');
// return the information including token as JSON
// set token to cookie using the httpOnly flag
res.cookie('access\_token', token, {httpOnly: true}).status(301).redirect('/');
}
}
});
此方法验证传入的身份凭据。为简单起见,在此示例中将它们进行了硬编码 -- 在真实的应用存储中,用户名和密码散列存在数据库。
验证后jsonwebtoken模块会对身份凭据的有效负载部分签生令牌,然后把access_token写入cookie同时设置httpOnly标志为true,以禁止来自客户端的javascript访问。最后,通过身份认证的客户端会被重定位到包含deepstream客户端脚本的真实APP页面。
启用HTTP验证
接下来,我们需要启动deepstream服务器的HTTP身份验证。可以通过配置文件来实现:
type: http options: endpointUrl: https://someurl.com/check-token permittedStatusCodes: [ 200 ] requestTimeout: 2000
配置了deepstream服务器在每次客户端尝试连接时向https://someurl.com/check-token发出POST请求,并且仅当它在2秒钟内收到HTTP状态代码为200的响应时,才允许连接。
deepstream登陆
现在,我们可以在deepstream客户端中执行client.login()
const deepstream = require( '@deepstream/client'); const client = new DeepstreamClient('localhost:6020') // Login method .login( null, ( success, clientData ) => {
}) .on( 'error', ( error ) => { console.error(error); });
我们不发送用户凭证,而是传递null给login -- 我们感兴趣的信息是JWT,它会作为头数据的一部分和身份验证请求一起发送,可以用req.body.connectionData.headers.cookie读取cookie中的JWT。
HTTP身份认证
客户端调用ds.login()时,deepstream服务器会将连接数据POST到配置中的/check-token路由:
//. . . const jwt = require('jsonwebtoken');
app.post('/check-token', function(req, res) { const token = getCookie(req.body.connectionData.headers.cookie, 'access_token'); jwt.verify(token, 'abrakadabra', function(err, decoded) { if (err) { res.status(403).send('Failed to authenticate token.' ); } else { // if everything is good, save to request for use in other routes res.status(200).json({ username: decoded.username }); } }); });
function getCookie( src, name ) { const value = "; " + src; const parts = value.split("; " + name + "="); if (parts.length == 2) return parts.pop().split(";").shift(); }
我们再次使用jsonwebtoken模块来验证令牌并将字符串解码为包含用户名的JavaScript对象,该用户名将传递回deepstream以标识连接。
保护路由
上面涵盖了常规的JWT身份验证流程 -- 但使用JWT,我们还可以防止任何未经授权的访问连接应用的 / 路由
Express通过中间件来简化此过程,会在处理请求之前先检查是否存在有效的JWT。
const jwt = require('jsonwebtoken'); const authMiddleware = function(req, res, next) { const token = req.cookies.access_token;
// decode token if (token) { // verifies secret and checks exp jwt.verify(token, 'abrakadabra', function(err, decoded) { if (err) { return res.json({ success: false, message: 'Failed to authenticate token.' }); } else { // if everything is good, save to request for use in other routes req.decoded = decoded; next(); } }); } else { // if there is no token // return an error return res.status(403).redirect('/login');
} }
// Protect route with middleware app.post('/protected', authMiddleware, routeHandler);
权限
Valve 介绍
了解Valve权限设置方法
deepstream使用一种称为Valve的强大的权限语言,允许您指定哪个用户可以对哪些数据执行哪个操作。
使用Valve,您可以:
- 限制单个用户或组的访问
- 设置单独的操作权限(例如,写、发布、监听)
- 设置单独的Records,Events,Rpcs权限
- 验证传入数据
- 与存储的数据比较
前置条件
学习本指南前需要了解deepstream服务器配置方式,因为我们需要告诉服务器权限规则的存储位置。 deepstream支持各种通信概念,例如数据同步、发布-订阅、请求-响应,而Valve足够灵活,可以为每个概念使用各自规则。本指南主要侧重于Records,因此最好熟悉Records。 由于权限从本质上是指单个用户的权利,因此了解用户身份验证在deepstream中如何工作也是好事一件。
让我们从一个例子开始 假设您正在运行一个论坛。为了避免恶意破坏和垃圾邮件,用户在注册后必须等待24小时才能创建新帖子或修改现有帖子。这意味着我们需要存储用户注册的时间以及他们的帐户信息。这可以使用http身份验证动态完成,但是为了简化本指南,我们将timestamp存储在serverData中,配合deepstream基于文件的身份认证使用。conf/users.yml文件中的用户配置项可能如下所示:
JohnDoe: password: gvb4563Z serverData: timestamp: 1482256123052
上面的代码段配置了用户JohnDoe,托管论坛的服务器需要知道John Doe何时注册,因此在serverData 中有一个timestamp。
使用deepstream作为后端,可以将所有论坛帖子存储在Records中(这是数据同步的概念)。以下Valve片段为新用户提供了只读访问权限:
record: "*": read: true listen: true delete: false create: "user.data.timestamp + 24 * 3600 * 1000 < now" write: "user.data.timestamp + 24 * 3600 * 1000 < now"
在record下的规则适用于所有涉及Records的操作:下面一行中的模式是record名称的通配符。在deepstream中,可以创建,写入,删除,读取record,还可以监听客户端对record的订阅情况。使用Valve,您可以对每个操作设置不同的权限。在上面的Valve片段中,我们允许所有人读取、监听record,但不允许删除record。在最后两行中,我们将用户的timestamp与当前时间进行比较,超过24小时才授予用户create 和write权限;now返回Unix时间,类似Javascript中的Date.now() ,以毫秒为单位24 * 3600 * 1000毫秒就是24小时。
最后,我们需要更新配置文件以启动自定义权限。假设我们将用户权限存储在路径conf/permissions.yml 中,可以在deepstream服务器的conf/config.yml 中进行设置:
permission: type: config options: path: ./permissions.yml
如上所示,要让deepstream使用基于文件的用户权限,需要权限规则文件、对配置文件更改、以及某些特定于用户的数据(可选)。
权限规则
通用的Valve规则看起来如下所示:
concept: "pattern": action: "expression"
对于每个action,客户端API中通常都有相应的接口,例如,JavaScript客户端调用record.set() 时需要record 的write 权限。deepstream中的每个Record、RPC、Event和经过身份认证的用户都拥有唯一的标识符(名称)。如果Valve想确定是否允许某个操作,则会在对应的段中查找权限:
- 搜索record,rpc或event段
- 搜索模式和标识符之间最匹配的规则
- 执行右侧表达式进行确认
在以下段落中,我们介绍了可能的操作。
文件格式 Valve语言使用YAML或JSON文件格式,权限规则必须始终覆盖每个可能的标识符,因为deepstream服务器不提供默认值。请注意,deepstream服务器附带一个许可文件conf/permissions.yml该文件允许执行所有操作。Valve设计为优先使用标识符把对象与相应规则进行匹配,因此应妥善选择标识符,使权限规则可以基于标识符完成选择。
标识符匹配 Valve可以使用固定(子)字符串、通配符和占位符(对于deepstream,我们将其称为path变量)来匹配标识符,这些占位符可以在表达式中使用。假设我们以name/lastname/middlename/firstname格式存储用户名,对应Valve代码:
presence: 'name/Doe/$middlename/$firstname': allow: false
与此规则匹配的用户名有John Adam Doe(匹配结果为name/Doe/Adam/John)或Jane Eve Doe(匹配结果为name/Doe/Eve/Jane)。前一种情况$firstname === 'John',后一种情况$firstname === 'Jane'。
Valve中的通配符是星号(符号*),*匹配任意字符直到字符串结尾。占位符以美元符$开头,后跟字母或数字,直到遇到斜线为止。原则上标识符可以包含任何字符,但是如果在标识符中使用星号,则deepstream无法提供专门匹配此字符的方法。
表达式 标识符匹配后,deepstream将计算右侧表达式。该表达式使用JavaScript子集,包括:
- 算术表达式
- 条件操作符
- 比较操作符
- 字符串函数如startsWith,endsWith,indexOf,match,toUpperCase,toLowerCase,和trim。
此外,您可以用now获取(在服务器上)当前时间,您还可以访问deepstream中的数据或引用的record。
任何登录deepstream服务器的客户端的用户数据都可以用Valve访问到,但是请注意该客户端的用户未必通过了身份认证,除非在配置中禁止了未经身份认证的用户登陆。您可以使用user.isAuthenticated 来检查该用户是否经过了身份认证(检查此属性时,三元运算符?:可能会很有用)。如果客户端通过了身份认证,则可以使用user.name访问用户名,使用访问user.data serverData。此外,Valve允许您使用规则检查关联数据,例如,对于一条record,您可以检查旧值和新值。由于有哪些数据项取决于其类型(record、event或RPC等),因此我们将在类型部分中讨论详细信息。
Valve使您能够引用records中的数据。在右侧表达式中,使用术语(identifier)来访问具有给定标识符的record,其中标识符identifier被解释为返回字符串的JavaScript表达式,例如('family/' + $lastname),被引用的record必须真实存在。请注意,使用引用record会忽略Valve权限限制,这意味着您绕过Valve规则间接获得了读权限。
在计算表达式时,您需要牢记一些陷阱
- 使用now获取当前时间需要您考虑考虑,通常哪些操作带有时间相关性,再提醒一次,now使用的是服务器时间。每当客户端在代码中使用当前时间时,都应牢记这一点。
- Valve支持读取引用存储的数据,但这会耗费昂贵的计算资源。因此,截至2016年12月21日,deepstream附带的默认配置最多允许三个交叉引用。
- 最后,注意变量类型(有隐式类型转换),JavaScript比较运算符和浮点运算常会可能有隐患 。
Records 可以创建,删除,读取,写入Records,也可以监听其他客户端对Records的订阅(Records教程详细介绍了这些操作,并解释了取消订阅、丢弃和删除Records之间的区别)。以下代码段是 Records的默认Valve代码:
record: "*": create: true # client.record.getRecord() read: true # client.record.getRecord(), record.get() write: true # record.set() listen: true # record.listen() delete: false # record.delete()
在Valve中,您可以通过oldData引用访问当前record的内容, 对于write操作可以使用data来检查修改后的record。
请注意,create权限仅当getRecord()而record不存在时使用,否则只需读权限。类似地,如果不修改记录(例如,已修改和未修改的记录相同),则写操作总是成功的。另外,如果服务器拒绝了写操作,则客户端必须处理错误;否则record的客户端副本将与服务器状态不同步。最后,不要混淆了record.get()和record.set()中的path与record identifier,后者是Valve用的。
用户上线 当用户登录经过身份认证时,deepstream会通知您。此权限用presence设置,唯一的选项是允许还是不允许监听:
presence: "*": allow: true # client.subscribe()
Events Events可以发布和订阅。此外,发Events的客户端还可以监听Events订阅情况。在event段中设置操作:
event: "*": publish: true # client.event.emit() subscribe: true # client.event.subscribe() listen: true # client.event.listen()
publish项可以用data在表达式中检查引用数据。
RPCs PRCs分为提供者和请求者。相应的权限部分由rpc段设置:
rpc: "*": provide: true # client.rpc.provide() request: true # client.rpc.make()
配置基于文件的权限管理 要使用基于文件的权限管理,配置文件必须设置permission.type。权限文件的名称必须在deepstream配置文件的permission.options.path中提供, 文件名可以任意取。如果权限文件使用了相对路径,则此路径以包含配置文件的目录为基本目录。
假设可以在conf/permissions.yml中找到权限规则,并且配置文件为conf/config.yml,则基于文件的权限管理配置如下所示:
permission: type: config options: path: ./permissions.yml
扩展阅读
更简洁的介绍(或复习)指南有Valve入门,Valve进阶和动态Valve。要了解如何使用Valve发送用户特定的数据,请参阅用户定制数据指南。
Valve 入门
Valve基础及deepstream中的权限管理
权限使您可以精确控制哪些record,event或RPC可以由哪个用户使用。
什么是权限?
权限允许您设定用户是否可以创建、写入、读取、删除record,发布或订阅event,提供或创建RPC,监听其他客户端的订阅。
权限如何工作?
deepstream使用一种称为“Valve”的权限语法。Valve规则是简单javascript字符串,提供简化功能集。每个Valve规则定义了与特定概念有关的特定动作,例如“record”的“write”。
它们看起来这样:
record:
# an auctioned item
auction/item/$sellerId/$itemId:
# everyone can see the item and its price
read: true
# only users with canBid flag in their authData can bid
# and bids can only be higher than the current price
write: "user.data.canBid && data.price > oldData.price"
# only the seller can delete the item
delete: "user.id == $sellerId"
如何使用Valve规则?
Valve规则可以用YAML或JSON写入permissions文件(文件路径要配置在config.yml中的permissions/options/path中)。
该文件具有三个嵌套级别:类型(record, event 或rpc)- 名称(record, event 或rpc的名称)- 动作(读取,写入,删除,发布等)
让我们从一个简单的例子开始
假设我们正在构建一个通知系统,该系统允许用户将其状态作为event发送(如果你愿意,可以想象成微型Twitter)。每个用户都可以监听另一个用户的状态改变,但是用户只能发布自己的状态更新。
首先,让我们为所有event定义默认操作:我们的示例平台是开放的因此每个人都可以发布和订阅,监听不是该平台的默认功能因此让我们将其关闭:
event: "*": publish: true subscribe: true listen: false
接下来,让我们为状态event定义规则。我们的event名称遵循模式user-status/
event: "*": publish: true subscribe: true listen: false user-status/$userId: publish: "user.id === $userId" #users can only share their own status
本示例介绍了两个基本概念:
- 优先级:Valve使用简单的优先级来找出要应用的权限规则:最长(即最详细)的规则获胜。user-status/$userId比通用规则"*"更长,因此会用它的发布操作。但是,对于订阅和发布,权限规则将退回到一般规则"*"。
- 路径变量:前缀$将record、RPC或event的部分标识为变量,例如user-status/$userId中的$userId,这些变量将在权限规则内可用。在规则中使用路径变量是Valve中的一个核心概念,例如在record名字中使用用户名变量实现权限控制。
下一步是什么?
本篇快速介绍了Valve可以做什么,但还有更强大的概念,例如内置变量,字符串函数和交叉引用。要了解这些内容,让我们继续进阶教程。
Valve 进阶
学习如何释放Valve的洪荒之力
好了,是时候看看Valve高级规则了。
变量
Valve自动将一组变量注入其权限规则。
- data 和 oldData
data 包含event和RPC的数据。它可以用来验证有效载荷。
# make sure a tweet contains max 140 characters
publish: "data.content.length < 140"
# make sure firstname is a string
write: "typeof data.firstname === 'string'"
对于record,data是即将更新的数据,oldData是当前数据。这有助于进行比较:
# make sure bids at an auction can only go up
write: "data.price > oldData.price"
# make sure that \`owner\` can't be changed once written
write: "!data.owner || data.owner == oldData.owner"
- user
user对象包含了正在尝试操作的用户的信息。它提供user.id(登录时的用户名)和user.data。
user.data 是用户登录时提供的元数据。如果您使用的是基于文件的身份验证则它是data字段,如果您使用http webhook 返回的数据它是serverData数据。user.data是存储身份数据(如{role: 'admin'})或访问级别(如{ access: 'All'})或一些标志(如{ canDeletePosts: false })的好地方。
- now
权限规则还允许用now 做基于时间的验证,例如,网站上的新用户只有在注册后超过24小时才能创建新内容或修改现有数据:
record: "*": record: "user.data.timestamp + 24*60*60*1000 < now" write: "user.data.timestamp + 24*60*60*1000 < now"
字符串函数
Valve内置了多种字符串函数,即startsWith,endsWith,indexOf,match,toUpperCase,toLowerCase和trim。它们对于比较值很有用,例如
rpc: book-purchase: request: "data.card.issuer.toLowerCase() == 'visa' && data.card.number.match(/^4[0-9]{12}(?:[0-9]{3})?$/)"
交叉引用
交叉引用使您可以引用deepstream中其他record的数据。这是一项难以置信的通用功能,可让您检查状态或用户数据,例如确保所购买的商品仍然有存货或验证前提条件,又例如确保用户只能投票一次。交叉引用写为_(record名)。
make-call: request: _('shop-status').isOpen == true
该_()函数引用的record名称可以是动态创建的,例如从字符串或路径变量。
# Make sure an item is still in stock purchase/$itemId: request: _('item/' + $itemId ).inStock > 0
嵌套交叉引用
交叉引用甚至可以嵌套。假设我们正在运营一家在线药房,并且只能在有许可的国家/地区销售某些类别的药品。这是我们使用的数据:
// record drug/iqbxxluu-2lc9bl30t18 { name: 'Aprotinin', categoryId: 'iqbxyw8u-1e686wg77xk' }
// record category/iqbxyw8u-1e686wg77xk { name: 'general Antifibrinolytics' allowedCountries: [ 'FRA', 'SPA' ] }
// user.data { country: 'USA' }
现在,定义权限规则执行以下步骤:
- 加载用户尝试购买的药物的信息
- 使用药物的categoryId来加载其所属的类别
- 检查用户所在的国家/地区是否在可以销售此药物类别的国家/地区列表中
# Make sure a drug's category is cleared for sale in the user's country purchase/$drugId: request: _('category/' + _( 'drug/' + $drugId ).categoryId ).allowedCountries.indexOf( user.data.country ) > -1
上例中,购买将被拒绝,因为该药品的类别仅在法国和西班牙才允许销售,但用户来自美国。
交叉引用对性能的影响
每个交叉引用在每次权限验证中都会对deepstream的缓存执行一个附加查询 -- 这会减慢事务处理速度。嵌套的交叉引用是一个接一个地加载交叉引用,因此可能导致明显的延迟。您可以在deepstream的配置文件中为交叉引用指定最大嵌套深度,例如maxRuleIterations: 3。
动态Valve
客户端和服务器均可读取的实时权限
额滴神呐...权限!权限总是很难解释。deepstream是一个实时服务器,甚至它的权限也可以在客户端和服务器间实时共享--如果您希望他们共享的话。
好消息是,使用被称为“Valve”的权限语言,deepstream把实时权限共享变得非常容易。本教程假定您已经了解Valve的方法,如果您还没有看过它,请务必先阅读“Valve入门”和“Valve进阶”教程。 等哈先,为什么我要实时权限?
很多时候,您需要在不同地方使用相同的权限集:
- 服务器 在受信任的环境中强制执行权限规则。
- 客户端 提供即时验证和防御性设计,通过提前屏蔽违规项来提升用户体验。
随着权限的更改 -- 例如,将用户赶出聊天组或不再允许交易者购买某种股票 -- 您希望这些权限不仅立即生效,而且还希望优雅地从客户端的用户界面中删除关联选项 -- 当它们变得不可用时立刻执行。
本教程的目标
有3个普通用户和1个具有个人凭据的管理员,有一个全局颜色record,任何用户都可以将其设置为红色,绿色或蓝色。每个用户都有三个按钮,每种颜色一个,单击时设置全局颜色。管理员用户可以决定允许哪个用户将全局颜色设置为哪个值。用户权限的任何更改都需要实时反映在其GUI上,并且会被服务器强制执行。请注意:您可以在Github上找到此示例的代码。效果图如下
登陆
我们将使用基于文件的身份认证和明文密码来简化操作。要启用基于文件的身份认证,请在服务器配置文件config.yml的auth部分中进行配置。
auth: type: file options: path: ./users.yml # Path to the user file. Can be json, js or yaml hash: false # false indicates that we're using cleartext passwords
Every user has associated metadata that will be shared with both Valve and the client for permissioning. Here’s what this will look like: 每个用户都有关联的元数据,这些元数据将在Valve和客户端共享以实现实时权限。如下所示:
# User A userA: password: "usera-pass" serverData: permissionRecord: "permissions/usera" role: "user" clientData: permissionRecord: "permissions/usera" role: "user"
注意事项:
- 此数据存储在users.yml中
- 数据被指派两次。serverData将在服务器端的Valve规则中以user.data提供,clientData将作为第二个参数传递给客户端login()的successful回调。
- role可以是user或admin。管理员的客户端上会显示管理员的GUI,可以设置用户的权限。普通用户的GUI可以设置全局颜色 -- 如果权限允许他们设置。
- permissionRecord 是record的名称,它将包含以下格式的权限信息:
{ red: true, blue: true, green: false }
权限
我们的权限存储在一个名为permissions.yml的文件中。record部分如下所示:
record: "*": create: true write: true read: true delete: false listen: false
"global-color": write: "_(user.data.permissionRecord)[data.color]"
"permissions/*": write: "user.data.role === 'admin'"
- 默认权限
通常我们允许所有操作,但关闭删除和监听权限,因为本示例不使用这些功能。默认情况下拒绝所有操作,仅使能明确允许的操作是个好选择。
"*": create: true write: true read: true delete: false listen: false
- 允许用户设置全局颜色
这里我们决定是否允许用户将全局颜色设置为特定值。user.data.permissionRecord是在users.yml文件的serverData部分中指定的权限record的名称。
_()是交叉引用,用于将record数据加载到我们的权限规则中。data.color是用户尝试写入时的传入的颜色值。
"global-color": write: "_(user.data.permissionRecord)[data.color]"
- 仅允许管理员设置权限
接下来,我们需要确保只有管理员才能设置用户的权限。与前面类似,我们将检查对所有权限record(permissions/usera等等)的每次写操作,看尝试写操作的角色是不是admin。
"permissions/*": write: "user.data.role === 'admin'"
总结
我们只需要用这种机制就能确保在客户端和服务器上实时共享权限。每次用户尝试写入global-color时,都会相应地检查其权限。每次管理员启用或禁用权限时,普通用户GUI上的对应按钮都会使能或禁用。
用户定制数据
如何给每个用户发送不同的数据
应用程序的常见需求是需要将不同的数据发送给不同的用户。无论是社交供稿的更新,频繁购买者的折扣,约会平台上的匹配项列表,还是任何其他类型的私人或特定用户的信息。
幸运的是,deepstream的所有三个核心概念(数据同步,发布-订阅和请求-响应)均提供了实现此目标的各种方法。窍门就是将用户特定的record或event名称与deepstream的权限语言Valve结合使用。
特定用户的Records
提供私有或用户定制的Records的简单方法就是在record名称中包含用户名。如果您的社交网络有Lisa Miller的个人资料,只需将个人资料存储在名为profile/lisa-miller的record里:
const profile = ds.record.getRecord('profile/lisa-miller')
现在,我们需要确保每个人都可以阅读Lisa的个人资料,但只有Lisa自己可以编辑她的信息。这可以使用Valve实现。在permissions.yml的record段中,我们创建以下规则:
"profile/$username": read: true write: "user.id === $username"
此规则如何生效?首先我们创建profile/$username模式,只要访问名称与该模式匹配的record就会应用该规则。 read: true确保每个人都可以读取。user.id === $username确保record名称的$username部分要与用户登录的用户名相同才可以写。
特定用户的RPCs
到目前为止,一切都很简单。现在让我们看一个更高级的示例,其中包括HTTP身份认证节点和后端进程,该进程提供了用户定制数据作为对远程过程调用(RPC)的响应。假设我们经营一家在线宠物食品店,并且用户订购的频率越高,她获得的折扣就越高。这意味着我们在电子市场设置中需要三件事:
- 搭建身份认证服务器,用于检查尝试登录用户的凭据
- 可以访问价格及用户折扣的后端进程,该进程提供RPC服务来检索价格
- 一种确保用户在询问价格时使用的用户名就是他们自己的用户名的方法
总而言之,我们的设置如下所示:
http-auth-server <----> deepstream <----> RPC-provider ↖----→ client
让我们逐步介绍各个组件吧。首先客户端需要登录,我们将使用一个非常基本的登录表单:用户名,密码和一个标有“登录”的按钮。 您可以在Github存储库中找到该文件以及本指南的所有其他文件。 用户单击“登录”后,客户端将执行deepstream的登录方法,并提供用户名和密码作为数据。
login() { this.ds = deepstream( 'localhost:6020' ) this.ds.login({ username: this.username(), password: this.password() }, this._onLogin.bind( this )) }
请注意:我正在使用ES6类语法和非常简单但功能强大的KnockoutJS进行视图绑定。但是,相同的原理适用于React,Angular,Vue,Android,iOS或您内心想要的其他任何东西。
现在需要验证用户名和密码。我们将通过告诉deepstream对给定的URL发出HTTP POST请求来实现。可以在config.yml的auth段中进行配置:
auth: type: http options: endpointUrl: http://localhost:3000/authenticate-user permittedStatusCodes: [ 200 ] requestTimeout: 2000
该请求将由Express简单编写的HTTP服务器处理,该服务器检查用户凭据并在成功时返回HTTP状态代码200,失败时返回403。
为简单起见,我们将在此处使用明文密码的硬编码。在实际应用中,理想情况下此信息应被散列存储在数据库中,或者由oAuth API提供。
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); const port = 3000; const users = { 'user-a': { 'password': 'user-a-pass', 'serverData': { 'role': 'user' } }, 'user-b': { 'password': 'user-b-pass', 'serverData': { 'role': 'user' } }, 'data-provider': { 'password': 'provider-pass', 'serverData': { 'role': 'provider'} } }
app.use(bodyParser.json());
app.post('/authenticate-user', function (req, res) { console.log( 'received auth request for ' + req.body.authData.username ); const user = users[ req.body.authData.username ]; if( user && user.password === req.body.authData.password ) { res.status( 200 ).json({ username: req.body.authData.username, serverData: user.serverData }); } else { res.sendStatus( 403 ); } });
app.listen( port, function () { console.log( `listening on port ${port}` ); });
您是否注意到上面有一个叫“data-provider”额外用户?我们将使用它来认证来自后端进程的连接,这些后端进程可以向用户提供数据。这样的“数据提供者”首先需要连接并登录deepstream:
const deepstream = require( '@deepstream/client' ); const deepstreamUrl = 'localhost:6020'; const credentials = { username: 'data-provider', password: 'provider-pass' }; const ds = deepstream( deepstreamUrl );
ds.login( credentials, ( success, error, errorMsg ) => { if( success ) { console.log( 'connected to ' + deepstreamUrl ); } else { console.log( `failed to connect to ${deepstreamUrl} with ${errorMsg}` ); } });
接下来,我们将给数据提供者增加一个RPC接口get-price。这意味着我们告诉deepstream,每当客户端请求get-price时,此提供者将能够响应。
const itemPrice = 100; const userdata = { 'user-a': { discount: 0.1 }, 'user-b': { discount: 0.3 } }
ds.rpc.provide( 'get-price', ( data, response ) => { const discount = userdata[ data.username ].discount; const finalPrice = itemPrice - ( discount * itemPrice ); response.send( finalPrice ); });
让我们看一下上面的代码片段。为简单起见,我们将价格和各种折扣指定为静态数据,当用户发出请求时,他们将其用户名作为RPC数据的一部分发送:
this.ds.rpc.make( 'get-price', { username: this.username() }, this._onRpcResponse.bind( this ) );
为了确保提供的用户名确实是用户登录时使用的用户名,我们将再次使用deepstream的权限语言Valve:
request: "data.username === user.id"
上面的规则意味着提供者可以确保获得有效的、经过身份认证的用户名,并且可以在计算了正确的折扣后返回价格。
特定用户的Events和监听
特定于用户的Events,即deepstream的发布-订阅机制如何实现呢?从根本上说,它们的工作方式与Records相同:将用户名作为event名称的一部分,并使用Valve确保只有正确的用户才能订阅正确的event。
但这是否意味着您必须不断向所有用户发送event,无论这些订阅者是否在线?不是!幸运的是deepstream提供了一个称为监听的概念,它使您可以监视客户的record或event订阅,并仅在实际需要时才提供数据。
让我们看一个(有点荒谬的)示例:我们希望使用一系列event消息来创建用户,例如“Hey Lisa!”,“Hey Lisa!”,“Hey Lisa!”,等等。
this.ds.event.subscribe( 'user-updates/lisa-miller', ( msg ) => { // display the message });
在后端进程,我们注册一个“监听者”:
ds.event.listen( 'user-updates/*', ( match, response ) => { const username = match.replace( 'user-updates/', '' ); startUserGreeting( username ) response.accept()
response.onStop(() => {
endUserGreeting( username )
})
})
一旦有客户端订阅user-updates/lisa-miller event我们就发送特定消息给lisa-miller,一旦取消订阅,我们就会停止。要重申的是,对record也可以这样做。