使用搜索应用程序与不受信任的客户端

编辑

使用搜索应用程序与不受信任的客户端

编辑

构建用于搜索用例的前端应用程序时,返回搜索结果主要有两种方法:

  1. 客户端(用户的浏览器)向应用程序后端发出 API 请求,后端再向 Elasticsearch 发出请求。Elasticsearch 集群不会暴露给最终用户。
  2. 客户端(用户的浏览器)直接向搜索服务发出 API 请求——在这种情况下,客户端可以访问 Elasticsearch 集群。

本指南描述了采用第二种方法时的最佳实践。具体来说,我们将解释如何将搜索应用程序与向搜索应用程序搜索 API发出直接请求的前端应用程序一起使用。

这种方法有一些优点:

  • 无需维护前端应用程序和 Elasticsearch 之间的直通查询系统
  • 直接向 Elasticsearch 发出请求可加快响应时间
  • 查询配置在一个地方管理:您在 Elasticsearch 中的搜索应用程序配置

我们将介绍:

使用具有角色限制的 Elasticsearch API 密钥

编辑

当前端应用程序可以向 Elasticsearch 发出直接 API 请求时,限制它们可以执行的操作非常重要。在本例中,前端应用程序应该只能调用搜索应用程序搜索 API。为确保这一点,我们将创建具有角色限制的 Elasticsearch API 密钥。角色限制用于指定角色生效的条件。

以下 Elasticsearch API 密钥只能通过搜索应用程序搜索 API 访问website-product-search搜索应用程序:

resp = client.security.create_api_key(
    name="my-restricted-api-key",
    expiration="7d",
    role_descriptors={
        "my-restricted-role-descriptor": {
            "indices": [
                {
                    "names": [
                        "website-product-search"
                    ],
                    "privileges": [
                        "read"
                    ]
                }
            ],
            "restriction": {
                "workflows": [
                    "search_application_query"
                ]
            }
        }
    },
)
print(resp)
const response = await client.security.createApiKey({
  name: "my-restricted-api-key",
  expiration: "7d",
  role_descriptors: {
    "my-restricted-role-descriptor": {
      indices: [
        {
          names: ["website-product-search"],
          privileges: ["read"],
        },
      ],
      restriction: {
        workflows: ["search_application_query"],
      },
    },
  },
});
console.log(response);
POST /_security/api_key
{
  "name": "my-restricted-api-key",
  "expiration": "7d",
  "role_descriptors": {
    "my-restricted-role-descriptor": {
      "indices": [
        {
          "names": ["website-product-search"], 
          "privileges": ["read"]
        }
      ],
      "restriction":  {
        "workflows": ["search_application_query"] 
      }
    }
  }
}

indices.name必须是搜索应用程序的名称(复数),而不是底层的 Elasticsearch 索引。

restriction.workflows必须设置为具体值search_application_query

指定工作流限制至关重要。如果没有此限制,Elasticsearch API 密钥可以直接调用_search并发出任意 Elasticsearch 查询。在处理不受信任的客户端时,这是不安全的。

响应将如下所示:

{
  "id": "v1CCJYkBvb5Pg9T-_JgO",
  "name": "my-restricted-api-key",
  "expiration": 1689156288526,
  "api_key": "ztVI-1Q4RjS8qFDxAVet5w",
  "encoded": "djFDQ0pZa0J2YjVQZzlULV9KZ086enRWSS0xUTRSalM4cUZEeEFWZXQ1dw"
}

然后可以直接在授权标头中使用编码值。这是一个使用 cURL 的示例:

curl -XPOST "https://127.0.0.1:9200/_application/search_application/website-product-search/_search" \
 -H "Content-Type: application/json" \
 -H "Authorization: ApiKey djFDQ0pZa0J2YjVQZzlULV9KZ086enRWSS0xUTRSalM4cUZEeEFWZXQ1dw" \
 -d '{
  "params": {
    "field_name": "color",
    "field_value": "red",
    "agg_size": 5
  }
}'

如果expiration不存在,则默认情况下 Elasticsearch API 密钥永不过期。可以使用使 API 密钥无效 API使 API 密钥失效。

具有角色限制的 Elasticsearch API 密钥也可以使用字段级和文档级安全性。这进一步限制了前端应用程序查询搜索应用程序的方式。

使用搜索应用程序进行参数验证

编辑

您的搜索应用程序使用搜索模板来呈现查询。模板参数传递给搜索应用程序搜索 API。对于前端应用程序或不受信任的客户端使用的 API,我们需要进行严格的参数验证。搜索应用程序定义一个 JSON 模式,该模式描述了搜索应用程序搜索 API 允许的参数。

