更好的对象转字典

一. 方便但不完美的__dict__

对象转字典用到的方法为__dict__. 比如对象对象a的属性a.name='wk', a.age=18, 那么如果直接将使用a.__dict__获得对应的字典的值为: {name: 'wk', aget:18}, 很方便, 但是也存在一些限制. 其不完美之处在于:

比如:

class A(object):
    name = 'wukt'
    age = 18

    def __init__(self):
        self.gender = 'male'
a = A()
print(a.__dict__)

此时的打印结果是:
{gender: 'male'}
但是类变量name和age无法一同转换.

二. 使用dict

使用dict的方式如下, 如果直接使用会报错.

a = A()
dict(a)
  1. 使用dict之时, 将自动调用类中的keys方法, keys中定义了字典的键, 调用keys方法后, 程序将依照字典取值的方式尝试获得这些键对应的值.
  2. 当使用如字典的取值方式时: 比如a['name'], 将会调用类中的__getitem__方法, __getitem__方法决定了这个值是多少.

因此只需要在一例中添加两个方法就可以使对象可以通过dict转字典:

def keys(self):
    return ('name', 'age' )

def __getitem__(self, item):
    return getattr(self, item)

通过这种方式, 既可以支持类变量的转换, 又可以自定义需要转换的字段.

完整代码:

class A(object):
    name = 'wukt'
    age = 18

    def __init__(self):
        self.gender = 'male'

    def keys(self):
        '''当对实例化对象使用dict(obj)的时候, 会调用这个方法,这里定义了字典的键, 其对应的值将以obj['name']的形式取,
        但是对象是不可以以这种方式取值的, 为了支持这种取值, 可以为类增加一个方法'''
        return ('name', 'age', 'gender')

    def __getitem__(self, item):
        '''内置方法, 当使用obj['name']的形式的时候, 将调用这个方法, 这里返回的结果就是值'''
        return getattr(self, item)

a = A()
r = dict(a)
print(r)

flask: 內置HttpException的rest风格改进

在api的设计中, 无论异常还是正常数据均需要服务器以json的格式返回, 为了对异常的统一管理, 同时为了后续更加方便的返回和验证数据, 我们自定义异常返回类.
设计异常数据的返回格式为:

{
    "error_code": 999,
    "msg": "sorry, we make a mistake",
    "request": "POST /v1/client/register"
}

异常值分别代表:

999  未知错误
1006  客户端错误
1007  服务器错误
1000 参数错误
...

其中json中的request包含了请求的方法和路径.


一. 定义一个最基本的类

二. 参数错误和客户端错误类

三. 所有的异常


一. 定义一个最基本的类

为了使代码简洁, 首先定义一个最基本的类, 供其它类继承, 这个自定义的APIException继承HTTPException.
1. 为了返回特定的body信息, 需要重写get_body;
2. 为了指定返回类型, 需要重写get_headers.
3. 为了接收自定义的参数, 重写了__init__;
4. 同时定义了类变量作为几个默认参数.(code500和error_code:999 均表示未知错误, error_code表示自定义异常code)

from flask import request, json
from werkzeug.exceptions import HTTPException


class ApiException(HTTPException):
    code = 500
    msg = 'sorry, we make a mistake'
    error_code = 999

    def __init__(self, code=None, msg=None, error_code=None, header=None):
        if code:
            self.code = code
        if msg:
            self.msg = msg
        if error_code:
            self.error_code = error_code
        super(ApiException, self).__init__(msg, None)

    def get_body(self, environ=None):
        body = dict(
            msg=self.msg,
            error_code=self.error_code,
            # 形如request="POST v1/client/register"
            request=request.method + ' ' + self.get_url_no_param()
        )
        text = json.dumps(body)
        return text

    def get_headers(self, environ=None):
        return [('Content-Type', 'application/json')]

    @staticmethod
    def get_url_no_param():
        full_url = str(request.full_path)
        main_path = full_url.split('?')
        return main_path[0]

二. 参数错误和客户端错误类

这两个类继承ApiException, 只是重写了三个默认的类变量.


