如何使用Python Flask编写Web服务

本文提供了一个教程,指导如何使用Python Flask创建一个简单的RESTful Web服务,该服务接收Threat Stack的JSON Webhook,解析警报ID,从Threat Stack获取详细警报数据,并将数据存档到AWS S3。文章涵盖了项目结构、应用模块、视图和蓝图,以及如何处理与Threat Stack和S3的交互。虽然这个服务尚未包含前端展示和错误处理,但它展示了一个适用于初学者的简单Web服务构建结构。
摘要由CSDN通过智能技术生成

我们的许多客户正在使用我们的Webhook功能来构建有用的服务,但不幸的是,其他客户却没有。 我们经常听到他们的团队中没有人足够熟练地编写一种服务,该服务可以提取Webhook负载并处理数据。 这使得他们要么希望(不太可能)从他们的开发团队那里获得周期,要么就继续没有。

但是,如果您可以编写自己的Web服务呢? 您可以自动执行多少个涉及从系统A中获取数据并将其输入到系统B中的例行任务?

学会足够好的编码可能是您工具箱中的一项主要技能,也是优化组织中安全流程的一项重要资产。 在本文中,我将引导您完成一个教程,该教程将使您开始使用Python Flask编写自己的Web服务。

我们正在建立的

具体来说,我将逐步创建一个提供RESTful Web服务的简单Python Flask应用程序。 该服务将提供以下端点:

  • 从Threat Stack接收JSON格式的有效负载(webhook)
  • 解析有效负载以获取威胁堆栈警报ID
  • 从Threat Stack检索详细的警报数据
  • 将Webhook和警报数据存档到AWS S3

但是在我跳进去之前,请记住两点。 首先,我不会为任何形式的前端显示功能所困扰,因此您不必担心HTML或CSS。 其次,我的组织遵循Flask自己建议的组织 。 我将跳过单个模块模式,直接进入Packages and Blueprints模型

Flask教程种类繁多。 一方面,有一些教程介绍了如何构建小型,简单的应用程序(其中整个应用程序适合于一个文件)。 另一方面,有一些教程介绍了如何构建更大,更复杂的应用程序。 本教程填补了中间的一个甜头,并演示了一个简单的结构,但可以立即适应日益复杂的要求。

项目结构

我将要构建的项目结构(来自Explore Flask )如下所示:



   
   
Threatstack-to-s3

├── app

│   ├── __init__.py

│   ├── models

│   │   ├── __init__.py

│   │   ├── s3.py

│   │   └── threatstack.py

│   └── views

│       ├── __init__.py

│       └── s3.py

├── gunicorn.conf.py

├── requirements.osx.txt

├── requirements.txt

└── threatstack-to-s3.py

顶级文件

我将从构建服务时对我有用的顶级文件开始讨论:

Gunicorn.conf.py:这是Gunicorn WSGI HTTP服务器的配置文件,将为该应用提供服务。 尽管应用程序可以自行运行和接受连接,但Gunicorn在处理多个连接并允许应用程序随负载扩展方面更有效。

Requirements.txt / requirements.osx.txt:此文件中列出了应用程序的依赖项。 pip实用程序使用它来安装所需的Python软件包。 有关安装依赖项的信息,请参见此README.md的“设置”部分。

Threatstack-to-s3.py:这是应用程序启动器。 如果要进行本地调试,则可以直接使用“ python”运行它,也可以将其作为参数传递给“ gunicorn”作为应用程序入口点。 有关如何启动服务的信息,请参见README.md

应用包(app /目录)

该应用程序包是我的应用程序包。 应用程序的逻辑位于此目录下。 正如我前面提到的,我选择将应用程序分成多个较小的模块,而不是使用单个整体式模块文件。

此软件包中定义的以下四个可用模块是:

注意: app.viewsapp.models不提供任何内容,并且它们的__init__.py文件为空。

应用模块

