python-flask 技能点使用-03 请求钩子实现审计日志

  • 场景分析

         使用python flask开发web系统,该系统是基于用户认证鉴权的web系统, 系统中涉及到关键数据的操作,因此需要针对业务操作进行记录(也就是审计日志),便于管理员后期查看,在基于java的Spring系列框架中我们可以借助于AOP面向切面的编程来完成,在使用Flask时可以借助于请求钩子  @app.before_request +  @app.after_request 来实现,关键难点在于如何保证记录的是同一个请求URL(一个是请求前预记录,一个是请求后根据返回码进行补充)

  • 代码实现
    •  如何实现预记录+修改补充,在预记录时生成一个唯一性UUID标记本次URL请求
      • 尝试一: 使用request.headers中扩充携带UUID,失败,报错说headers是不可变的
      • 尝试二: 使用request.session中扩充携带UUID,与上面类似
      • 尝试三: 通过HashMap携带,在后期分布式应用中不一定可靠
      • 尝试四: 通过请参数中扩展,存在的问题 如何在PUT、POST、GET、DELETE不同请求方式中实现
    • 定义数据库实体类
# 业务审计日志表
class BusinessAuditLog(db.Model):
    __tablename__ = 'business_aduit_record'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    uuid = db.Column(db.String(50))
    url = db.Column(db.String(50))
    req_params = db.Column(db.String(5000))
    req_method = db.Column(db.String(10))
    is_succ = db.Column(db.String(10))
    cost_time = db.Column(db.Integer)
    resp_code = db.Column(db.String(10))
    resp_data = db.Column(db.Text(5000))
    resp_msg = db.Column(db.String(500))
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    start_time = db.Column(db.DateTime(50), default=datetime.now)
    end_time = db.Column(db.DateTime(50), default=datetime.now)


    def __int__(self, url, req_params, req_method, user_id, start_time, uuid):
        self.url = url
        self.req_params = req_params
        self.req_method = req_method
        self.user_id = user_id
        self.start_time = start_time
        self.uuid = uuid
    •  请求前预记录(携带token校验代码)
# 不需要验证的接口地址
no_use_auth_urls = ["/admin/user/logout", "/admin/user/login", "/admin/user/register", "/admin/user/code",
                    "/admin/menu/import", "/admin/menu/tree", "/admin/menu/tree/i18n", "/favicon.ico"]


# 在每一次请求之前调用,这时候已经有了请求,可以在这个方法里面做请求的校验
# 如果请求的校验不成功,可以直接在此方法中进行响应,直接return之后那么就不会执行视图函数了 目前必须在这个文件中才会执行
@app.before_request
def before_request():
    url = request.url
    base_url = request.base_url
    root_url = request.root_url
    host_url = request.host_url
    full_path = request.full_path
    f_path = full_path.split("?")
    bus_url = str(f_path[0])
    print("====================== ", bus_url)
    is_static = bus_url.startswith("/static")
    if bus_url not in no_use_auth_urls and is_static == False:
        print('before_request bus_url : ', bus_url)
        token = request.headers.get('Authorization')
        user_id = UserToken.get_user_id_from_token(token)
        if token is None:
            return JsonResponse.fail(9999, 'request headers missing Authorization')
        if not RedisUtil.validate_token(token):
            return JsonResponse.fail(9401,
                                     'request headers missing Authorization or Authorization is expired,need to login')
        # 对于系统相关业务的GET请求不记录
        method = request.method
        if bus_url.startswith("/admin/") and method == 'GET':
            return None
        if is_static:
            return None
        req_uuid = str(uuid.uuid4())
        req_data = str(request.data, encoding='utf-8')
        start_time = time.time()
        start_time_hm = int(round(start_time * 1000))
     # GET/DELETE请求方式中存在的情况 if req_data is None or CommonTool.is_none(req_data) or req_data == '{}': req_data = "{\"REQ_UUID\": \"" + req_uuid + "\",\"START_TIME\": " + str(start_time_hm) + "}" else: req_data_dict = json.loads(req_data) req_data_dict['REQ_UUID'] = req_uuid req_data_dict['START_TIME'] = str(start_time_hm) req_data = json.dumps(req_data_dict) req_data_b = req_data.encode(encoding='utf-8') request.data = req_data_b req_params = None if method == 'DELETE' or method == 'GET': req_params = request.args.to_dict().__str__() else: req_params = request.get_data(as_text=True) db.session.add(BusinessAuditLog(url=bus_url, req_params=req_params, req_method=method, user_id=user_id, start_time=datetime.now(), uuid=req_uuid)) db.session.commit() return None
    • 请求后补充更新 
# 在执行完视图函数之后调用,并且会把视图函数生成的响应传入,故其函数需要有一个response参数,可以在此方法中对响应进行进一步处理
@app.after_request
def after_request(response):
    full_path = request.full_path
    f_path = full_path.split("?")
    bus_url = str(f_path[0])
    is_static = bus_url.startswith("/static")
    if is_static or bus_url in no_use_auth_urls:
        return response

    response.headers['Content-Type'] = 'application/json'
    res = response.json
    resp_code = int(res['code'])
    req_data = request.data

    if resp_code != 9401 and CommonTool.is_not_none(req_data) and req_data is not None:
        resp_msg = res['msg']
        resp_data = res['data']

        req_data = str(request.data, encoding='utf-8')
        req_data_json = json.loads(req_data)
        req_uuid = req_data_json['REQ_UUID']
        start_time_hm = req_data_json['START_TIME']

        end_time = time.time()
        end_time_hm = int(round(end_time * 1000))

        method = request.method
        cost_time = end_time_hm - int(start_time_hm)
        is_succ = None
        if resp_code == 200:
            is_succ = "Y"
        else:
            is_succ = "N"
        BusinessAuditLog.query.filter(BusinessAuditLog.uuid == req_uuid,
                                      BusinessAuditLog.req_method == method,
                                      BusinessAuditLog.url == bus_url
                                      ).update(
            {
                "end_time": datetime.now(),
                "cost_time": cost_time,
                "resp_code": resp_code,
                "resp_data": json.dumps(resp_data),
                "resp_msg": resp_msg,
                "is_succ": is_succ
            }
        )
        db.session.commit()
    return response

  

posted @ 2023-05-15 11:13  521pingguo1314  阅读(173)  评论(0)    收藏  举报