class ClientTypeError(ApiException): code = 400 msg = 'client is invalid' error_code = 1006 # 参数错误 class ParameterException(ApiException): code = 400 msg = 'invalid parameter' error_code = 1000

一切定义完毕!
那么该怎么如何抛出异常呢?

# 抛出参数异常
raise ParameterException(msg=self.errors)

当然也可以只捕捉异常.

三. 所有的异常

至此我们无法将一些特殊的异常返回为json格式, 比如客户端提交的数据不符合规范等 在程序中, 我们会对所有的已知异常跑出一个ApiException的异常.但是无法对未知的异常琢一处理, 因此我们可以使用AOP的思想, 在程序的出口统一处理, 直接在入口函数中编写一个函数

# 为了捕捉所有的异常, 我们需要绑定异常的基类 Exception, Flask>1.0
@app.errorhandler(Exception)
def framework_error(e):
    # ApiExcetion
    # HttpException
    # Exception
    if isinstance(e, ApiException):
        return e
    if isinstance(e, HTTPException):
        code = e.code
        msg = e.description
        error_code = 1007
        return ApiException(code=code, msg=msg, error_code=error_code)
    else:
        if not app.config['DEBUG']:
            return ApiException()
        raise e

此时便可以捕捉所有异常了.

当然在一些正常的返回中也可以使用这个ApiException. 比如:

class Success(ApiException):
    code = 200
    msg = 'success'
    error_code = 0

@api.route('/register', methods=['POST'])
def create_client():
    #...
    return Success()

JWT vs Session

一. session

1.1 session对于服务器的开销

在传统的用户登录认证中,都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大

1.2 session对于服务扩展性的限制

用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

1.3 session+cookie认证方式存在的风险

CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

二. jwt(json web token)

jwt的认证方式只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。

2.1 jwt生成的流程

  • 客户端使用用户名和密码请求服务器
  • 服务器验证后返回一个jwt
  • 客户端存储jwt, 每次请求都会携带这个jwt
  • 服务器验证jwt, 并返回数据

2.2 jwt的构成:

jwt有三部分构成: header, payload, signature

2.2.1 header(头部)

jwt头部中承载了两个信息, 完整的头部类似与:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

typ: 类型
alg: 加密算法, 通常为HMAC SHA256

2.2.2 payload(载荷)

载荷就是存放有效信息的地方, 这些有效信息, 包含三个部分:
– 标准中注册的声明
– 公共的声明
– 私有的声明

标准中注册的声明 (建议但不强制使用):
  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明:

公共的声明可以任意添加信息, 但是不可以包括敏跟信息, 因为可以被解密.
私有的声明:

私有的声明:

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

payload的样式形如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

2.2.3 signature(签名)

签名由base64加密后的header和加密后的payload以’.’链接, 然后顺纯色header中声明的加密方式进行加盐secret后加密.

2.3 如何使用jwt:

客户端根据接口文档, 一般讲jwt加到请求头内, 比如Authorization中或者自定义字段x-access-token等方式.后台程序取出对应部分的信息进行验证即可.

生成过程举例:

if check_password_hash(user.password, auth.password):
        payload = {
            'public_id': user.public_id,
            'exp': datetime.now() + timedelta(minutes=10)
        }
        token = jwt.encode(payload, app.config['SECRET_KEY'])
        return jsonify({'token': token.decode('utf8')})
    return make_response(
                'Could not verify', 401,
                {'WWW-Authencate': 'Basic realm = "Login Required!"'})

验证装饰器举例

客户端请求时将jwt放置在请求头的x-access-token字段中:


def token_required(func): # decoration @wraps(func) def decorated(*args, **kwargs): token = None if 'x-access-token' in request.headers: token = request.headers['x-access-token'] if not token: return jsonify({'message': 'Token is missing!'}), 401 try: data = jwt.decode(token, app.config['SECRET_KEY']) corrent_user = User.query.filter_by(public_id=data['public_id']).first() except: return jsonify({'message': 'Token is invalid!'}) return func(corrent_user, *args, **kwargs) return decorated

flask工作原理图

