상황
이제 Tomcat에 JNDI 설정하는 것은 끝냈다.(아래 포스팅 참고)
2023.10.23 - [Back/Spring Boot] - [SpringBoot] 외장 tomcat에 JNDI 설정 (+war)
고객사의 요구에 따라 JNDI 방식으로 DB를 연결하는 방식에 암호화를 요구하는 경우도 있다고 한다.
구글링해보니, 생각보다 최신 버전에 맞는 구현방법이 없어서 조금 헤맸다.
Tomcat에 JNDI 기본설정은 선행되었다는 가정하에
대략적인 순서를 정리해보면
- 암복호화를 위한 새프로젝트 만들고 jar로 패키징하기
- 서버에 jar 적용
- 서버 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) 새 프로젝트를 만든다.
2) lib 하위에 tomcat-jdbc.jar 파일 옮겨두기
2-1) 프로젝트 하위에 lib 디렉토리를 생성한다.
2-2) 로컬 PC에 다운로드 받아놓은 tomcat 폴더에서 lib 하위에 tomcat-jdbc.jar 파일을 방금 생성한 플젝 lib 하위로 붙여 넣는다.
✏️나는 JNDI 암호화를 할 Spring Boot 플젝의 버전이랑 호환되는 tomcat 버전을 찾아서 다운받았다.
자신의 플젝 버전과 싱크를 맞추어 버전을 확인하는 부분 잊지 말것!
2-3) Project Structure에 Library를 추가한다.
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로 패키징
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연결이 잘 되면 성공한 것이다.
'Back > Spring Boot' 카테고리의 다른 글
컨트롤러에서 Enum 타입 @RequestParam @PathVariable로 매핑하기 (2) | 2024.04.15 |
---|---|
@RequestBody DTO json데이터가 null일 때 (3) | 2024.02.07 |
[SpringBoot] 외장 tomcat에 JNDI 설정 (+war) (1) | 2023.10.23 |
[SpringBoot] war로 배포하기, 외장 톰캣 구동 (+Trouble Shooting) (1) | 2023.10.11 |
[Gradle] biuld.gradle 살펴보기 (0) | 2023.09.05 |
댓글