在函数计算的一些使用场景中,有时需要在函数中访问第三方服务以获取数据或触发其他工作流。第三方服务往往会通过白名单等机制进行访问控制,这也就需要我们事先将执行函数的机器 IP 加入到白名单中。
而函数计算在执行的过程中,会动态为函数分配执行机器,是无法预先获知机器外网 IP 的,也就无法事先添加白名单而造成无法通过第三方服务的访问控制。这个问题可以通过搭建代理的方式来解决,本文对此方案的步骤进行详细介绍。
具体场景及解法思路 假设用户服务 A ,使用函数计算实现,同时 A 在执行的过程中需要访问受保护的资源,该资源提供方为服务 B (如 MySQL 等),B 对来访者采用白名单方式鉴权。由于函数运行实例的不确定性及不可枚举性,无法在服务 B 事先添加白名单。
基本思想及方案是:搭建 Nginx 代理 A -> B 的访问,B 在白名单中添加代理服务器 IP,放入所有通过代理到达的请求并返回受保护资源。如考虑到代理服务器可被其他恶意访问者攻击或扫到,导致服务 B 中的数据被获取,可考虑在 proxy 或服务 B 中另行增加 Token 等校验方式。
Illustration
函数 A 使用 HTTP 触发器,接收外部用户实际请求,并在处理请求过程中访问函数 B 尝试获取受保护数据,如果获得该数据,则进行进一步处理操作,否则不进行特定操作。
函数 B 在这里同样使用 HTTP 触发器。B 利用白名单校验来访者,如果来访者通过校验则返回请求的资源,否则返回鉴权失败。
代理服务器使用 ECS 搭建,直接利用 Nginx 进行服务代理。
在服务 A 与服务 B 中创建函数 分别对 A 和 B 新建立两个服务,在左侧导航栏选择,分别进入到新建的服务中。 单击 【创建函数】,在创建函数页面: 单击 【选择全部的语言】,在下拉菜单中选择 Python2.7(本示例代码基于 Python )。 选择 【空白函数】。 选择【不创建触发器】。 创建函数并填写所在服务、函数名称、描述信息和运行环境信息。 单击【下一步】。 核对信息无误后,单击【创建】。 编写代码。 对于函数 A ,采用最简单的 HTTP 触发器方式建立,代码可直接 copy 下述内容并在控制台上提交保存。 对于函数 B ,使用了 Django 框架实现,因此可采用命令行工具 fcli 或 PythonSDK 上传代码包,也可使用 OSS 或 zip 包直接上传。 对 A 和 B 中的函数分别构建 HTTP 触发器。 在创建或搭建过程中如遇困难可参考这里的详情:
HTTP 触发器示例 至此,我们的服务就搭建完成了。因为 B 中的白名单只添加了代理服务器的公网 IP,此时 A 中直接调用 B 是无法获取授权的。下面,我们具体看下各个函数的内容。
编写函数 A 函数 A 直接采用普通的HTTP触发器触发。
编写函数 函数说明:
handler 为函数调用入口,主要为业务逻辑,在这里分别模拟了不经过代理调用函数 B 及经过代理调用函数 B 的调用结果。 get_data_by_url 主要封装了请求函数 B 数据的业务逻辑。 my_http_request 实现了利用代理发送 HTTP 请求的方式。
import logging import urllib, urllib2, sys import ssl import json
proxy_address = 'ip:port'
data_service_host = 'https://{id}.{region}.fc.aliyuncs.com' data_service_path = '/service/path' def handler(environ, start_response): """ entrance """ url = data_service_host + data_service_path # 使用代理访问 proxy_result = get_data_by_url(url, proxy_address) # 不使用代理访问 normal_result = get_data_by_url(url, None) # 展示用,直接将两种情况的返回拼接后展示出来 result = { "query_with_proxy_result": proxy_result, "query_without_proxy_result": normal_result } status = '200 OK' response_headers = [('Content-type', 'text/json')] start_response(status, response_headers) return json.dumps(result) def get_data_by_url(url, proxy): """ 用户数据服务访问封装 """ result = { "success": False, "secret_data": '', "data_service_raw_data": {} } content = my_http_request(url, proxy) if content: content = json.loads(content) result["data_service_raw_data"] = content # 模拟访问后数据处理 if "authorized" in content and content["authorized"]: result["success"] = True result["secret_data"] = content["protected_data"] return result def my_http_request(url, proxy=None): """ 带proxy的网络请求 """ try: ret = None socket_fd = None request = urllib2.Request(url) request.add_header('User-Agent', 'Fake_browser/1.0') if proxy: request.set_proxy(proxy, 'http') opener = urllib2.build_opener() socket_fd = opener.open(request) ret = socket_fd.read() except Exception as e: ret = json.dumps({"info": "exception in proxy query: %s" % e}) finally: if socket_fd: socket_fd.close() return ret 编写函数 B 函数 B 我们采用 Django 搭建了一个 Web 服务,服务中使用白名单设置进行鉴权,通过后返回相应数据。代码结构如下:
project/ ├── lib │ ├── Django-1.11.13.dist-info │ ├── django │ ├── pytz │ └── pytz-2018.4.dist-info ├── main.py └── src ├── init.py ├── bin │ ├── init.py │ └── manage.py ├── conf │ ├── init.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── data └── views ├── init.py └── view.py 以下章节为Django代码示例。
编写函数 函数入口:
#!/usr/bin/env python
import sys import os
sys.path.insert(0, os.path.dirname(os.path.abspath(file)) + '/lib') sys.path.append(os.path.join(os.path.dirname(os.path.abspath(file)), "src")) import django print (django.version) import views.view from conf.wsgi import application def handler(environ, start_response): import urlparse parsed_tuple = urlparse.urlparse(environ['fc.request_uri']) li = parsed_tuple.path.split('/') global base_path if not views.view.base_path: views.view.base_path = "/".join(li[0:5]) return application(environ, start_response) urls.py:
from django.conf.urls import url from django.contrib import admin from views import view urlpatterns = [ url(r'^index$', view.index), url(r'^get_data$', view.get_data), ] views.py:
#!/usr/bin/env python
from django.http import HttpResponse from django.conf import settings import logging import json logger = logging.getLogger() base_path = "" def index(request): """ 测试入口 """ logger.info("Django request detected! url: index") white_list = settings.WHITE_LIST allowed_hostlist = ' ' for allowed_host in white_list: allowed_hostlist += allowed_host allowed_hostlist += ' ' return HttpResponse("
WHITE_LIST = [ "127.0.0.1", "xx.xx.xx.xx" ] 搭建代理步骤 在这里我们直接使用 Nginx 进行了服务的代理。Nginx 可以部署到 ECS 中,也可以部署到普通服务器上。Nginx 的编译安装可直接在网上搜索或参考这里,安装后,配置文件代理部分可简单配置如下:
server{ resolver x.x.x.x; listen 8080; location / { proxy_pass http://$http_host$request_uri; } } 注意:
不能有 hostname 必须有 resolver( dns ),即上面的x.x.x.x,换成你们的 DNS 服务器 ip 即可。 $http_host 和 $request_uri 是 nginx 系统变量,不要想着替换他们,保持原样就 OK。 在实际生产环境中,我们建议搭建负载均衡集群(利用 nginx 、keepalived 等)及代理服务器集群进行代理服务,以提高系统可用性及整体性能。 查看dns方法:
cat /etc/resolv.conf 上传函数 A 、B 的代码并配置好触发器,并启动代理 Nginx ,我们就可以开始进行测试了。
本页目录 直连函数 B 获取数据结果 触发函数 A 本文使用上一步中创建的函数,测试结果如下。
直连函数 B 获取数据结果 直接使用浏览器触发函数 B,由于本地电脑 IP 不在 B 的白名单中,返回权限未校验:
{ # 未通过校验返回空白 "protected_data": "", "white_list": [ "127.0.0.1", "x.x.x.x" ], "authorized": false, # 返回的是来访者ip "remote_ip": "y.y.y.y" } 触发函数 A 通过浏览器触发函数 A ,获得 A 在处理请求过程中分别使用 Proxy 及直连方式访问函数 B 的结果:
{ "query_with_proxy_result": { "secret_data": "Alibaba", "success": true, "data_service_raw_data": { "remote_ip": "x.x.x.x", "white_list": [ "127.0.0.1", "x.x.x.x" ], "authorized": true, "protected_data": "Alibaba" } }, "query_without_proxy_result": { "secret_data": "", "success": false, "data_service_raw_data": { "remote_ip": "yy.yy.yy.yy", "white_list": [ "127.0.0.1", "x.x.x.x" ], "authorized": false, "protected_data": "" } } } 我们可以看到通过代理拿到了受保护的数据。
至此,已完成整体试验流程。您可以参考上述流程进行试验,并结合产品实际情况进行调整及应用。
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。