本文是初学时根据慕课网视频flask高级编程(鱼书)第五章, 第六章边学习边整理, 如有错误请指正, 谢谢.


  • 整体过程

  一个请求进入flask框架后, flask会首先实例化一个Request Context封装了这次请求的相关信息(Request), 然后将请求上下文推入栈_request_ctx_stack(这是LocalStack的一个实例).

  在RequestContext对象入栈之前会检查App Context对应栈栈顶的元素, 如果不是当前的app, 则会先将app推入. 因此如果在一个请求中使用(注意是在请求中)使用current_app是不需要手动push的.

  current_app取得是_app_ctckx_stack 的栈顶元素中的app属性, 这个属性就是我们自己创建的app=Flask(__name__), 如果栈顶为空,则提示unbound, 同样的request指的是_request_ctx_stack的栈顶对应对象, 当一个请求结束的时候会出栈.

  Local 与 LocalStack: werkzeug, LocalStack作为线程隔离对象栈的基本特性

其他

  • 手动压栈和出栈

如果要在没有请求的情况下使用核心对象需要手动push和pop, 一个例子

ctx = app.app_context()
ctx.push() # 入栈, push是LocalStack中的方法
# 其他语句
ctx.pop()  # 出栈

以上代码等同于:

with app.app_context():
    # 其他语句

比如在异步发送邮件的时候:

from flask import render_template, current_app
from flask_mail import Message
from threading import Thread
from app.ext import mail

def async_send_mail(app, msg):
    #获取当前程序的上下文
    with app.app_context():
        mail.send(message=msg)


def send_mail(subject, to, tem, **kwargs):
    app = current_app._get_current_object()
    msg = Message(
        subject=subject, recipients=[to], sender=app.config['MAIL_USERNAME'])
    msg.html = render_template('email/' + tem + '.html', **kwargs)
    send = Thread(target=async_send_mail, args=(app, msg))
    send.start()


  • 值得说明

Local使用字典的方式实现的线程隔离,
localStack封装了Local对象, 把Local对象作为自己的属性从而实现了线程隔离的栈结构
_request_ctx_stack和_app_ctx_stack 都是LocalStack的实例(在flask源码的globals.py末尾可以找到)

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)

Flask: 核心对象, 承载了各种功能, 比如配置信息,注册路由的信息等
AppContext: 对核心对象的封装, 同时附加了额外的参数
Request: 请求信息, 比如url相关的参数
RequestContext: 对Request对象的封装

  • 参考资料
  1. Flask 的 Context 机制

  2. flask高级编程(鱼书)第五章, 第六章

带有参数的装饰器

1. 函数带多个参数

# 普通的装饰器, 打印函数的运行时间
def decrator(func):
    def wrap(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        end_time = time.time()
        print('运行时间为', end_time-start_time)
        return res
    return wrap

2. 装饰器带有多个参数

当装饰器带有多个参数的时候, 装饰器函数就需要多加一层嵌套:

比如:

def decrator(*dargs, **dkargs):
    def wrapper(func):
        def _wrapper(*args, **kargs):
            print ("装饰器参数:", dargs, dkargs)
            print ("函数参数:", args, kargs)
            return func(*args, **kargs)
        return _wrapper
    return wrapper

为什么被装饰函数体可以传入内层呢?

装饰器函数有多个参数, 需要以@decrator(1, a=2)的方式使用, 这时候decrator是已经执行的(因为加了括号), 可以粗略的理解为加载被装饰函数的上的是wrapper, 所以这和普通的装饰器并无差别.

  • 又如flask源码中的:
   def route(self, rule, **options):
        """Like :meth:`Flask.route` but for a blueprint.  The endpoint for the
        :func:`url_for` function is prefixed with the name of the blueprint.
        """
        def decorator(f):
            endpoint = options.pop("endpoint", f.__name__)
            self.add_url_rule(rule, endpoint, f, **options)
            return f
        return decorator

flask的蓝图route源码中的装饰器, 最内层直接返回return f 并没有多加一层处理的函数, 在无需对被装饰函数进行过多处理的时候这是较为方便的做法. route源码中只是对装饰器参数进行了处理.