You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot如何在每次PostgreSQL连接前后设置/清除会话变量(含非事务查询)

解决PostgreSQL行级安全(RLS)在Spring Boot非事务查询中的会话变量自动设置问题

针对你的场景——Spring Boot结合Spring Data、PostgreSQL和HikariCP,需要在**所有数据库连接(包括非事务查询)**获取前设置user_id会话变量,归还时清除,以适配Employee表的行级安全策略,可通过以下方案实现:

核心思路

通过代理HikariCP的数据源和数据库连接,实现每次从连接池取出连接时自动注入当前用户的user_id,连接归还到池前清除该变量,确保无论事务内还是非事务的查询都能触发RLS规则。

具体实现

1. 代理数据库连接,处理会话变量的设置与清除

实现java.sql.Connection接口,包装原始连接,在构造时设置会话变量,重写close()方法在归还连接前清除变量:

import java.sql.*;
import your.package.UserService;

public class RlsAwareConnectionProxy implements Connection {

    private final Connection targetConnection;
    private final UserService userService;

    public RlsAwareConnectionProxy(Connection targetConnection, UserService userService) {
        this.targetConnection = targetConnection;
        this.userService = userService;
        setSessionUserId();
    }

    // 设置RLS会话变量,用PreparedStatement避免SQL注入
    private void setSessionUserId() {
        Integer userId = userService.getCurrentUserId();
        if (userId == null) {
            return;
        }
        try (PreparedStatement stmt = targetConnection.prepareStatement("SET user_id = ?::integer")) {
            stmt.setInt(1, userId);
            stmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("Failed to set RLS session variable 'user_id'", e);
        }
    }

    // 清除会话变量,出错仅记录不抛出,避免影响连接归还
    private void clearSessionUserId() {
        try (Statement stmt = targetConnection.createStatement()) {
            stmt.executeUpdate("RESET user_id");
        } catch (SQLException e) {
            // 替换为你的日志组件,如log.error("Failed to clear RLS session variable", e);
            System.err.println("Failed to clear RLS session variable: " + e.getMessage());
        }
    }

    @Override
    public void close() throws SQLException {
        clearSessionUserId();
        targetConnection.close();
    }

    // 以下为Connection接口所有方法的委托实现,直接调用原始连接的对应方法
    @Override
    public Statement createStatement() throws SQLException {
        return targetConnection.createStatement();
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return targetConnection.prepareStatement(sql);
    }

    @Override
    public CallableStatement prepareCall(String sql) throws SQLException {
        return targetConnection.prepareCall(sql);
    }

