RESTHeart那件小事-2

上周主要介绍了RESTHeart的安装以及从MongoDB中读取文件相关的示例,本周将继续介绍两个非常重要的部分:写入以及安全操作。(PS:今天是母亲节,祝天下的母亲节日快乐!大家都要听妈妈的话哦~)

00:00/00:00

写入操作

简介

本章节将介绍RESTHeart如何使用PUT/POST/PATCH/DELETE等动作仅仅通过HTTP协议来创建、修改及删除MongoDB数据中的资源。
除了根目录(\)下的只读资源,所有资源都有一个JSON格式表示的状态(查阅表示形式的资料了解更多信息。)

示例

下面的请求:

  1. PUT /<db name>/<coll name> {"name":"mickey", "$currentDate": { "created_on": true}}

<db name>数据库上创建了一个<coll name> 集合,并将集合的状态设置为如下的属性(_etag属性是RESTHeart自动加上的,查阅文档了解更多信息):

  1. {
  2. "_id":"<coll name>",
  3. "created_on":{
  4. "$date":1494659126189
  5. },
  6. "name":"mickey",
  7. "_etag":{
  8. "$oid":"5916b0360a975a266ddfc778"
  9. }
  10. }

上面只是简单地介绍了文档资源。而相同的概念也可以运用于其他资源类型,例如数据库和集合等。

写入操作

下面的表格中总结了各个写入动作的语义:

操作 语义
PUT 插入或更新请求URL所标识的资源:如果资源不存在,那么PUT请求会创建该资源,并且将它的状态设置为请求JSON内容;如果资源存在,那么PUT请求会将它的状态设置为请求JSON内容。
POST 在请求URL说标识的资源之下插入或更新资源:如果资源不存在,那么PUT请求会创建该资源,并且将它的状态设置为请求JSON内容;如果资源存在,那么PUT请求会将它的状态设置为请求JSON内容。插入或 更新的资源通过请求JSON中的_id属性进行标识。如果请求中不包含_id字段,那么就会自动生成一个新的ObjectId,而新文档的URL会在响应这种通过Location头文件返回。
PATCH 修改请求URL说标识的资源状态,并且只应用那些请求JSON指定了的属性。PUTPOST都是替换整个状态而PATCH只是修改通过请求JSON中传递的属性。
DELETE 删除请求URL所标识的资源

upset:如果资源存在则进行更新,如果资源不存在则插入。它源于updateinsert的组合。

点符号

RESTHeart 使用点符号来存取数组元素以及一个嵌入文档的字段。
点符号可以用于PUT/PATCH以及POST操作。

数组

如果我们要指定或存取一个数组的某个元素(基于0的索引位置),将数组名字和基于0的索引位置链接起来,并且使用引号括起来:
"<array>.<index>"

示例:
  1. #原始数组:
  2. {
  3. "_id":"<doc id>",
  4. "array":[1,2,3,4,5,6]
  5. }
  6. #请求:
  7. PATCH /<db name>/<coll name>/<doc id> {"array.2": 200}
  8. #返回结果:
  9. {
  10. "_id":"<doc id>",
  11. "array":[1,2,200,4,5,6]
  12. }

嵌入文档

如果使用点操作符来指定或存取嵌入文档的字段,将嵌入文档的名字与字段名字使用点操作符(.)连结,并使用引号括起来:
"<embedded document>.<index>"

示例:
  1. #原始数组:
  2. {
  3. "_id":<doc id>,
  4. "name":{
  5. "first":"mickey",
  6. "last":"zhou"
  7. }
  8. }
  9. #请求:
  10. PATCH /<db name>/<coll name>/<doc id> {"name.last": "liu"}
  11. #返回结果:
  12. {
  13. "_id":<doc id>,
  14. "name":{
  15. "first":"mickey",
  16. "last":"liu"
  17. }
  18. }

更新操作

RESTHeart允许使用所有MongoDB的更新操作符。可以查阅MongoDB更新操作符的相关文档了解更多信息。
更新操作符可以被使用于PUT/PATCH以及POST操作中。

示例

考虑下面的文档:

  1. {
  2. "_id":"00003",
  3. "timestamp":{
  4. "$date":1494665692555
  5. },
  6. "array":[
  7. {
  8. "id":21,
  9. "value":21
  10. }
  11. ],
  12. "count":21,
  13. "message":"mickey's blog"
  14. }