应用程序模块负责创建Flask应用程序。 它导出单个函数create_app() ,该函数将创建Flask应用程序对象并对其进行配置。 当前,它会初始化与我的应用程序视图相对应的应用程序蓝图。 最终, create_app()会做其他事情,例如初始化日志记录,但是为了清楚和简单起见,我现在跳过了。

App / __ init__.py


   
   
from flask import Flask

def _initialize_blueprints ( application ) :
    '''
    Register Flask blueprints
    '''

    from app. views . s3 import s3
    application. register_blueprint ( s3 , url_prefix = '/api/v1/s3' )

def create_app ( ) :
    '''
    Create an app by initializing components.
    '''

    application = Flask ( __name__ )

    _initialize_blueprints ( application )

    # Do it!
    return application
Copy

Threatstack-to-s3.py使用此模块启动应用程序。 它导入create_app() ,然后使用它创建Flask应用程序实例。

Threatstack-to-s3.py



   
   
#!/usr/bin/env python
from app import create_app

# Gunicorn entry point.
application = create_app ( )

if __name__ == '__main__' :
    # Entry point when run via Python interpreter.
    print ( "== Running in debug mode ==" )
    application. run ( host = 'localhost' , port = 8080 , debug = True )
Copy

视图和烧瓶蓝图

在讨论其余三个模块之前,我将讨论什么视图和Flask蓝图,然后深入研究app.views.s3模块。

视图:视图是应用程序使用者所看到的。 该应用程序没有前端,但是有一个公共API端点。 将视图视为使用该应用程序的人或事物(例如,消费者)可以并且应该暴露的观点。 最佳做法是使视图尽可能简单。 如果端点的工作是接收数据并将其复制到S3,则使其执行该功能,但在应用程序模型中隐藏如何完成此操作的详细信息。 视图应主要代表消费者希望看到的动作,而细节(消费者不关心的细节)存在于应用程序模型中(稍后描述)。

Flask蓝图:之前我说过,我将使用Packages and Blueprints布局而不是单个模块应用程序。 蓝图包含我的API端点结构的一部分。 这使我可以对API的相关部分进行逻辑分组。 就我而言,每个视图模块都是其自己的蓝图。

学到更多

Flask网站上的带有Blueprints文档的模块化应用程序

Explore Flask是一本有关使用Flask开发Web应用程序的最佳实践和模式的书。

App.views.s3模块

Threatstack-to-s3服务接收Threat Stack Webhook HTTP请求,并将警报数据的副本存储在S3中。 我在这里存储允许某人执行此操作的API端点集。 如果回头看一下app / __ init__.py ,您会发现我已经将端点集植于/ api / v1 / s3

从app / __ init__.py


   
   
    from views. s3 import s3
    app. register_blueprint ( s3 , url_prefix = '/api/v1/s3' )
Copy

我使用此路径有以下几个原因:

  • API:请注意,这是一个API,我不应该期望前端。 也许有一天我会添加一个前端。 可能没有,但是我觉得这在心理上是有用的,并且是对其他人的标志
  • V1:这是API的版本1。 如果我需要进行重大更改以适应新需求,则可以添加v2,以便在将所有使用者迁移到新版本时存在两个API。
  • S3:这是我正在连接并使用的服务。 在这里,我可以自由地随意命名路径的这一部分,但我希望保持描述性。 例如,如果该服务将数据中继到HipChat,则可以将其命名为hipchat

app.views.s3中 ,我现在提供一个端点/ alert ,它表示我正在操作的对象,并且仅响应HTTP POST请求方法。

切记:构建API时,URL路径应代表名词,而HTTP请求方法应代表动词。

App / views / s3.py


    
    
'''
API to archive alerts from Threat Stack to S3
'''


from flask import Blueprint , jsonify , request
import app. models . s3 as s3_model
import app. models . threatstack as threatstack_model

s3 = Blueprint ( 's3' , __name__ )


