在遵循访问控制的情况下,为REST API序列化SQLAlchemy模型?

7 投票
1 回答
4259 浏览
提问于 2025-04-16 13:15

目前,我们以及大多数网页框架的序列化方式是通过某种方法调用,将模型转储成某种格式。在我们的情况下,每个模型都有一个 to_dict() 方法,它会构建并返回一个键值对字典,键是字段名,值是实例变量。

在我们的代码中,有很多类似这样的片段: json.dumps(**some_model_object.to_dict()),这会将 some_model_object 序列化成json格式。最近,我们决定向用户开放一些内部资源,但这些资源中有些特定的私有实例值,如果请求的用户不是超级用户,我们不想在序列化时传输这些值。

我正在尝试设计一个更简洁的方案,以便更容易进行序列化,同时也能将数据序列化成除了json以外的格式。我认为这是一个很好的面向切面设计/编程的应用场景,在这里,切面会尊重请求的访问控制,并根据请求用户的权限来序列化对象。

这是我目前的一个类似方案:

from framework import current_request


class User(SQLAlchemyDeclarativeModel):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    first_name = Column(Unicode(255))
    last_name = Column(Unicode(255))
    private_token = Column(Unicode(4096))

    def to_dict(self):
        serialized = dict((column_name, getattr(self, column_name))
                          for column_name in self.__table__.c.keys())

        # current request might not be bound yet, could be in a unit test etc.
        if current_request and not current_request.user.is_superuser():
            # we explicitly define the allowed items because if we accidentally add
            # a private variable to the User table, then it might be exposed.
            allowed = ['id', 'first_name', 'last_name']
            serialized = dict((k, v) for k, v in serialized.iteritems() if k in allowed)

        return serialized

可以看出,这样并不理想,因为我必须将数据库模型与当前请求耦合在一起。虽然这样做是 非常 明确的,但请求耦合是一种代码异味,我正在尝试寻找更干净的解决方案。

我考虑过的一种方法是,在模型上注册一些字段,像这样:

class User(SQLAlchemyDeclarativeModel):
    __tablename__ = 'users'
    __public__ = ['id', 'first_name', 'last_name']
    __internal__ = User.__exposed__ + ['private_token']

    id = Column(Integer, primary_key=True)
    first_name = Column(Unicode(255))
    last_name = Column(Unicode(255))
    private_token = Column(Unicode(4096))

然后,我会有一个序列化类,在每次WSGI调用时与当前请求绑定,这个类会使用所需的序列化器。例如:

import simplejson

from framework import JSONSerializer  # json serialization strategy
from framework import serializer

# assume response format was requested as json
serializer.register_serializer(JSONSerializer(simplejson.dumps))
serializer.bind(current_request)

然后在我的视图中,我只需这样做:

from framework import Response

user = session.query(User).first()
return Response(code=200, serializer.serialize(user))

serialize 的实现如下:

def serialize(self, db_model_obj):
    attributes = '__public__'
    if self.current_request.user.is_superuser():
        attributes = '__private__'

    payload = dict((c, getattr(db_model_obj, c)) 
                   for c in getattr(db_model_obj, attributes))

    return self.serialization_strategy.execute(payload)

对于这种方法的可读性和清晰性有什么看法?这算是一个符合Python风格的解决方案吗?

提前感谢!

1 个回答

7

通过一个混合类来建立“序列化”的约定:

class Serializer(object):
    __public__ = None
    "Must be implemented by implementors"

    __internal__ = None
    "Must be implemented by implementors"

    def to_serializable_dict(self):
        # do stuff with __public__, __internal__
        # ...

保持简单,使用WSGI集成。只需“注册”JSONSerializer作为一个对象,其他的都是一些Java/Spring的东西,不需要那么复杂。下面是我在pylons 1.0中的解决方案,我还没有使用pyramid:

def my_controller(self):
   # ...

   return to_response(request, response, myobject)


# elsewhere
def to_response(req, resp, obj):
    # this would be more robust, look in
    # req, resp, catch key errors, whatever.
    # xxx_serialize are just functions.  don't need state
    serializer = {
       'application/json':json_serialize,
       'application/xml':xml_serialize,
       # ...
    }[req.headers['content-type']]

    return serializer(obj)

撰写回答