进行下面的操作:

  1. PATCH /<db name>/<coll name>/<doc id> {
  2. # 增加新的属性``pi``(如果没有指定操作符,则会应用``$set``操作符)
  3. "pi":3.14,
  4. # 使用``$inc``字段操作符将``count``字段的值增加1
  5. "$inc":{"count":21},
  6. # 使用``$push``数组操作符向``array``字段增加一个新的条目
  7. "$push":{"array":{"id":1,"value":0}},
  8. # 使用``$unset``字段操作符删除``message``属性
  9. "$unset":{"message":null},
  10. #使用``$currentDate``操作符将``timestamp``字段更新为当前日期
  11. "$currentDate": {"timestamp": true}
  12. }

请求完成后,资源的状态更新为:

  1. {
  2. "_id":"00003",
  3. "timestamp":{
  4. "$date":1494666697677
  5. },
  6. "array":[
  7. {
  8. "id":21,
  9. "value":21
  10. },
  11. {
  12. "id":1,
  13. "value":0
  14. }
  15. ],
  16. "count":42,
  17. "_etag":{
  18. "$oid":"5916cdc90a975a266ddfc78a"
  19. },
  20. "pi":3.14
  21. }

批量写入请求

批量写入请求在一次单一的请求中创建、更新或删除多个文档。

POST一个文档数组

如果想要插入或更新多个文档的话,传递一个文档的数组作为请求的主体。
POST包含已有的_ids的文档来进行更新。批量的POSTPATCH的操作行为相同:只会更新请求数据中的属性。
响应中包括创建的文档的URIs。所有插入或更新的文档有相同的_etag属性,将会包含在_ETAG响应头文件中:可以在_eTag属性上对集合中的文档进行顾虑,从而检索出使用同一个请求进行插入或更新的文档。

示例
  1. #请求
  2. POST /<db name>/<coll name> [ { "seq": 1 }, { "seq": 2 }, { "seq": 3 }, { "seq": 4 } ]
  3. #响应
  4. {
  5. "_embedded": {
  6. "rh:result": [
  7. {
  8. "_links": {
  9. "rh:newdoc": [
  10. {
  11. "href": "/xxx/yyy/5716560a2d174cac010daf17"
  12. },
  13. {
  14. "href": "/xxx/yyy/5716560a2d174cac010daf18"
  15. },
  16. {
  17. "href": "/xxx/yyy/5716560a2d174cac010daf19"
  18. },
  19. {
  20. "href": "/xxx/yyy/5716560a2d174cac010daf1a"
  21. }
  22. ]
  23. },
  24. "inserted": 4,
  25. "deleted": 0,
  26. "modified": 0,
  27. "matched": 0
  28. }
  29. ]
  30. }
  31. }

使用通配符文档ID来PATCH多个文档