以下示例定义了一个具有严格参数验证的搜索应用程序:

resp = client.search_application.put(
    name="website-product-search",
    search_application={
        "indices": [
            "website-products"
        ],
        "template": {
            "script": {
                "source": {
                    "query": {
                        "term": {
                            "{{field_name}}": "{{field_value}}"
                        }
                    },
                    "aggs": {
                        "color_facet": {
                            "terms": {
                                "field": "color",
                                "size": "{{agg_size}}"
                            }
                        }
                    }
                },
                "params": {
                    "field_name": "product_name",
                    "field_value": "hello world",
                    "agg_size": 5
                }
            },
            "dictionary": {
                "properties": {
                    "field_name": {
                        "type": "string",
                        "enum": [
                            "name",
                            "color",
                            "description"
                        ]
                    },
                    "field_value": {
                        "type": "string"
                    },
                    "agg_size": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10
                    }
                },
                "required": [
                    "field_name"
                ],
                "additionalProperties": False
            }
        }
    },
)
print(resp)
const response = await client.searchApplication.put({
  name: "website-product-search",
  search_application: {
    indices: ["website-products"],
    template: {
      script: {
        source: {
          query: {
            term: {
              "{{field_name}}": "{{field_value}}",
            },
          },
          aggs: {
            color_facet: {
              terms: {
                field: "color",
                size: "{{agg_size}}",
              },
            },
          },
        },
        params: {
          field_name: "product_name",
          field_value: "hello world",
          agg_size: 5,
        },
      },
      dictionary: {
        properties: {
          field_name: {
            type: "string",
            enum: ["name", "color", "description"],
          },
          field_value: {
            type: "string",
          },
          agg_size: {
            type: "integer",
            minimum: 1,
            maximum: 10,
          },
        },
        required: ["field_name"],
        additionalProperties: false,
      },
    },
  },
});
console.log(response);
PUT _application/search_application/website-product-search
{
  "indices": [
    "website-products"
  ],
  "template": {
    "script": {
      "source": {
        "query": {
          "term": {
            "{{field_name}}": "{{field_value}}"
          }
        },
        "aggs": {
          "color_facet": {
            "terms": {
              "field": "color",
              "size": "{{agg_size}}"
            }
          }
        }
      },
      "params": {
        "field_name": "product_name",
        "field_value": "hello world",
        "agg_size": 5
      }
    },
    "dictionary": {
      "properties": {
        "field_name": {
          "type": "string",
          "enum": ["name", "color", "description"]
        },
        "field_value": {
          "type": "string"
        },
        "agg_size": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10
        }
      },
      "required": [
        "field_name"
      ],
      "additionalProperties": false
    }
  }
}

使用该定义,搜索应用程序搜索 API 执行以下参数验证:

  • 它只接受field_namefield_valueaggs_size参数
  • field_name仅限于采用“name”、“color”和“description”值
  • agg_size定义术语聚合的大小,它只能取110之间的值

使用 CORS

编辑

使用这种方法意味着用户的浏览器将直接向 Elasticsearch API 发出请求。Elasticsearch 支持跨域资源共享 (CORS),但此功能默认情况下处于禁用状态。因此,浏览器将阻止这些请求。

为此,有两种解决方法:

在 Elasticsearch 上启用 CORS
编辑

这是最简单的选项。通过将以下内容添加到您的elasticsearch.yml文件中来在 Elasticsearch 上启用 CORS:

http.cors.allow-origin: "*" # Only use unrestricted value for local development
# Use a specific origin value in production, like `http.cors.allow-origin: "https://<my-website-domain.example>"`
http.cors.enabled: true
http.cors.allow-credentials: true
http.cors.allow-methods: OPTIONS, POST
http.cors.allow-headers: X-Requested-With, X-Auth-Token, Content-Type, Content-Length, Authorization, Access-Control-Allow-Headers, Accept

在 Elastic Cloud 上,您可以通过编辑您的 Elasticsearch 用户设置来执行此操作。

  1. 从您的部署菜单中,转到编辑页面。
  2. Elasticsearch部分,选择管理用户设置和扩展
  3. 使用上面的配置更新用户设置。
  4. 选择保存更改
通过支持 CORS 的服务器代理请求
编辑

如果您无法在 Elasticsearch 上启用 CORS,则可以通过支持 CORS 的服务器代理请求。这比较复杂,但这是一个可行的选项。

了解更多

编辑