租户隔离 一个客户就是一个租户,每个租户的数据在数据表中都有个一个tenantid字段用来与其他租户隔离
1 2 3 4 5 6 7 8 9 10 11 12 from flask import g, request def get_tenant_from_request () : auth = validate_auth(request.headers.get('Authorization' )) return Tenant.query.get(auth.tenant_id) def get_current_tenant () : rv = getattr(g, 'current_tenant' , None ) if rv is = None : rv = get_tenant_from_request() g.current_tenant = rv return rv
例如,每个租户有自己的Project,像下面这样批量直接修改project又忘记带上tenantid查询字段,就会把其他租户的project一并修改了
1 2 3 4 5 6 7 def batch_update_projects (ids, changes) : projects = Project.query.filter( Project.id.in_(ids) & Project.status != ProjectStatus.INVISIBLE ) for project in projects: update_projects(project, changes)
我们可以override Project的query属性以及使用sqlalchemy的event listener来自动的进行租户隔离,也就是说我们在使用orm查询的时候,会自动带上tenentid
查询字段,这样无论是修改、删除、查询都只会是操作自己租户下面的数据,毕竟在orm中,删除和修改都是要先查询的嘛
1 2 3 4 5 6 7 class Project (TenantBoundMixin, db.Model) : id = db.Column(db.Integer, primary_key=True ) name = db.Column(db.String(100 )) status = db.Column(db.Integer) def __repr__ (self) : return '<Project name=%r>' % self.name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from sqlalchemy.ext.declarative import declared_attr class TenantQuery (db.Query) : current_tenant_constrained = True def tenant_unconstrained_unsafe (self) : rv = self._clone() rv.current_tenant_constrained = False return rv class TenantBoundMixin (object) : query_class = TenantQuery @declared_attr def tenant_id (cls) : return db.Column(db.Integer, db.ForeignKey('tenant.id' )) @declared_attr def tenant (cls) : return db.relationship(Tenant, uselist=False ) @db.event.listens_for(TenantQuery, 'before_compile', retval=True) def ensure_tenant_constrained (query) : for desc in query.column_descriptions: if hasattr(desc['type' ], 'tenant' ) and \ query.current_tenant_constrained: query = query.filter_by(tenant=get_current_tenant()) return query
接下来我们来看一下使用
1 2 3 4 5 6 >>> Project.query.all()[<Project name='project42' >] >>> Project.query.tenant_unconstrained_unsafe().all()[<Project name='project1' >, Project.name='project2' , ...]
审计日志 1 2 3 4 5 6 7 8 9 10 11 12 13 def log (action, message=None) : data = { 'action' : action 'timestamp' : datetime.utcnow() } if message is not None : data['message' ] = message if request: data['ip' ] = request.remote_addr user = get_current_user() if user is not None : data['user' ] = User db.session.add(LogMessage(**data))
更多详细信息,请参考
flask多租户实践