Spring Boot R2DBC如何在每次查询前设置数据库会话变量?
在R2DBC Spring Boot应用中自动设置会话变量实现行级安全
核心思路
无需修改现有业务代码,通过R2DBC连接装饰器拦截连接创建流程,自动前置会话变量设置SQL;同时借助Reactor Context传递Web会话中的行级安全参数,全程保持非阻塞特性。
实现步骤
步骤1:将Web会话参数存入Reactor Context
在Web过滤器中,提取Web会话里的行级安全参数(比如当前用户ID),存入Reactor Context,确保后续R2DBC操作能异步获取该值:@Component public class RowSecurityContextFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { Mono<String> userIdMono = exchange.getSession() .map(session -> session.getAttribute("userId")) .cast(String.class) .defaultIfEmpty("anonymous"); return chain.filter(exchange) .contextWrite(ctx -> userIdMono.map(userId -> ctx.put("ROW_SECURITY_USER_ID", userId)) .blockOptional() .orElse(ctx)); } }步骤2:装饰ConnectionFactory自动执行SET语句
实现ConnectionFactoryDecorator接口,拦截连接创建过程,每次获取到新连接后,先执行会话变量设置的参数化SQL,再将连接返回给业务逻辑使用:@Component public class RowSecurityConnectionDecorator implements ConnectionFactoryDecorator { @Override public ConnectionFactory decorate(ConnectionFactory originalFactory) { return new ConnectionFactory() { @Override public Publisher<? extends Connection> create() { return originalFactory.create() .flatMap(connection -> Mono.deferContextual(contextView -> { String userId = contextView.getOrDefault("ROW_SECURITY_USER_ID", "anonymous"); // 使用参数化查询避免SQL注入风险 return connection.createStatement("SET SESSION row_security_user_id = $1") .bind(0, userId) .execute() .then(Mono.just(connection)); })); } @Override public ConnectionFactoryMetadata getMetadata() { return originalFactory.getMetadata(); } }; } }步骤3:确保装饰器生效
Spring Boot会自动扫描并应用ConnectionFactoryDecorator类型的Bean,无需额外配置。如果是自定义配置场景,也可以手动声明Bean:@Configuration public class R2dbcConfig { @Bean public ConnectionFactoryDecorator rowSecurityConnectionDecorator() { return new RowSecurityConnectionDecorator(); } }
关键注意事项
- SQL注入防护:必须使用参数化绑定(如示例中的
$1和bind(0, userId))生成SET语句,绝对禁止字符串拼接,避免恶意参数注入。 - 上下文传递有效性:业务层的所有R2DBC操作(仓库返回的Mono/Flux)必须保持在原始Reactor Context中,禁止使用
block()等阻塞操作破坏上下文传递链。 - 连接复用处理:若使用连接池,每次获取连接时都会重新执行SET语句,覆盖连接复用前的旧变量值,确保行级安全参数始终对应当前请求的用户。
替代方案:DatabaseClient拦截器
如果仅需拦截特定DatabaseClient的查询,可自定义执行器,在每次查询前自动插入SET语句:
@Bean public DatabaseClient secureDatabaseClient(ConnectionFactory connectionFactory) { return DatabaseClient.builder() .connectionFactory(connectionFactory) .executeFunction((conn, stmt) -> Mono.deferContextual(ctxView -> { String userId = ctxView.getOrDefault("ROW_SECURITY_USER_ID", "anonymous"); return conn.createStatement("SET SESSION row_security_user_id = $1") .bind(0, userId) .execute() .then(stmt.execute()); })) .build(); }
业务代码中使用此secureDatabaseClient执行的所有查询,都会自动前置会话变量设置逻辑。
内容的提问来源于stack exchange,提问作者kylie.zoltan
相关产品推荐
相关产品推荐