@ s3. route ( '/alert' , methods = [ 'POST' ] )
def put_alert ( ) :
    '''
    Archive Threat Stack alerts to S3.
    '''

    webhook_data = request. get_json ( )
    for alert in webhook_data. get ( 'alerts' ) :
        alert_full = threatstack_model. get_alert_by_id ( alert. get ( 'id' ) )
        s3_model. put_webhook_data ( alert )
        s3_model. put_alert_data ( alert_full )

    status_code = 200
    success = True
    response = { 'success' : success }

    return jsonify ( response ) , status_code  
Copy

现在,我将逐步介绍该模块的一些关键部分。 如果您对Python足够熟悉,则可以跳过接下来的几行导入,但是如果您想知道为什么我要重命名导入的内容,请继续。



   
   
from flask import Blueprint , jsonify , request
import app. models . s3 as s3_model
import app. models . threatstack as threatstack_model  
Copy

我喜欢简洁和一致。 我可以通过以下方式来导入模型模块:



   
   
import app. models . s3
import app. models . threatstack
Copy

但这意味着我将使用如下功能:



   
   
app. models . s3 . put_webhook_alert ( alert )  
Copy

我也可以这样做:



   
   
from app. models import s3 , threatstack
Copy

但是,当我稍后创建s3 Blueprint对象时,这会中断,因为我将覆盖s3模型模块。



    
    
s3 = Blueprint ( 's3' , __name__ ) # We've just overwritten the s3 module we imported.  
Copy

由于这些原因,导入模型模块并对其稍做重命名就更容易了。

现在,我将遍历应用程序端点和与其关联的功能。



   
   
@ s3. route ( '/alert' , methods = [ 'POST' ] )
def put_alert ( ) :
    '''
    Archive Threat Stack alerts to S3.
    '''

Copy

第一行称为装饰器。 我向s3蓝图添加了一条名为/ alert的路由(扩展为/ api / v1 / s3 / alert ),当对它发出HTTP POST请求时,它将导致put_alert()被调用。

该函数的主体非常简单:

  • 获取请求的JSON数据
  • 遍历警报键中的数组
  • 对于每个警报:
    • 从Threat Stack检索警报详细信息
    • 将警报信息存储在S3中的请求中
    • 将警报详细信息存储在S3中


   
   
    webhook_data = request. get_json ( )
    for alert in webhook_data. get ( 'alerts' ) :
        alert_full = threatstack_model. get_alert_by_id ( alert. get ( 'id' ) )
        s3_model. put_webhook_data ( alert )
        s3_model. put_alert_data ( alert_full )
Copy

完成后,我将返回一个简单的JSON文档,指示事务成功或失败。 (注意:这里没有错误处理,因此,我当然已经对成功响应和HTTP状态代码进行了硬编码。稍后添加错误处理时,我将对其进行更改。)



   
   
    status_code = 200
    success = True
    response = { 'success' : success }

    return jsonify ( response ) , status_code
Copy

至此,我已经满足了我的要求,并完成了消费者的要求。 请注意,我还没有包含任何代码来说明如何实现请求。 我必须怎么做才能获得警报的详细信息? 我执行了哪些操作来存储警报? 如何在S3中存储和命名警报? 消费者并不真正在乎那些细节。 这是考虑如何在自己的服务中组织代码的好方法:消费者需要了解的内容应该存在于您的视图中。 消费者不需要知道的细节应该包含在您的模型中,我将要介绍它。

在讨论其余模块之前,我将讨论模型,即如何与我正在使用的服务(例如Threat Stack和S3)进行对话。

楷模

模型描述了“事物”,而这些“事物”是我要对其执行操作的事物。 通常,当您在Flask模型上搜索帮助时,博客和文档都喜欢在其示例中使用数据库。 尽管我现在正在做的事情并不遥远,但我只是将数据存储在对象存储中,而不是数据库中。 这不是我将来可能会使用Threat Stack接收的数据进行的唯一处理。

另外,我选择跳过面向对象的方法,而倾向于过程样式。 在更高级的Python中,我将为警报对象建模并提供一种操纵它的方法。 但这带来了比给定的将数据存储在S3中的任务所需的更多复杂性,并且还使代码更复杂,难以演示一个简单的任务。 为此,我选择了简洁和清晰,而不是技术上的正确性。

