본문 바로가기

마이바티스

Java, Spring Boot / Jsch: Auth fail => apache.sshd로 해결

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

 

반응형