如果需要使用PATCH操作来修改多条文档的属性,使用下面的语句:

  1. #PATCH 批量请求
  2. PATCH /<db name>/<coll name>/*?filter={<filter_query>}

filter查询参数为强制参数,指定了一个MongoDB查询。
响应包含更新的文档数目。
所有更新的文档拥有相同的_etag属性,将会包含在_ETAG响应头文件中:可以在_eTag属性上对集合中的文档进行顾虑,从而检索出使用同一个请求进行插入或更新的文档。

通配符文档ID:注意URI:PATCH /<db name>/<coll name>/*中的 *文档ID是一个批量文档更新操作,其中PATCH /<db name>/<coll name>修改集合的属性。

示例

向所有缺少num属性的文档中增加num属性

  1. #请求
  2. PATCH /db/coll/*?filter={"num": {"$exists": false } } { "num": 1 }
  3. #响应
  4. {
  5. "_embedded": {
  6. "rh:result": [
  7. {
  8. "inserted": 0,
  9. "deleted": 0,
  10. "modified": 9,
  11. "matched": 9
  12. }
  13. ]
  14. }
  15. }

使用通配符文档ID来删除多个文档

如果需要使用DELETE操作来修改多条文档的属性,使用下面的语句:

  1. #DELETE 批量请求
  2. DELETE /<db name>/<coll name>/*?filter={<filter_query>}

filter查询参数为强制参数,指定了一个MongoDB查询。
响应包含删除的文档数目。
通配符文档ID:注意URI:DELETE /<db name>/<coll name>/*中的 *文档ID是一个批量删除文档操作,其中DELETE /<db name>/<coll name>删除集合(为了安全起见,需要在请求的头文件中指明ETag)。

示例

删除所有creation_date属性早于1/1/2016的文档

  1. #请求
  2. DELETE /db/coll/*?filter={"creation_date": {"$lt": {"$date": 1451606400000 } } }
  3. #响应
  4. {
  5. "_embedded": {
  6. "rh:result": [
  7. {
  8. "inserted": 0,
  9. "deleted": 23,
  10. "modified": 0,
  11. "matched": 0
  12. }
  13. ]
  14. }

安全验证

启用和配置安全验证

介绍

如果需要启动安全验证,在启动RESTHeart时必须指定有合适安全验证配置的配置文件
配置文件默认的传统是:同时禁止验证和授权(不对用户进行验证,任何人可以做任何事情)
一旦启用安全验证,请求会经历下面的流程:

  • 客户端通过SSL提交一个请求,使用基本的验证方法提供用户名和密码证书
  • RESTHeart的身份管理器(IDM)启动请求的验证,例如,通过用户提供的用户名和密码对用户身份进行验证
    • 如果验证失败,请求会返回一个401 unauthorized的响应码
    • 如果验证成功,请求继续
  • RESTHeart的存取管理器(AM)启动请求的授权,例如,确定客户端是否在配置好的安全策略中拥有执行该请求的权限:
    • 如果授权失败,请求将返回一个403 Forbidden的响应码
    • 如果验证成功,请求继续
  • RESTHeart执行与MongoDB交互的请求,最后返回200 ok的响应码(根据请求类型以及执行结果确定)

需要搭建的配置部分包括:

  • htts listener 来启用SSL(如果不启用SSL的话,可以通过反向代理)
  • idm来指定身份管理器(该组件负责验证一个客户端,例如,验证它的身份)
  • access-manager来指定存取管理器(该组件负责授权一个请求,例如,确定客户端是否有执行请求的权限)

客户端必须通过基础认证机制发送用户名和密码认证。RESTHeart是无状态链接:并不会保存授权信息,因此每次请求都必须发送认证信息。查阅 客户端如何认证 了解更多信息。

HTTPS 监听器

HTTP并不安全:认证信息在传输过程中有可能会被泄露。因此,使用HTTPS是至关重要的。
有许多方法可以启用HTTPS:例如,我们可以在RESTHeart 之前创建一个网站服务器(例如,nginx)作为反向代理,或者可以使用能够提供负载均衡的云服务,用来为我们管理SSL(例如,亚马逊的ASW和谷歌云等)。
任何情况下,RESTHeart都可以直接暴露给HTTPS协议,并且配置好https监听程序。建议比较小的系统使用这种配置。

下面的配置文件显示了涉及到的选项:

  1. #### listeners
  2. https-listener: true
  3. https-host: 0.0.0.0
  4. https-port: 4443
  5. #### SSL configuration
  6. use-embedded-keystore: true
  7. #keystore-file: /path/to/keystore/file
  8. #keystore-password: password
  9. #certpassword: password

为了启用https配置https监听程序,使用下面的选项:

  • https-listenertrue进行启用
  • https-host为绑定监听器的ip地址
  • https-port为绑定监听器的端口号

为了启用https监听器,必须配置一个有效的SSL认证,有两个选项:

  • 使用默认的RESTHeart自签名的证书
  • 使用我们自己的证书
  • 如何使用默认的自签名证书

用来指定使用默认的、嵌入的自签名的证书的唯一选项是:use-embedded-keystore: true

使用自签名的证书保证数据进行了加密,防止了第三方的攻击。然而,也导致了一些客户端和所有浏览器的问题,因为它不能向客户端保证服务器的身份。例如:
-使用curl的时候,我们需要指定--insecure选项,否则会出现错误信息。
– 使用所有浏览器的时候,用户都会受到关于服务器身份的警告。

如何使用一个有效的证书
当然,我们需要从一个证书机构得到一个有效的证书:我们需要配置javakeystore来配置,并且制定下面的RESTHeart选项:

  • use-embedded-keystorefalse 以禁止默认的自签名证书
  • keystore-filekeystore 文件的路径
  • keystore-passwordkeystore的密码
  • certpassword:认证密码

身份管理器

配置

身份管理负责授权给一个客户端,验证它的证书,最后讲请求和它的用户名及角色联系起来。
如果客户端证书无效,返回的响应将会是HTTP/1.1 401 Unauthorized

IDM是插件化的:可以配置真实的IDM实现。可以查阅相关文档了解如何开发和配置一个自定义的IDMyaml配置文件中的idm部分如下:

  1. idm:
  2. implementation-class: org.restheart.security.impl.SimpleFileIdentityManager
  3. conf-file: ./etc/security.yml
  • implementation-class指定了实现IDMjava类。我们可以使用已有实现中的一种或者自己实现一个。
  • conf-file为一个yaml配置文件的路径,传递给IDM。注意:路径可以是绝对路径或者是针对restheart.jar的相对路径。

SimpleFileIdentityManager

RESTHeartSimpleFIleIdentityManageryaml文件中认证用户(类为:org.restheart.security.impl.SimpleFileIdentityManager),直观看来,配置文件的的形式大致如下:

  1. users:
  2. - userid: admim
  3. password: changeit
  4. roles: [admins]
  5. - userid: user
  6. password: changeit
  7. roles: [users]

DbFileIdentityManager

RESTHeartSimpleFIleIdentityManager认证在一个MongoDB的集合中定义的用户(类为:org.restheart.security.impl.SimpleFileIdentityManager),直观看来,配置文件的格式为:

  1. dbim:
  2. - db: userbase
  3. coll: _accounts
  4. cache-enabled: false
  5. cache-size: 1000
  6. cache-ttl: 60000
  7. cache-expire-policy: AFTER_WRITE

dbcoll属性指向用户的集合(在本示例中,为userbase._accounts)。其它的选项控制缓存,以避免IDM真的在每次请求的时候都发送一个MongoDB查询。

这个集合必须有以下字段:
_id:用户ID
password:字符串
role:字符串的数组

注意:_account集合名中的_前缀,RESTHeart将集合名以_开头的集合当作保留集合:它们不会通过API泄露给客户端。否则的话,用户将会非常容易地读取密码和创建用户。如果你真的想暴露出去,删除前缀并且保证敏感数据,我们可以有几种做法来实现:
– 通过存取管理器来强制定义一个合适的存取政策以保证用户只会读取他们自己的数据
– 通过使用一个表示转化器来从响应中过滤掉password属性

存取管理器

存取管理器负责在安全策略之上授权请求。
如果一个请求为授权(例如,安全策略不允许该请求),返回的响应将会是HTTP/1.1 403 Forbidden

AM是可插拔的:可以配置真实的AM实现。可以查阅相关文档了解如何开发和配置一个自定义的AMyaml配置文件中的access-manager部分如下:

  1. access-manager:
  2. implementation-class: org.restheart.security.impl.SimpleAccessManager
  3. conf-file: ./etc/security.yml
  • implementation-class指定了实现Access Managerjava类。我们可以使用已有实现中的一种或者自己实现一个。
  • conf-file为一个yaml配置文件的路径,传递给Access Manager。注意:路径可以是绝对路径或者是针对restheart.jar的相对路径。

SimpleFileAccessManager

RESTHeart中默认的存取管理器实现为SimpleFileAccessManager(类为:org.restheart.security.impl.SimpleFileAccessManager)。AM将在yaml中定义的存取策略强制作为一系列权限的集合。直观看来,配置文件的的形式大致如下:

  1. permissions:
  2. - role: admins
  3. predicate: path-prefix[path="/"]
  4. - role: $unauthenticated
  5. predicate: path-prefix[path="/publicdb/"] and method[value="GET"]
  6. - role: users
  7. predicate: path-prefix[path="/publicdb/"]

permissions给定了角色以及表达为一些http请求的谓语。将权限分配给一个特殊的角色$unauthenticated可以在不需要授权的基础上允许对一些资源的访问。

在URI重映射的资源上指定全选

mongo-mounts配置选项允许我们映射MongoDB资源的URI
规则:当资源的URI通过mongo-mounts配置选项进行重映射时,path属性的权限必须与mongo-mounts选项的what参数相关。
例如,使用下面的配置,所有数据库mydb的集合都给定了路径前缀/api

  1. mongo-mounts:
  2. - what: /mydb
  3. where: /api

集合/mydb/coll被重映射到了URI/api/coll上,而允许在集合上进行GET请求的谓语为:

  1. permissions:
  2. - role: $unauthenticated
  3. predicate: path-prefix[path="/coll"] and method[value="GET"]

客户端如何认证

简介

客户端通过标准的基础认证传递证书,对于一个HTTP用户代理而言,一个标准的方法就是:在发送请求的时候提供用户名和密码。
RESTHeart是无状态链接:并不会保存授权信息,因此每次请求都必须发送认证信息。

一些示例

使用http-a选项:
http -a userid:password GET 127.0.0.1:8080/
使用curl-user选项:
curl -i --user userid:password -X GET 127.0.0.1:8080/

基础认证

基础认证需要客户端使用认证请求头文件来发送证书。认证请求头文件的值必须是:Basic base64(<userid> + ':' + <password>)
换句话说:
– 用户名和密码被组合成了一个字符串userid:password,特别注意用户名和密码之间的冒号(用户名中不能包含冒号这个字符)
– 结果字符串是base 64编码的
– 编码字符串之前必须包含一个字符串Basic(注意中间有空格)

认证令牌

如果一个请求被成功认证,那么RESTHeart就会生成一个认证令牌并且在之后的每个响应中都会包含该令牌。之后的请求可以使用密码或认证令牌。
在基础的认证模式中,认证令牌被用作一个临时的密码。这也就意味授权请求头文件中可以使用密码或者是认证令牌本身:Authorization: Basic base64( + ‘:’ + ) 或者 Authorization: Basic base64( + ‘:’ + )
认证令牌信息在下面的响应头文件中传递:

  1. Auth-Token: 6a81d622-5e24-4d9e-adc0-e3f7f2d93ac7
  2. Auth-Token-Location: /_authtokens/user@si.com
  3. Auth-Token-Valid-Until: 2015-04-16T13:28:10.749Z

注意:在认证令牌位置头文件中的URI:客户端可以发送一个GET请求来获取令牌的信息或者发送一个DELETE操作来使其失效。当然,客户端只可以请求自己的令牌(否则的话,将会返回一个403 Forbidden的错误码)。

在开发网页应用的时候,认证令牌是一个非常重要的功能。因为每次请求都需要包含证书,我们需要将它们存储在cookie或者session(更优选择)中。登录界面可以使用真实的密码进行检验,而一旦登录成功,就可以存储和使用认证令牌了。

在多借点部署的时候(水平拓展),需要注意认证令牌。在这种情况下,我们需要禁止它,或者使用一个黏性session选项的负载均衡器,或者使用一个分布式认证令牌缓存实现。

认证令牌通过下面的配置选项进行实现:

  1. auth-token-enabled: true
  2. auth-token-ttl: 15 # time to live after last read, in minutes

检查证书和获取用户角色的建议方式

默认的配置文件创建可有用的GetRoleHandler,与/_logic/roles/<userid>进行了绑定。

请求GET /_logic/roles/<userid>可能的响应码为:

  • 401 Unauthorized => 缺少证书或错误的证书
  • 403 Forbidden => URL中的用户名与授权头文件中的用户名不匹配
  • 200 OK 证书匹配,将会返回下面的响应文档:
  1. {
  2. "_embedded": {},
  3. "_links": {
  4. "self": {
  5. "href": "/_logic/roles/user@si.com"
  6. }
  7. },
  8. "authenticated": true,
  9. "roles": [
  10. "USER"
  11. ]
  12. }

当然,如果请求成功的话,客户端将会得到返回的认证令牌。
我们能够非常轻易地从登录表格中检查用户的证书:客户端得到200的响应码就说明已经匹配成功,我们就可以存储认证令牌用于之后的请求,否则的话,就说明证书错误。

如何避免浏览器中的基础认证弹窗

使用基础认证。浏览器有可能会显示比较恶心的登录弹窗,这并不是我们想要的。
在屏幕下面发生了什么?服务器向浏览器发送了WWW-Authenticate响应头文件才是带来这个弹窗的真实原因。
我们有两种方式可以避免RESTHeart出现弹窗:

  • 在请求头文件中指定No-Auth-Challenge
  • 在查询中使用noauthchallenge

在这种情况下,RESTHeart将会响应401 Unauthorized

这个功能和认证令牌一起,可以帮助我们在基础认证机制的基础上实现一个基于表格的认证体验。

附一些简单的jQuery代码:

  1. function simplePUTTest(){
  2. $.ajax({ type: "PUT", url: "http://<your server ip>:8080/test/testcol/", data: JSON.stringify({"name":"mickey","$currentDate": { "created_on": true }}),contentType: "application/json"});
  3. }
  4. function simplePATCHTest(){
  5. $.ajax({ type: "PATCH", url: "http://<your server ip>:8080/test/testcol/00003", data: JSON.stringify({"pi":3.14,
  6. "$inc":{"count":21},
  7. "$push":{"array":{"id":1,"value":0}},
  8. "$unset":{"message":null},
  9. "$currentDate": {"timestamp": true}}),contentType: "application/json"});
  10. }
  11. function simplePOSTTest(){
  12. $.ajax({ type: "POST", url: "http://<your server ip>:8080/test/testcol", data: JSON.stringify({"_id": "00003",
  13. "$currentDate": { "timestamp": true },
  14. "array": [ {"id":21, "value": 21} ],
  15. "count": 21,
  16. "message": "mickey's blog"}),contentType: "application/json"});
  17. }
  18. </script>
打赏

mickey

记录生活,写给几十年后的自己。