App.models.threatstack模块

如您所app.models.threatstack模块处理与Threat Stack的通信。



   
   
'''
Communicate with Threat Stack
'''

import os
import requests

THREATSTACK_BASE_URL = os . environ . get ( 'THREATSTACK_BASE_URL' , 'https://app.threatstack.com/api/v1' )
THREATSTACK_API_KEY = os . environ . get ( 'THREATSTACK_API_KEY' )

def get_alert_by_id ( alert_id ) :
    '''
    Retrieve an alert from Threat Stack by alert ID.
    '''

    alerts_url = '{}/alerts/{}' . format ( THREATSTACK_BASE_URL , alert_id )

    resp = requests. get (
        alerts_url ,
        headers = { 'Authorization' : THREATSTACK_API_KEY }
    )

    return resp. json ( )
Copy

只需快速浏览以下几个要点:



   
   
THREATSTACK_BASE_URL = os . environ . get ( 'THREATSTACK_BASE_URL' , 'https://app.threatstack.com/api/v1' )
THREATSTACK_API_KEY = os . environ . get ( 'THREATSTACK_API_KEY' )
Copy

我不想在代码中保留Threat Stack API。 这只是良好的干净代码/安全性生活。 我现在要从我的环境中获取API密钥,因为它是一种快速,简单的解决方案。 在某些时候,我应该将所有配置集中在一个文件中,而不是将其隐藏在此处,因此代码和设置会更简洁一些。 这是另一回事,现在,设置记录在README.md中



   
   
def get_alert_by_id ( alert_id ) :
    '''
    Retrieve an alert from Threat Stack by alert ID.
    '''

    alerts_url = '{}/alerts/{}' . format ( THREATSTACK_BASE_URL , alert_id )

    resp = requests. get (
        alerts_url ,
        headers = { 'Authorization' : THREATSTACK_API_KEY }
    )

    return resp. json ( )
Copy

get_alert_by_id()函数获取警报ID,在Threat Stack平台上查询警报数据,然后返回该数据。 我正在使用Python 请求模块向Threat Stack API端点发出HTTP GET请求,该请求返回给定警报的警报信息。

阅读Threat Stack API文档

App.models.s3模块

app.models.s3模块处理与AWS S3的连接。



   
   
'''
Manipulate objects in AWS S3.
'''

import boto3
import json
import os
import time

TS_AWS_S3_BUCKET = os . environ . get ( 'TS_AWS_S3_BUCKET' )
TS_AWS_S3_PREFIX = os . environ . get ( 'TS_AWS_S3_PREFIX' , None )

def put_webhook_data ( alert ) :
    '''
    Put alert webhook data in S3 bucket.
    '''

    alert_time = time . gmtime ( alert. get ( 'created_at' ) / 1000 )
    alert_time_path = time . strftime ( '%Y/%m/%d/%H/%M' , alert_time )
    alert_key = '/' . join ( [ alert_time_path , alert. get ( 'id' ) ] )
    if TS_AWS_S3_PREFIX:
        alert_key = '/' . join ( [ TS_AWS_S3_PREFIX , alert_key ] )

    s3_client = boto3. client ( 's3' )
    s3_client. put_object (
        Body = json. dumps ( alert ) ,
        Bucket = TS_AWS_S3_BUCKET ,
        Key = alert_key
    )

    return None

def put_alert_data ( alert ) :
    '''
    Put alert data in S3.
    '''

    alert_id = alert. get ( 'id' )
    alert_key = '/' . join ( [ 'alerts' ,
                          alert_id [ 0 : 2 ] ,
                          alert_id [ 2 : 4 ] ,
                          alert_id
                          ] )

    if TS_AWS_S3_PREFIX:
        alert_key = '/' . join ( [ TS_AWS_S3_PREFIX , alert_key ] )

    s3_client = boto3. client ( 's3' )
    s3_client. put_object (
        Body = json. dumps ( alert ) ,
        Bucket = TS_AWS_S3_BUCKET ,
        Key = alert_key
    )

    return None
