ssh 隧道

localizer
lzq 2025-10-28 11:08:12 +08:00
parent 086ec04309
commit da1d060a08
12 changed files with 248 additions and 181 deletions

View File

@ -1,19 +0,0 @@
package com.njzscloud.common.mp.config;
import com.njzscloud.common.mp.support.DBTunnel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "mybatis-plus.tunnel", name = "enable", havingValue = "true")
@EnableConfigurationProperties({DbTunnelProperties.class})
public class DBTunnelAutoConfiguration {
@Bean(destroyMethod = "close")
public DBTunnel dbTunnel(DbTunnelProperties dbTunnelProperties) {
return new DBTunnel(dbTunnelProperties);
}
}

View File

@ -1,34 +0,0 @@
package com.njzscloud.common.mp.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties("mybatis-plus.tunnel")
public class DbTunnelProperties {
private boolean enable = false;
private SSHProperties ssh;
private DBProperties db;
@Getter
@Setter
public static class SSHProperties {
private String host;
private int port = 22;
private String user;
private String credentials;
private String passphrase;
private int localPort;
}
@Getter
@Setter
public static class DBProperties {
private String host;
private int port = 3306;
}
}

View File

@ -1,108 +0,0 @@
package com.njzscloud.common.mp.support;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.njzscloud.common.mp.config.DbTunnelProperties;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
public class DBTunnel {
private final DbTunnelProperties dbTunnelProperties;
private JSch jsch;
private Session session;
private ScheduledExecutorService heartbeatExecutor;
public DBTunnel(DbTunnelProperties dbTunnelProperties) {
this.dbTunnelProperties = dbTunnelProperties;
if (dbTunnelProperties.isEnable()) {
log.info("数据库 SSH 隧道已启用");
connect();
startHeartbeat();
}
}
private synchronized void connect() {
try {
if (isConnected()) {
return;
}
DbTunnelProperties.SSHProperties ssh = dbTunnelProperties.getSsh();
String credentials = ssh.getCredentials();
String passphrase = ssh.getPassphrase();
String user = ssh.getUser();
String host = ssh.getHost();
int port = ssh.getPort();
int localPort = ssh.getLocalPort();
DbTunnelProperties.DBProperties db = dbTunnelProperties.getDb();
String dbHost = db.getHost();
int dbPort = db.getPort();
jsch = new JSch();
// 使用私钥认证
File keyFile = new File(credentials);
if (!keyFile.exists()) {
throw new RuntimeException("私钥文件不存在: " + credentials);
}
if (passphrase != null) {
jsch.addIdentity(credentials, passphrase);
} else {
jsch.addIdentity(credentials);
}
// 创建SSH会话
session = jsch.getSession(user, host, port);
session.setConfig("StrictHostKeyChecking", "no"); // 禁用主机密钥检查
// 设置保持活动状态
session.setServerAliveInterval(60000); // 每分钟发送一次保持活动的数据包
session.setServerAliveCountMax(3); // 允许3次失败
session.connect();
// 设置端口转发 (本地端口 -> 远程数据库)
session.setPortForwardingL(localPort, dbHost, dbPort);
log.info("SSH 隧道已成功连接并转发端口 {} -> {}:{}", localPort, dbHost, dbPort);
} catch (JSchException e) {
log.error("SSH 隧道连接失败", e);
throw new RuntimeException("SSH 隧道连接失败", e);
}
}
private void startHeartbeat() {
heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
heartbeatExecutor.scheduleAtFixedRate(() -> {
try {
if (!isConnected()) {
log.warn("SSH 隧道连接已断开,尝试重新连接...");
connect();
}
} catch (Exception e) {
log.error("SSH 隧道心跳检测失败", e);
}
}, 30, 30, TimeUnit.SECONDS); // 每30秒检查一次连接状态
}
public boolean isConnected() {
return session != null && session.isConnected();
}
public void close() {
if (heartbeatExecutor != null) {
heartbeatExecutor.shutdownNow();
}
if (session != null && session.isConnected()) {
session.disconnect();
log.info("SSH隧道已关闭");
}
}
}

View File

@ -1,3 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.njzscloud.common.mp.config.MpAutoConfiguration,\
com.njzscloud.common.mp.config.DBTunnelAutoConfiguration
com.njzscloud.common.mp.config.MpAutoConfiguration