    @Override
    public String nativeSQL(String sql) throws SQLException {
        return targetConnection.nativeSQL(sql);
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {
        targetConnection.setAutoCommit(autoCommit);
    }

    @Override
    public boolean getAutoCommit() throws SQLException {
        return targetConnection.getAutoCommit();
    }

    @Override
    public void commit() throws SQLException {
        targetConnection.commit();
    }

    @Override
    public void rollback() throws SQLException {
        targetConnection.rollback();
    }

    @Override
    public boolean isClosed() throws SQLException {
        return targetConnection.isClosed();
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return targetConnection.getMetaData();
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {
        targetConnection.setReadOnly(readOnly);
    }

    @Override
    public boolean isReadOnly() throws SQLException {
        return targetConnection.isReadOnly();
    }

    @Override
    public void setCatalog(String catalog) throws SQLException {
        targetConnection.setCatalog(catalog);
    }

    @Override
    public String getCatalog() throws SQLException {
        return targetConnection.getCatalog();
    }

    @Override
    public void setTransactionIsolation(int level) throws SQLException {
        targetConnection.setTransactionIsolation(level);
    }

    @Override
    public int getTransactionIsolation() throws SQLException {
        return targetConnection.getTransactionIsolation();
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return targetConnection.getWarnings();
    }

    @Override
    public void clearWarnings() throws SQLException {
        targetConnection.clearWarnings();
    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        return targetConnection.createStatement(resultSetType, resultSetConcurrency);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return targetConnection.prepareStatement(sql, resultSetType, resultSetConcurrency);
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return targetConnection.prepareCall(sql, resultSetType, resultSetConcurrency);
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return targetConnection.getTypeMap();
    }

    @Override
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
        targetConnection.setTypeMap(map);
    }

    @Override
    public void setHoldability(int holdability) throws SQLException {
        targetConnection.setHoldability(holdability);
    }

    @Override
    public int getHoldability() throws SQLException {
        return targetConnection.getHoldability();
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        return targetConnection.setSavepoint();
    }

    @Override
    public Savepoint setSavepoint(String name) throws SQLException {
        return targetConnection.setSavepoint(name);
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {
        targetConnection.rollback(savepoint);
    }

    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
        targetConnection.releaseSavepoint(savepoint);
    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return targetConnection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return targetConnection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return targetConnection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        return targetConnection.prepareStatement(sql, autoGeneratedKeys);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
        return targetConnection.prepareStatement(sql, columnIndexes);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        return targetConnection.prepareStatement(sql, columnNames);
    }

    @Override
    public Clob createClob() throws SQLException {
        return targetConnection.createClob();
    }

    @Override
    public Blob createBlob() throws SQLException {
        return targetConnection.createBlob();
    }

    @Override
    public NClob createNClob() throws SQLException {
        return targetConnection.createNClob();
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        return targetConnection.createSQLXML();
    }

    @Override
    public boolean isValid(int timeout) throws SQLException {
        return targetConnection.isValid(timeout);
    }

    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException {
        targetConnection.setClientInfo(name, value);
    }

    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException {
        targetConnection.setClientInfo(properties);
    }

    @Override
    public String getClientInfo(String name) throws SQLException {
        return targetConnection.getClientInfo(name);
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        return targetConnection.getClientInfo();
    }

    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
        return targetConnection.createArrayOf(typeName, elements);
    }

    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
        return targetConnection.createStruct(typeName, attributes);
    }

    @Override
    public void setSchema(String schema) throws SQLException {
        targetConnection.setSchema(schema);
    }

    @Override
    public String getSchema() throws SQLException {
        return targetConnection.getSchema();
    }

    @Override
    public void abort(Executor executor) throws SQLException {
        targetConnection.abort(executor);
    }

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
        targetConnection.setNetworkTimeout(executor, milliseconds);
    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        return targetConnection.getNetworkTimeout();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return targetConnection.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return targetConnection.isWrapperFor(iface);
    }
}

2. 代理Hikari数据源,返回包装后的连接

创建代理数据源类,注入原始Hikari数据源和UserService,每次获取连接时返回上面定义的代理连接:

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import your.package.UserService;

@Component
public class RlsAwareDataSourceProxy implements DataSource {

    private final DataSource hikariDataSource;
    private final UserService userService;

    public RlsAwareDataSourceProxy(
            @Qualifier("dataSource") DataSource hikariDataSource,
            UserService userService) {
        this.hikariDataSource = hikariDataSource;
        this.userService = userService;
    }

    @Override
    public Connection getConnection() throws SQLException {
        Connection originalConn = hikariDataSource.getConnection();
        return new RlsAwareConnectionProxy(originalConn, userService);
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        // HikariCP已配置全局用户名密码,直接复用getConnection()逻辑
        return getConnection();
    }

    // 以下为DataSource接口方法的委托实现,直接调用原始数据源的对应方法
    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return hikariDataSource.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return hikariDataSource.isWrapperFor(iface);
    }

    @Override
    public java.io.PrintWriter getLogWriter() throws SQLException {
        return hikariDataSource.getLogWriter();
    }

    @Override
    public void setLogWriter(java.io.PrintWriter out) throws SQLException {
        hikariDataSource.setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        hikariDataSource.setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return hikariDataSource.getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return hikariDataSource.getParentLogger();
    }
}

3. 配置代理数据源为默认数据源

在配置类中标记代理数据源为@Primary,确保Spring Data的Repository使用该数据源:

import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import your.package.UserService;

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource primaryDataSource(
            @Qualifier("dataSource") DataSource hikariDataSource,
            UserService userService) {
        return new RlsAwareDataSourceProxy(hikariDataSource, userService);
    }
}

关键注意事项

  • 用户ID传递:UserService.getCurrentUserId()需通过ThreadLocal存储当前请求的用户ID(如登录后将用户ID存入ThreadLocal),确保在获取连接时能拿到正确值;异步任务需配合TaskDecorator传递ThreadLocal上下文。
  • SQL注入防护:使用PreparedStatement设置会话变量,避免字符串拼接带来的注入风险。
  • 异常处理:清除会话变量时若出错,仅记录日志不抛出异常,防止连接无法正常归还到池。
  • 连接复用:该方案适配HikariCP的连接复用机制,每次取出连接都会重新设置当前用户的user_id,不会出现用户数据串扰。

内容的提问来源于stack exchange,提问作者hido

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 01:13:12