Springboot项目使用SSH转发端口访问mysql服务器

1.环境参数

<parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.7.1</version>
      <relativePath/> 
</parent>
<dependency>
       <groupId>com.jcraft</groupId>
       <artifactId>jsch</artifactId>
       <version>0.1.53</version>
</dependency>
<dependency>
       <groupId>com.baomidou</groupId>
       <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
       <version>3.5.1</version>
</dependency>
<dependency>
       <groupId>com.baomidou</groupId>
       <artifactId>mybatis-plus-boot-starter</artifactId>
       <version>3.5.2</version>
 </dependency>

项目使用mybaits的多数据源dynamic datasource包,采用yml配置的形式,配合@DS("xxx")注解实现多数据源切换(因为数据源是确定的,故没有采用数据源配置到数据库动态加载的形式)

yml配置如下:

spring:
  datasource:
    dynamic:
      primary: master
      strict: false
      datasource:
        master:
          url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/db1?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
          username: xxxx
          password: xxxxxxx
          driver-class-name: com.mysql.cj.jdbc.Driver
          hikari:
            connection-test-query: SELECT 1
        slave:
          url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/db2?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai
          username: xxxxxx
          password: xxxxxxx
          driver-class-name: com.mysql.cj.jdbc.Driver
          hikari:
            connection-test-query: select 1 from dual

业务需求是增加两个数据源,是同一个mysql数据源下的两个数据库。但是这个mysql数据库需要使用ssh转发的形式才能访问,这里采用JSCH这个包针对指定端口进行转发,其他端口不转发。

1.新建SSHConfiguration类

@Component
@Slf4j
public class SSHConfiguration {
    // 需要ssh连接到的机器地址和账号密码
    private final static String SSH_REMOTE_SERVER = "xxx.xxx.xxx.xxx";
    private final static int SSH_REMOTE_PORT = 22;
    private final static String SSH_USER = "xxxx";
    private final static String SSH_PASSWORD = "xxxxxx";
    // 需要最终代理到的数据库地址(直连无法访问,必须ssh连上目标机器后才能访问)
    private final static String MYSQL_REMOTE_SERVER = "xxx.xxx.xxx.xxx";
    private final static int REMOTE_PORT = 3306;
	// 需要代理的端口,项目启动后,走该端口的请求都会代理到ssh机器上
    public static int PROXY_PORT = 0;
    // ssh连接会话
    private Session sesion; 

	 // 连接ssh机器
    public SSHConnection() throws Throwable {
        JSch jsch = new JSch();
        sesion = jsch.getSession(SSH_USER, SSH_REMOTE_SERVER, SSH_REMOTE_PORT);
        sesion.setPassword(SSH_PASSWORD);
        Properties config = new Properties();
        // 设置为no即为无脑接受ssh机器的key,其实是一种不安全的做法,根据实际情况做选择
        config.put("StrictHostKeyChecking", "no");
        sesion.setConfig(config);
        sesion.connect(); 

        // 切记这里要这样写,端口写0目的是让运行环境的机器自主分配代理的端口,如果这里写死可能会引发端口占用的问题,让机器自己选择空闲的端口是更好的选择,坏处就是数据库连接地址无法配置到yml文件里,因为接口是动态的
        PROXY_PORT = sesion.setPortForwardingL(0, MYSQL_REMOTE_SERVER, REMOTE_PORT);
       // 像下面这种写法就可能会导致端口绑定错误 
       // session.setPortForwardingL("localhost",REMOTE_PORT,MYSQL_REMOTE_SERVER,REMOTE_PORT);
       log.info("SSH代理连接成功!");
    }

    public void closesSH() {
        sesion.disconnect();
    }
}

2.配置动态数据库

使用mybatis-plus自带的加载动态数据源的方式,参考文档地址:https://www.kancloud.cn/tracy5546/dynamic-datasource/2268595 (需要付费)

我这里动态添加数据源的类实现了CommandLineRunner接口,为的是springboot启动成功后就加载这两个数据源,然后数据源具体代理的端口都是根据SSHConfiguration连接成功后分配的端口。
代码如下:

@Slf4j
@Component
public class DynamicAddDS implements CommandLineRunner {
    @Autowired
    private DataSource dataSource;
    // private final DataSourceCreator dataSourceCreator; //3.3.1及以下版本使用这个通用
    @Autowired
    private HikariDataSourceCreator hikariDataSourceCreator;


    void addSshDs1() {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        dataSourceProperty.setUrl("jdbc:mysql://127.0.0.1:" + SSHConnection.PROXY_PORT + "/db1?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai");
        dataSourceProperty.setUsername("xxxx");
        dataSourceProperty.setPassword("xxxxxx");
        dataSourceProperty.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
        log.info("设置proxyds1数据源=" + dataSourceProperty.getUrl());
        // 注意下面方法的第一个形参(String ds),这个名称就是@DS("xxx")注解的名称,后面即可用@DS("proxyds1")去控制需要以哪个数据源去查询
        ds.addDataSource("proxyds1", dataSource);
    }

    void addSshDs2() {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        dataSourceProperty.setUrl("jdbc:mysql://127.0.0.1:" + SSHConnection.PROXY_PORT + "/db2?characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&serverTimezone=Asia/Shanghai");
        dataSourceProperty.setUsername("xxxx");
        dataSourceProperty.setPassword("xxxxxx");
        dataSourceProperty.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
        log.info("设置proxyds2数据源=" + dataSourceProperty.getUrl());
        ds.addDataSource("proxyds2", dataSource);
    }

    @Override
    public void run(String... args) throws Exception {
        addSshDs1();
        addSshDs2();
    }
}

这样基本完成了主要配置,随便写一个查询Mapper接口

@DS("proxyds1")
@Mapper
public interface TestDao extends BaseMapper<Map<String,Object>> {


    /**
     * 随便写的测试方法
     *
     * @return {@link List}<{@link Map}<{@link String},{@link String}>>
     */
    @Select("select count(1) from sys_user")
    int coutAll();

}

这样就可以在service注入这个Mapper进行查询了

3.总结

这个方案适合原有的多数据源都配置在yml文件中的项目,因为如果全部改造成动态数据源且数据源参数写入数据中的话,工程量稍稍有点大了。


代理的端口一定要给JSCH自动分配,不然容易出现本机运行正常,部署到服务器出现错误,这种错误就是需要代理的端口在该机器上已被占用了 ``` com.jcraft.jsch.JSchException: PortForwardingL: local port 127.0.0.1:3306 cannot be bound. at com.jcraft.jsch.PortWatcher.(PortWatcher.java:158) at com.jcraft.jsch.PortWatcher.addPort(PortWatcher.java:110) at com.jcraft.jsch.Session.setPortForwardingL(Session.java:1847) at com.jcraft.jsch.Session.setPortForwardingL(Session.java:1828) at com.jcraft.jsch.Session.setPortForwardingL(Session.java:1809) at com.jcraft.jsch.Session.setPortForwardingL(Session.java:1792) ```

也就是要使用下面方法的返回值去动态的配置代理端口

int proxyPort = session.setPortForwardingL(0,host,port)

而不能使用下面这种方式

session.setPortForwardingL(resource_host,resource_port,host,port)

还有要注意如下方法中的第一个形参名称,后续@DS("xxx")注解全靠这个参数作为标识了

ds.addDataSource("proxyds2", dataSource);

示例代码:https://github.com/ong-ing/case_analysis_demo/tree/master/ssh_dynamic_ds

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×