View File

@ -19,10 +19,14 @@
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
<groupId>com.njzscloud</groupId>
<artifactId>njzscloud-common-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,129 @@
package com.njzscloud.common.sshtunnel;
import cn.hutool.core.util.StrUtil;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.njzscloud.common.core.tuple.Tuple2;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class SSHTunnel {
private final List<Tuple2<JSch, Session>> tunnels = new LinkedList<>();
private final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor();
private final AtomicBoolean running = new AtomicBoolean(false);
public SSHTunnel(SSHTunnelProperties sshTunnelProperties) {
if (sshTunnelProperties.isEnable()) {
log.info("SSH 隧道已启用");
running.set(true);
List<SSHTunnelProperties.SSHProperties> tunnelsProperties = sshTunnelProperties.getTunnels();
tunnelsProperties.stream()
.filter(SSHTunnelProperties.SSHProperties::isEnable)
.forEach(it -> {
Tuple2<JSch, Session> tunnel = createTunnel(it);
tunnels.add(tunnel);
});
reconnectTask();
}
}
private Tuple2<JSch, Session> createTunnel(SSHTunnelProperties.SSHProperties sshProperties) {
try {
String host = sshProperties.getHost();
int port = sshProperties.getPort();
String user = sshProperties.getUser();
String pwd = sshProperties.getPwd();
String credentials = sshProperties.getCredentials();
String passphrase = sshProperties.getPassphrase();
log.info("正在连接服务器:{}:{}", host, port);
JSch jsch = new JSch();
Session session;
if (StrUtil.isNotBlank(credentials)) {
// 使用私钥认证
File keyFile = new File(credentials);
if (!keyFile.exists()) {
throw new RuntimeException("私钥文件不存在: " + credentials);
}
if (passphrase != null) {
jsch.addIdentity(credentials, passphrase);
} else {
jsch.addIdentity(credentials);
}
session = jsch.getSession(user, host, port);
} else if (StrUtil.isNotBlank(pwd)) {
// 创建SSH会话
session = jsch.getSession(user, host, port);
// 使用密码认证
session.setPassword(pwd);
} else {
throw new RuntimeException("未提供有效认证方式(私钥或密码)");
}
session.setConfig("StrictHostKeyChecking", "no"); // 禁用主机密钥检查
// 设置保持活动状态
session.setServerAliveInterval(60000); // 每分钟发送一次保持活动的数据包
session.setServerAliveCountMax(3); // 允许3次失败
session.connect();
List<SSHTunnelProperties.ProxyProperties> proxyProperties = sshProperties.getProxy();
for (SSHTunnelProperties.ProxyProperties proxyProperty : proxyProperties) {
int localPort = proxyProperty.getLocalPort();
String targetHost = proxyProperty.getTargetHost();
int targetPort = proxyProperty.getTargetPort();
// 设置端口转发 (本地端口 -> 目标地址)
session.setPortForwardingL(localPort, targetHost, targetPort);
log.info("已创建隧道,本地端口:{} --> 服务器:{}:{} --> 目标地址:{}:{}", localPort, host, port, targetHost, targetPort);
}
return Tuple2.create(jsch, session);
} catch (JSchException e) {
log.error("SSH 隧道连接失败", e);
throw new RuntimeException("SSH 隧道连接失败", e);
}
}
private void reconnectTask() {
reconnectExecutor.scheduleAtFixedRate(() -> {
if (!running.get()) return;
try {
for (Tuple2<JSch, Session> tunnel : tunnels) {
Session session = tunnel.get_1();
if (!session.isConnected()) {
log.warn("SSH 隧道连接已断开,尝试重新连接...");
session.connect();
}
}
} catch (Exception e) {
log.error("重连任务执行失败", e);
}
}, 30, 30, TimeUnit.SECONDS); // 每30秒检查一次连接状态
}
public void close() {
running.set(false);
reconnectExecutor.shutdownNow();
for (Tuple2<JSch, Session> tunnel : tunnels) {
Session session = tunnel.get_1();
if (session != null && session.isConnected()) {
String host = session.getHost();
int port = session.getPort();
session.disconnect();
log.info("SSH 隧道已关闭,服务器:{}:{}", host, port);
}
}
tunnels.clear();
}
}

