본문 바로가기
Back/Spring Boot

[SpringBoot, Tomcat] Tomcat JNDI DB 정보 암호화

by 은z 2023. 10. 24.

상황

이제 Tomcat에 JNDI 설정하는 것은 끝냈다.(아래 포스팅 참고)

2023.10.23 - [Back/Spring Boot] - [SpringBoot] 외장 tomcat에 JNDI 설정 (+war)

 

[SpringBoot] 외장 tomcat에 JNDI 설정 (+war)

상황 WAS에 데이터베이스 정보를 설정하고 연결하는 방식으로 개발해달라는 요청이 들어왔다. 이번 기회를 통해 tomcat에 JNDI 설정을 하는 방법을 정리해보았다. 실행환경 및 버전 OS : Ubuntu 20.04.3 L

zoetechlog.tistory.com

 

 

고객사의 요구에 따라 JNDI 방식으로 DB를 연결하는 방식에 암호화를 요구하는 경우도 있다고 한다.

구글링해보니, 생각보다 최신 버전에 맞는 구현방법이 없어서 조금 헤맸다.

 

 

Tomcat에 JNDI 기본설정은 선행되었다는 가정하에

대략적인 순서를 정리해보면

  1. 암복호화를 위한 새프로젝트 만들고 jar로 패키징하기
  2. 서버에 jar 적용
  3. 서버 Tomcat에 설정파일 수정

 


실행환경 및 버전

  • OS : Ubuntu 20.04.3 LTS
  • build tool : Gradle
  • IDE : Intellij 유료버전
  • JDK : open JDK 11
  • Spring Boot : 2.7.9
  • Tomcat : 9.0.71

 

✔️암호화 복호화를 위한 새 프로젝트 만들고 jar로 패키징하기

 

1) 새 프로젝트를 만든다.

CREATE 버튼 눌러 생성!

 

 

2) lib 하위에 tomcat-jdbc.jar 파일 옮겨두기

 

2-1) 프로젝트 하위에 lib 디렉토리를 생성한다.

lib 디렉토리 생성

 

 

 

2-2) 로컬 PC에 다운로드 받아놓은 tomcat 폴더에서 lib 하위에 tomcat-jdbc.jar 파일을 방금 생성한 플젝 lib 하위로 붙여 넣는다.

✏️나는 JNDI 암호화를 할 Spring Boot 플젝의 버전이랑 호환되는 tomcat 버전을 찾아서 다운받았다.

자신의 플젝 버전과 싱크를 맞추어 버전을 확인하는 부분 잊지 말것!

lib 하위로 copy&paste
짜잔

 

 

2-3) Project Structure에 Library를 추가한다.

OK 버튼 누르면 적용 완료.

 

3) 암호화, 복호화 기능을 하는 Class 생성하기

이제 JNDI DB설정을 암호화하고, 복호화하는 기능을 만들어보자.

참고로 나는 DB url, username, password 부분을 암호화할 것이다.

 

3-1) src 하위에 package를 만들고, 생성되어 있던 Main 클래스를 만든 package 하위로 옮긴다.

✏️package의 이름은 사용자가 임의로 만들어도 된다. package의 path가 Tomcat server.xml에서 필요하다는 것만 기억해두자.

 

3-2) Main 클래스를 Encryptor 클래스로 변경한다.

✏️main 메소드를 실행하여 DB 계정의 암호화 정보를 얻을 수 있다. 추출해낸 암호화 코드를 server.xml에 넣을 것!

package secured;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class Encryptor {
    private static final String ALGORITHM = "AES";
    private static final String defaultSecretKey = "asdfo23jda3sds";
    private Key secretKeySpec;

    public Encryptor()
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, UnsupportedEncodingException {
        this(null);
    }

    public Encryptor(String secretKey)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException {
        this.secretKeySpec = generateKey(secretKey);
    }

    public String encrypt(String plainText) throws InvalidKeyException, NoSuchAlgorithmException,
            NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        return asHexString(encrypted);
    }

    public String decrypt(String encryptedString) throws InvalidKeyException, IllegalBlockSizeException,
            BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        byte[] original = cipher.doFinal(toByteArray(encryptedString));
        return new String(original);
    }

    private Key generateKey(String secretKey) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        if (secretKey == null) {
            secretKey = defaultSecretKey;
        }
        byte[] key = (secretKey).getBytes("UTF-8");
        MessageDigest sha = MessageDigest.getInstance("SHA-1");
        key = sha.digest(key);
        key = Arrays.copyOf(key, 16); // use only the first 128 bit
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        kgen.init(128); // 192 and 256 bits may not be available
        return new SecretKeySpec(key, ALGORITHM);
    }

    private final String asHexString(byte buf[]) {
        StringBuffer strbuf = new StringBuffer(buf.length * 2);
        int i;
        for (i = 0; i < buf.length; i++) {
            if (((int) buf[i] & 0xff) < 0x10) {
                strbuf.append("0");
            }
            strbuf.append(Long.toString((int) buf[i] & 0xff, 16));
        }
        return strbuf.toString();
    }

    private final byte[] toByteArray(String hexString) {
        int arrLength = hexString.length() >> 1;
        byte buf[] = new byte[arrLength];
        for (int ii = 0; ii < arrLength; ii++) {
            int index = ii << 1;
            String l_digit = hexString.substring(index, index + 2);
            buf[ii] = (byte) Integer.parseInt(l_digit, 16);
        }
        return buf;
    }

    public static void main(String[] args) throws Exception {
        Encryptor aes = new Encryptor(defaultSecretKey);
        String testid = "testid";
        System.out.println("Id:" + aes.encrypt(testid));

        String testpassword = "testpassword";
        System.out.println("Pw:" + aes.encrypt(testpassword));

        String url = "testURL";
        System.out.println("url:" + aes.encrypt(url));
    }
}

 