Copy

我将介绍有趣的部分:



   
   
TS_AWS_S3_BUCKET = os . environ . get ( 'TS_AWS_S3_BUCKET' )
TS_AWS_S3_PREFIX = os . environ . get ( 'TS_AWS_S3_PREFIX' , None )
Copy

同样,此应用程序没有配置文件,但我需要设置一个S3存储桶名称和可选前缀。 我最终应该解决此问题-该设置记录在README.md中 ,到目前为止已经足够了。

函数put_webhook_data()put_alert_data()有很多重复的代码。 我没有重构它们,因为在重构之前更容易看到逻辑。 如果仔细观察,您会发现它们之间的唯一区别是如何定义alert_key 。 我将专注于put_webhook_data()



   
   
def put_webhook_data ( alert ) :
    '''
    Put alert webhook data in S3 bucket.
    '''

    alert_time = time . gmtime ( alert. get ( 'created_at' ) / 1000 )
    alert_time_path = time . strftime ( '%Y/%m/%d/%H/%M' , alert_time )
    alert_key = '/' . join ( [ 'webhooks' , alert_time_path , alert. get ( 'id' ) ] )
    if TS_AWS_S3_PREFIX:
        alert_key = '/' . join ( [ TS_AWS_S3_PREFIX , alert_key ] )

    s3_client = boto3. client ( 's3' )
    s3_client. put_object (
        Body = json. dumps ( alert ) ,
        Bucket = TS_AWS_S3_BUCKET ,
        Key = alert_key
    )

    return None
Copy

该函数接受一个名为alert的参数。 回顾app / views / s3.py警报只是发送到端点的JSON数据。 Webhook数据按日期和时间存储在S3中。 发生在2017-01-17 13:51的警报587c0159a907346eccb84004作为webhooks / 2017/01/17/13/51 / 587c0159a907346eccb84004存储在S3中。

我从获取警报时间开始。 自Unix时代以来,Threat Stack已发送警报时间(以毫秒为单位),并且需要将其转换为秒,这是Python处理时间的方式。 我花时间将其解析为将成为目录路径的字符串。 然后,我加入存储Webhook数据的顶级目录,基于时间的路径,最后是警报ID,以形成S3中Webhook数据的路径。

Boto 3是Python中用于处理AWS资源的主要模块。 我初始化了一个boto3客户对象,以便可以与S3交谈并将对象放在那里。 s3_client.put_object()BucketKey参数非常简单,它们是S3存储桶的名称以及我要存储的S3对象的路径。 Body参数是将我的警报转换回字符串。

结语

我现在拥有的是一个功能强大的Python Flask Web服务,它可以接收Threat Stack Webhook请求,获取警报的详细信息,并将其存档在S3中。 这是一个很好的开始,但是要准备生产还需要做更多的工作。 您可能会立即问:“如果出现问题怎么办?” 没有异常处理可处理诸如Threat Stack或S3通讯失败之类的问题。 我故意省略了它以保持代码清晰。 也没有授权密钥检查。 这意味着任何人都可以向其发送数据。 (并且由于我不执行任何错误检查或异常处理,所以它们可能会使服务崩溃。)此外,也没有TLS加密处理。 那是我留给Nginx或Apache使用的东西,它们将是应用程序前面的Web服务器。 所有这些以及更多这些都是在将此Web服务投入生产之前需要解决的问题。 但是现在这是一个开始,应该可以帮助您在开始构建自己的服务时变得更加舒适。

资源资源

查看GitHub存储库以获取Threat Stack to S3服务

因为该应用程序要经过修订,所以请查看本文中使用的版本

查看Tom关于Python Flask中异常处理的新教程。

本文最初出现在Threat Stack博客上 经许可重新发布。

翻译自: https://opensource.com/article/17/3/writing-web-service-using-python-flask

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值