24.05.23 (목)
지난 번 글을 작성하고 하루가 지났는데 문제가 생겼다.
잘되던 SSH 연결이 갑자기 Jsch: Auth fail 이 나면서 연결이 안되는 문제였다.
구글링을 엄청 하던 도중 Jsch 라이브러리 자체가 오래된 레거시이고 유지보수도 안된다는 글이 보였다.
그래서 SSH를 연결할 수 있는 다른 라이브러리를 찾던 도중 apache.sshd 라이브러리를 발견해 이걸로 구현 하고자 한다.
블로그에 자료가 많지 않아 공식문서 보고 구현시도를 했다.
시작
환경: Java17, Spring Boot 3.2.5, MyBatis, Gradle 8.7
1. 기존에 안되는 Jsch 의존성 삭제하고 apache.sshd 라이브러리 의존성추가
implementation 'org.apache.sshd:sshd-core:2.7.0'
2. 기존 Jsch 관련 코드를 apache.sshd 라이브러리 활용해서 다시 작성
@Bean
public DataSource dataSource() throws Exception {
// SSH 연결설정
SshClient client = SshClient.setUpDefaultClient();
client.start();
// 암호키 경로와 암호키에 설정된 패스워드 부분 설정
KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser(); // 선언
Path path = Paths.get(sshPrivateKey); // 암호키 경로
Collection<KeyPair> keys = loader.loadKeyPairs(null, path, FilePasswordProvider.of(sshPassphrase)); // FilePasswordProvider를 통해 암호키에 설정된 패스워드를 넣어준다.
// 세션생성 (SSH 계정, SSH 호스트 주소, SSH 포트)
ClientSession session = client.connect(sshUsername, sshHost, sshPort).verify(10, TimeUnit.SECONDS).getSession();
// 세션에 접속하기 위한 인증
session.addPublicKeyIdentity(keys.iterator().next());
session.auth().verify(10, TimeUnit.SECONDS);
// 포트 포워딩 설정
SshdSocketAddress localAddress = new SshdSocketAddress("localhost", sshLocalPort);
SshdSocketAddress remoteAddress = new SshdSocketAddress(sshRemoteHost, sshRemotePort);
// SSH 접속 시작
session.startLocalPortForwarding(localAddress, remoteAddress);
// SSH 접속 후 DB 접속
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(jdbcUrl);
dataSource.setUsername(dbUserName);
dataSource.setPassword(dbUserPw);
return dataSource;
}
3. import가 잘 안될 때 참고
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.util.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.sshd.client.SshClient;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import javax.sql.DataSource;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
4. 전체 소스코드 ( 하단에 최종 리팩토링한 소스 있으니 이거 참고 ㄴㄴ)
package org.daeng2go.daeng2go_server.common.config;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.util.security.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.sshd.client.SshClient;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import javax.sql.DataSource;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
@EnableTransactionManagement
@RequiredArgsConstructor
@MapperScan(basePackages="여긴 본인 프로젝트 mapper가 있는 경로를 집어넣을 것!")
public class MybatisConfig {
// 로컬 환경에서 사용시 전부 주석 해제
// 개발, 운영에 올릴 땐 mapperPath, configPath 빼고 전부 주석
@Value(value = "${ssh.host}")
String sshHost;
@Value(value = "${ssh.user}")
String sshUsername;
@Value(value = "${ssh.ssh_port}")
int sshPort;
@Value(value = "${ssh.private_key}")
String sshPrivateKey;
@Value(value = "${ssh.passphrase}")
String sshPassphrase;
@Value(value = "${ssh.local_port}")
int sshLocalPort;
@Value(value = "${ssh.remote_host}")
String sshRemoteHost;
@Value(value = "${ssh.remote_port}")
int sshRemotePort;
@Value(value = "${spring.datasource.driver-class-name}")
String driverClassName;
@Value(value = "${spring.datasource.url}")
String jdbcUrl;
@Value(value = "${spring.datasource.username}")
String dbUserName;
@Value(value = "${spring.datasource.password}")
String dbUserPw;
@Value(value = "${mybatis.mapper-locations}")
String mapperPath;
@Value(value = "${mybatis.config-location}")
String configPath;
// 로컬 사용시 주석
// 개발, 운영에 올릴 땐 주석 풀기
//private final DataSource dataSource;
// 로컬 사용시에만 주석 풀기
// 개발, 운영에 올릴 땐 주석
@Bean
public DataSource dataSource() throws Exception {
// SSH 연결설정
SshClient client = SshClient.setUpDefaultClient();
client.start();
// 암호키 경로와 암호키에 설정된 패스워드 부분 설정
KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser(); // 선언
Path path = Paths.get(sshPrivateKey); // 암호키 경로
Collection<KeyPair> keys = loader.loadKeyPairs(null, path, FilePasswordProvider.of(sshPassphrase)); // FilePasswordProvider를 통해 암호키에 설정된 패스워드를 넣어준다.
// 세션생성 (SSH 계정, SSH 호스트 주소, SSH 포트)
ClientSession session = client.connect(sshUsername, sshHost, sshPort).verify(10, TimeUnit.SECONDS).getSession();
// 세션에 접속하기 위한 인증
session.addPublicKeyIdentity(keys.iterator().next());
session.auth().verify(10, TimeUnit.SECONDS);
// 포트 포워딩 설정
SshdSocketAddress localAddress = new SshdSocketAddress("localhost", sshLocalPort);
SshdSocketAddress remoteAddress = new SshdSocketAddress(sshRemoteHost, sshRemotePort);
// SSH 접속 시작
session.startLocalPortForwarding(localAddress, remoteAddress);
// SSH 접속 후 DB 접속
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(jdbcUrl);
dataSource.setUsername(dbUserName);
dataSource.setPassword(dbUserPw);
return dataSource;
}
// 로컬 사용시 dataSource()
// 개발, 운영에 올릴 땐 dataSource
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
// mybatis-config.xml 설정
Resource mybatisConfig = new ClassPathResource(configPath);
sessionFactory.setConfigLocation(mybatisConfig);
// mapper.xml 위치 설정
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] mapperLocations = resolver.getResources(mapperPath);
sessionFactory.setMapperLocations(mapperLocations);
return sessionFactory.getObject();
}
@Bean
public PlatformTransactionManager transactionManager() throws Exception {
return new DataSourceTransactionManager(dataSource());
}
}
4-1 위 코드 리팩토링 - 주석처리하면서 관리하는게 불편해서 수정 진행 ( 이게 최종)
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
@EnableTransactionManagement
@RequiredArgsConstructor
@MapperScan(basePackages="org.example.example.**.mapper")
public class MybatisConfig {
@Value(value = "${spring.datasource.driver-class-name}")
String driverClassName;
@Value(value = "${spring.datasource.url}")
String jdbcUrl;
@Value(value = "${spring.datasource.username}")
String dbUserName;
@Value(value = "${spring.datasource.password}")
String dbUserPw;
@Value(value = "${mybatis.mapper-locations}")
String mapperPath;
@Value(value = "${mybatis.config-location}")
String configPath;
@Profile("local")
@Bean
public DataSource dataSourceWithSsh(
@Value(value = "${ssh.host}") String sshHost,
@Value(value = "${ssh.user}") String sshUsername,
@Value(value = "${ssh.ssh_port}") int sshPort,
@Value(value = "${ssh.private_key}") String sshPrivateKey,
@Value(value = "${ssh.passphrase}") String sshPassphrase,
@Value(value = "${ssh.local_port}") int sshLocalPort,
@Value(value = "${ssh.remote_host}") String sshRemoteHost,
@Value(value = "${ssh.remote_port}") int sshRemotePort) throws Exception {
// SSH 연결설정
SshClient client = SshClient.setUpDefaultClient();
client.start();
// 암호키 경로와 암호키에 설정된 패스워드 부분 설정
KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser();
Path path = Paths.get(sshPrivateKey);
Collection<KeyPair> keys = loader.loadKeyPairs(null, path, FilePasswordProvider.of(sshPassphrase));
// 세션생성 (SSH 계정, SSH 호스트 주소, SSH 포트)
ClientSession session = client.connect(sshUsername, sshHost, sshPort).verify(10, TimeUnit.SECONDS).getSession();
// 세션에 접속하기 위한 인증
session.addPublicKeyIdentity(keys.iterator().next());
session.auth().verify(10, TimeUnit.SECONDS);
// 포트 포워딩 설정
SshdSocketAddress localAddress = new SshdSocketAddress("localhost", sshLocalPort);
SshdSocketAddress remoteAddress = new SshdSocketAddress(sshRemoteHost, sshRemotePort);
// SSH 접속 시작
session.startLocalPortForwarding(localAddress, remoteAddress);
// SSH 접속 후 DB 접속
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(jdbcUrl);
dataSource.setUsername(dbUserName);
dataSource.setPassword(dbUserPw);
return dataSource;
}
@Profile({"dev", "prod"})
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(jdbcUrl);
dataSource.setUsername(dbUserName);
dataSource.setPassword(dbUserPw);
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// mybatis-config.xml 설정
Resource mybatisConfig = new ClassPathResource(configPath);
sessionFactory.setConfigLocation(mybatisConfig);
// mapper.xml 위치 설정
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] mapperLocations = resolver.getResources(mapperPath);
sessionFactory.setMapperLocations(mapperLocations);
return sessionFactory.getObject();
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
5. 잘 이해가 안가면 전에 작성한 글 참고
https://fuckingjava.tistory.com/181
인텔리제이 SSH DB 연결하기
1. 삼단메뉴 -> View -> Tool Windows -> Database 2. + 모양 클릭 -> Data Source -> 본인이 사용 하는 DB 선택 (예시는 Maria DB) 3. SSH/SSL 클릭 -> Use SSH tunnel 체크 -> SSH configuration 맨 우측에 문서모양 같은거 클릭 4
fuckingjava.tistory.com
https://fuckingjava.tistory.com/182
Spring Boot SSH AWS JDBC 연결 with 마이바티스
24.05.20 (월)회사 신규 프로젝트 진행 중 개발서버 DB 연결 과정에서 SSH 터널링을 통해 연결하던 도중 생긴 문제이다. 인텔리제이로 DB연결은 했으나 Spring Boot 실행시 jdbc 연결이 안되는 문제가 생
fuckingjava.tistory.com
공식문서: https://github.com/apache/mina-sshd
'마이바티스' 카테고리의 다른 글
Spring Boot SSH AWS JDBC 연결 with 마이바티스 (0) | 2024.05.20 |
---|---|
마이바티스 문법 (0) | 2023.10.19 |
페이징 처리시 필요한 것들 (0) | 2023.07.14 |
마이바티스 쿼리 로그 보기 (0) | 2023.05.16 |
스프링 부트에서 마이바티스 설정하기 (0) | 2023.04.04 |