3-2) EncryptedDataSourceFactory 클래스를 생성한다.

package secured;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.sql.SQLException;
import java.util.Properties;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.naming.Context;
import javax.sql.DataSource;

import org.apache.tomcat.jdbc.pool.DataSourceFactory;
import org.apache.tomcat.jdbc.pool.PoolConfiguration;
import org.apache.tomcat.jdbc.pool.XADataSource;

public class EncryptedDataSourceFactory extends DataSourceFactory {
    private Encryptor encryptor = null;

    public EncryptedDataSourceFactory() {
        try {
            encryptor = new Encryptor(); // If you've used your own secret key, pass it in...
        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | UnsupportedEncodingException e) {
            System.out.println("Error instantiating decryption class." + e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public DataSource createDataSource(Properties properties, Context context, boolean XA) throws InvalidKeyException,
            IllegalBlockSizeException, BadPaddingException, SQLException, NoSuchAlgorithmException,
            NoSuchPaddingException {
        // Here we decrypt our password.
        PoolConfiguration poolProperties = EncryptedDataSourceFactory.parsePoolProperties(properties);
        poolProperties.setUrl(encryptor.decrypt(poolProperties.getUrl())); // URL
        poolProperties.setUsername(encryptor.decrypt(poolProperties.getUsername())); // UserName
        poolProperties.setPassword(encryptor.decrypt(poolProperties.getPassword())); // Password
        // The rest of the code is copied from Tomcat's DataSourceFactory.
        if (poolProperties.getDataSourceJNDI() != null && poolProperties.getDataSource() == null) {
            performJNDILookup(context, poolProperties);
        }
        org.apache.tomcat.jdbc.pool.DataSource dataSource = XA ? new XADataSource(poolProperties)
                : new org.apache.tomcat.jdbc.pool.DataSource(poolProperties);
        dataSource.createPool();
        return dataSource;
    }
}

 

 

4) jar로 패키징

이 상태에서 OK버튼 클릭!

 

Build 클릭하면 jar가 생성된다.
out 하위에 생성된 jar

 

5) jar 파일 서버의 Tomcat  > lib 디렉토리에 넣기

📌이제 패키징한 jar파일을 어플리케이션 배포할 서버의 tomcat > lib 폴더에 넣는다.

 

📌 실행 시에 권한을 주기 위해 아래 명령어로 권한도 수정한다.

chmod -Rf 755 encrypt_tomcat_module_jar

 

[JNDI를 설정할 프로젝트의 application 소스에서 적용할 부분]

✔️properties 파일에 jndi 적용

📌jndi-name 을 명시해준다.

java:comp/env/  이후에   jdbc/j4s  Tomcat에서 사용할 name이다.

spring:
  datasource:
    jndi-name: java:comp/env/jdbc/j4s

 

 

 

[tomcat 설정파일에서 적용할 부분]

✔️server.xml

📌톰캣의 conf/server.xml 을 vi 편집기로 열어 아래와 같이 추가한다.

"암호화"로 둔 부분은 자신의 프로젝트 DB설정에 따라 적절히 명시해준다.

<GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->

        <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
                pathname="conf/tomcat-users.xml" />


		<!--  추가한 부분 시작   -->
        <Resource name="jdbc/j4s"
                  factory="secured.EncryptedDataSourceFactory"
                  auth="Container"
                  type="javax.sql.DataSource"
                  username="암호화"
                  password="암호화"
                  driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
                  url="암호화"/>
		<!--  추가한 부분 끝   -->

</GlobalNamingResources>

👀눈 여겨볼 부분은 name과 factory 그리고 암호화할 부분이다!

name 은 위에서 application properties에서 설정한 name으로 명시한다. (ex. java:comp/env/  빼고)

factory 는 암복호화 패키징한 jar파일에서 EncryptedDataSourceFactory.java의 path를 적어준다.

 

 

✔️context.xml

📌 톰캣의 conf/context.xml 을 vi 편집기로 열어 아래와 같이 추가한다. (name, global 부분도 잘 맞춰서 명시하기!)

<Context>

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
    
    
    <!-- 추가한 부분 시작 -->
    <ResourceLink auth="Container" name="jdbc/j4s" global="jdbc/j4s" type="javax.sql.DataSource" />
	<!-- 추가한 부분 끝 -->
    
</Context>

 


✔️ war 파일 배포 후 서비스 시작해보기

📌 이제 설정은 마쳤으니, 프로젝트를 war로 패키징하여 설정해둔 톰캣 위에서 배포해보자!

그리고 startup.sh 명령어를 통해 실행하여 DB연결이 잘 되면 성공한 것이다.

 

댓글