View File

@ -0,0 +1,18 @@
package com.njzscloud.common.sshtunnel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "ssh-tunnel", name = "enable", havingValue = "true")
@EnableConfigurationProperties({SSHTunnelProperties.class})
public class SSHTunnelAutoConfiguration {
@Bean(destroyMethod = "close")
public SSHTunnel sshTunnel(SSHTunnelProperties SSHTunnelProperties) {
return new SSHTunnel(SSHTunnelProperties);
}
}

View File

@ -0,0 +1,66 @@
package com.njzscloud.common.sshtunnel;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@Getter
@Setter
@ConfigurationProperties("ssh-tunnel")
public class SSHTunnelProperties {
private boolean enable = false;
private List<SSHProperties> tunnels;
@Getter
@Setter
public static class SSHProperties {
private boolean enable = true;
/**
*
*/
private String host;
/**
* SSH
*/
private int port = 22;
/**
*
*/
private String user;
/**
*
*/
private String pwd;
/**
*
*/
private String credentials;
/**
*
*/
private String passphrase;
private List<ProxyProperties> proxy;
}
@Getter
@Setter
public static class ProxyProperties {
/**
*
*/
private int localPort;
/**
*
*/
private String targetHost;
/**
*
*/
private int targetPort;
}
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.njzscloud.common.sshtunnel.SSHTunnelAutoConfiguration

View File

@ -26,6 +26,10 @@
<groupId>com.njzscloud</groupId>
<artifactId>njzscloud-common-http</artifactId>
</dependency>
<dependency>
<groupId>com.njzscloud</groupId>
<artifactId>njzscloud-common-sshtunnel</artifactId>
</dependency>
<dependency>
<groupId>com.njzscloud</groupId>
<artifactId>njzscloud-common-wechat</artifactId>

View File

@ -1,11 +1,11 @@
spring:
datasource:
url: jdbc:mysql://47.115.226.143:3306/njzscloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true
username: root
password: zsy2022
# url: jdbc:mysql://localhost:33061/njzscloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true
# url: jdbc:mysql://47.115.226.143:3306/njzscloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true
# username: root
# password: admin888999
# password: zsy2022
url: jdbc:mysql://localhost:33061/njzscloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true&allowMultiQueries=true
username: root
password: admin888999
security:
auth-ignores:
- /auth/obtain_code
@ -27,6 +27,7 @@ spring:
- /payment/wechat/refundNotify
- /district/areaList
- /biz_audit_config/copy
- /order_info/gps_test
app:
default-place:
province: 320000
@ -47,18 +48,18 @@ oss:
secret-key: zllX0ZJ1EwsZXT6dE6swCLgTF4ImGg
bucket-name: cdn-zsy
mybatis-plus:
tunnel:
enable: false
ssh:
ssh-tunnel:
enable: true
tunnels:
- enable: true
host: 139.224.54.144
port: 22
user: root
credentials: D:/我的/再昇云/服务器秘钥/139.224.54.144_YZS_S1.pem
localPort: 33061
db:
host: localhost
port: 33061
proxy:
- localPort: 33061
targetHost: localhost
targetPort: 33061
wechat:
# app-id: wx3c06d9dd4e56c58d
@ -75,7 +76,7 @@ wechat:
cert-serial-no: 1BCB1533688F349541C7B636EF67C666828BADBA
# 文件路径
private-key-path: classpath:cert/apiclient_cert.p12
# private-key-path: D:/project/再昇云/代码/njzscloud/njzscloud-svr/src/main/resources/cert/apiclient_cert.p12
# private-key-path: D:/project/再昇云/代码/njzscloud/njzscloud-svr/src/main/resources/cert/apiclient_cert.p12
# 支付回调地址
notify-url: http://115.29.236.92:8082/payment/wechat/notify
# 退款回调地址

View File

@ -48,6 +48,11 @@
<artifactId>njzscloud-common-ws</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njzscloud</groupId>
<artifactId>njzscloud-common-sshtunnel</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njzscloud</groupId>
<artifactId>njzscloud-common-sichen</artifactId>