Mybatis Configuration Intercepter(1)_DB암호화

떡ol·2022년 10월 22일
0

Mybatis Interceptor는 XML 파일에 작성한 SQL의 메타정보들을 얻을 수 있고 파라미터를 가공해서 쿼리를 실행할 수 있다.

실제 운영에서 회원 관리를 위한 자료를 Insert한다고 하자. 유저들은 마음에 안들겠지만 개인들의 주민번호(인증서)를 이용해서 회원가입에 이용하기도 하고, 전화번호, 카드정보를 DB에 담아두기도 한다. 그렇다고 이런자료를 직원들이 다 볼 수 있게 평문으로 저장하는건 바람직하지 않다.
그래서 그러한 민감한 자료를 저장할 땐 암호화하여 저장하고, 유저가 웹페이지를 서칭 할 때는 다시 평문으로 복호화하여 이용하는데 불편함이 없게 해야한다.

1. CryptoManager.java 만들기

첫번째로 암호화 관련 유틸클래스를 만들어 이 객체를 통해 암 복호화 가능한 기능을 구현하자.
암복호화 관련 알고리즘은 'AES-256'를 이용하였다. 여기참고
양방향 암복호화를 이용한건 고객정보가 무조건 암호화로 이루어져야하는건 맞지만, 혹시 모를 CS문의가 올때 고객정보를 확인할 필요가 있으므로... 나 이 카드 등록안했는데 왜 이 카드로 결제 됐죠?

@Component
public class CryptoManager{
	
	private static CryptoManager INSTANCE;
	
	Map<String, Map<String, Object>> cryptoCheckYaml = null;
	
	@Resource
	Crypto crypto;
	
	@PostConstruct
	private void init() {
		INSTANCE = this;
		try {
			InputStream cryptoFile = new ClassPathResource("crypto-config.yml").getInputStream();
			cryptoCheckYaml = new Yaml().load(cryptoFile);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static CryptoManager getInstance() {
		return INSTANCE;
	}
	
	public boolean isCryptoSql(String sqlId) {
		return cryptoCheckYaml.containsKey(sqlId);
	}
	
	public Object getEncryptoParams(Object param, String sqlId) {
		Map<String, Object> newParam = new HashMap<String, Object>();
		if(param instanceof Map) {
			newParam = (Map<String, Object>) param;
			for(Entry<String, Object> entry : newParam.entrySet()) {
				newParam.put(entry.getKey(), setEncryptoString(sqlId, entry.getKey(), entry.getValue()));
			}
		}else {
			return param;
		}
		return newParam;
	}
	
	public List<Map<String, Object>> getDecryptResults(List<Map<String, Object>> results, String sqlId){
		List<Map<String, Object>> newResult = new ArrayList<Map<String,Object>>();
		for(int i=0; i<results.size(); i++) {
			if(results.get(i) instanceof Map) {
				Map<String, Object> newMap = new HashMap<String, Object>();
				Map<String, Object> oldMap = results.get(i);
				for (Entry<String, Object> data : oldMap.entrySet()) {
					newMap.put(data.getKey(), setDecryptoString(sqlId, data.getKey(), data.getValue()));
				}
				newResult.add(newMap);
			}else return results;
		}
		return newResult;
	}
	
	
	//set
	private Object setEncryptoString(String sqlId, String key, Object value) {
		String newValue = null ;
		Object data = cryptoCheckYaml.get(sqlId).get("encParams");
		if(data instanceof List) {
			if(((List) data).contains(key)) {
				newValue =  crypto.encrypt((String)value); 				
			}else return value;
		}else{
			return value;
		}
		return newValue;
	}
	
	
	private Object setDecryptoString(String sqlId, String key, Object value) {
		String newValue = null ;
		Object data = cryptoCheckYaml.get(sqlId).get("decColumns");
		if(data instanceof List) {
			if(((List) data).contains(key)) {
				newValue =  crypto.decrypt((String)value); 
			}else return value;
		}else{
			return value;
		}
		return newValue;
	}
	
	 
}

1.1 CryptoManager 객체생성

CryptoManager를 static 변수로 선언하였고, @Component에 의해 생성된 객체가 @PostConstruct를 통해 INSTANCE에 저장되었다.
@PostConstruct는 생성자처럼 @Bean이 첫 로드된 직후 한번 실행된다. 이제 이 녀석은 프로젝트에서 어느 공간에서나 @Resource로 불러오기만하면 싱글톤으로 사용가능하다. (생각해보니 생성자에 담아서 그냥 사용해도 되겠다...)

1.2 Yaml().load 객체생성

추가로 new Yaml().load(cryptoFile)도 로드하고 글로벌 변수로 저장하여 이 또한 여러번 새 객체의 생성없이 불러오는게 가능하다.

##crypto-config.yml
#USER TABLE
com.mapper.DBtestMP.getUserByCrypto : 
  decColumns:
    - PWD
com.mapper.DBtestMP.insertUser : {encParams:[pwd], decColumns:[]}

다음과 같이 yaml형식, Json형식 두개로 만들어서 사용해보았다. yaml은 한줄로 작성이 안된다.
볼러오는건 cryptoCheckYaml.containsKey(sqlId)에 containsKey에서 해당 키를 검색하여 찾아오는게 가능하다.



나머지 getter, setter 설명은 생략하겠다.

2. Mybatis Intercepter

2.1 @Intercepts

Sql 쿼리문을 실행하면

  • @Intercepts이 선언되어있는 클래스
  • Bean id sqlSession에 속성 configLocation 프로퍼티에 선언되어있는 파일

를 스캔하여 중간에 가로채서 원하는 작업을 하게금 명령할 수 있게된다.
아래는 순서대로 bean에 설정내용과 와 해당 파일에 대한 내용이다.

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource"/>
		<property name="configLocation" value="classpath:/sqlmap/sqlmapconfig.xml"/>  <!--본인에게 알맞는주소로 변경-->
</bean>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<plugins>
		<plugin interceptor="com.intercepter.MybatisCryptoInterceptor"/>
	</plugins>
</configuration>

그리고 이제 @Intercepter가 선언된 파일을 살펴보자.

@Intercepts({ 
	@Signature(
		args = { 
			MappedStatement.class
			,Object.class
			,RowBounds.class
			,ResultHandler.class}
		, method = "query"
		, type = Executor.class) 
	, 
	@Signature(
			args = { 
				MappedStatement.class
				,Object.class}
			, method = "update"
			, type = Executor.class) 

})
public class MybatisCryptoInterceptor implements Interceptor {

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		CryptoManager cryptoManager = CryptoManager.getInstance();
		MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; //MAAPED_STATMENT_INDEX
		String sqlId = ms.getId();
		String method = invocation.getMethod().getName();
		
		if(!cryptoManager.isCryptoSql(sqlId)) {
			return invocation.proceed();
		}else if("update".equals(method)) {
			Object param = (Object) invocation.getArgs()[1]; //PARAMETER_INDEX
			invocation.getArgs()[1] = cryptoManager.getEncryptoParams(param, sqlId);
			return invocation.proceed();
		}else if ("query".equals(method)) {
			Object results = invocation.proceed();
			if(results instanceof List) {
				return results = cryptoManager.getDecryptResults((List<Map<String, Object>>) results, sqlId);
			}else {
				return results;
			}
		}else {
			return invocation.proceed();			
		}
	}
	
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}
}

2.2 @Signature

@Signature에 대한 설명이다.

속성값설명
typeExecuter라는 인터페이스는 Mybatis의 XML 파일에 작성된 SQL을 실행한다.
해당 인터페이스 내부를 보면 각 mybatis method 시그니처 정보를 볼 수 있다
type은 Executer말고도 정말많은데, "open Declaration"을 클릭해서 해당 .jar안에 컴파일된 interface들을 참고하면 다양한 type와 오버로딩된 args들을 확인가능하다.
methodinsert or update or delete가 실행되면 Executer의 update 라는 메소드를 호출하며
select는 query 라는 메소드를 호출한다.
argsmapper에 작성된 method를 호출할때 전달된 파라미터이다.
MapperStatement : 공통적으로 Object[] 타입의 인덱스 0번에 필수로 저장된다. 여기에 XML 메타정보가 담겨있다.
RowBounds : 페이징 작업에서사용되는 offset을 설정 가능하게 해준다.
ResultHandler : 결과값을 바로 인풋해서 리턴요청을 할 수 있는데 주로 너무 많은 데이터를 요청할 때 응답이 오래 걸리는 작업을 커스터마이징 하여 바로 리턴 호출 할 수있게 해준다.

결국 해석해보자면,insert or update or delete문(method="update"를 뜻하)을 Executer(실행)시 를 반환해주며 args를 두가지 반환(MappedStatement, Object)해준다.
마찬가지로 select문(query)도 args를 4개 반환해준다를 뜻한다.

그럼 추가적으로 오버라이딩된 intercepter 매서드를 알아보겠다.

  • cryptoManager : CryptoManager.getInstance() 앞서 설명한 싱글톤으로 관리한다는 그 객체를 가져와서 사용한다.
  • MappedStatement : 메타정보를 담아 사용한다. 본 코드에서는 sqlId를 얻기 위해 사용한다.
  • invocation : intercepter가 작동하면 여기에 정보가 다 담긴다. 배열형식으로 불러오거나 바꿔쳐서 리턴값을 보낼 수 있다.
    • invocation.getArgs()[0]; //MAAPED_STATMENT_INDEX (MapperStatement type)
    • invocation.getArgs()[1]; //PARAMETER_INDEX (Object type)

invocation에 어떤 객체를 담고있고, 사용가능한지는 추가적으로 공부해보면 좋다.

다음으로는 코드에 조건문을 해석해보겠다.

if(!cryptoManager.isCryptoSql(sqlId)) { // 앞서 설명한 yml파일을 읽어 암복호화 작업을 해도되는 자료인지 판단.
			return invocation.proceed();
		}else if("update".equals(method)) { // 암복호화 대상이며, insert,update,delete문이면 해당 조건문 실행
			Object param = (Object) invocation.getArgs()[1]; 
			invocation.getArgs()[1] = cryptoManager.getEncryptoParams(param, sqlId); // 파라미터를 암호화해서 다시 넣어주고...
			return invocation.proceed(); // 쿼리문을 실행한다.
		}else if ("query".equals(method)) { // 암복호화 대상이며, select문이면 해당 조건문을 실행하며
			Object results = invocation.proceed(); //먼저 자료를 받아오고
			if(results instanceof List) { // 이게 여러 자료가 추출된 리스트타입이면,
				return results = cryptoManager.getDecryptResults((List<Map<String, Object>>) results, sqlId); //복호화 하여 이 리스트형 자료를 그대로 리턴해주면 된다.
			}else { // 이게 여러자료가 아닌 resultType= "String"인 한개에 자료일경우 그냥 암복호화 진행없이 결과를 출력한다.
				return results;
			}
		}else {
			return invocation.proceed(); // else문이다. 다 제껴지면 실행			
		}

설명에는 mapper.xml에 resultType가 String일때 한개만 반환되므로 암 복호화 없이 실행하게 했는데, 원한다면 또 조건주어서 관리해도 된다.

3. 정리

3.1 흐름설명

  1. Yaml().load(cryptoFile)와 CryptoManager 객체를 초기화하고, INSTANCE에 저장하여 싱글톤으로 관리한다.
  2. Mybatis Intercepter에서 현재 sql실행문이 우선 암 복호화를 진행할 컬럼이나 파라미터인지 확인하고 update, query냐에 따라서 구분하여 실행하게 한다.
  3. 암호화 과정은 invocation에서 파라미터를 불러와 getEncryptoParams에 넣어 처리를 한다. 암호화대상이면 치환되고, 아니면 그냥 있는 그대로를 return한다.
  4. 복호화 과정은 invocation으로 받은 결과값을 getDecryptResults에 넣어 처리를 하고 List<Map<String, Object>>형식으로 return해준다.
  5. 결과를 확인해보면 된다.

3.2 결과확인

그럼이제 쿼리도 만들고 페이지도 만들어서 테스트 해보면 된다.

	<select id="getUserByCrypto" resultType="Map">
		SELECT ID,PWD,NAME,ADDR FROM USERM WHERE ROWNUM <![CDATA[<=]]> 10
	</select>

	<insert id="insertUser">
		INSERT INTO USERM (USER_NUM, ID, PWD, NAME, ADDR, TEL) 
			VALUES (USER_SEQ.nextval, #{id}, #{pwd}, #{name}, #{addr}, #{tel})
	</insert>
  <%@ page language="java" contentType="text/html; charset=UTF-8"
      pageEncoding="UTF-8"%>
  <!DOCTYPE html>
  <html>
  <head>
  <meta charset="UTF-8">
  <title>유저등록</title>
  </head>
  <body>
  유저등록
  <form action="/userEnroll" method="post">
      ID :<input type="text" name="id" value="monster_1"/><br>
      PASSWORD :<input type="password" name="pwd" value="12345s"/><br>
      NAME :<input type="text" name="name" value="김감자"/><br>
      ADDR : <input type="text" name="addr" value="서울특별시 강서구"/><br>
      TEL :<input type="text" name="tel" value="0222223333"/><br>
      <input type="submit"/>
  </form>
  </body>
  </html>

잘 작동한다.



참고자료들___
(참고) [Mybatis] - Mybatis Interceptor
(참고) mybatis를 통한 Select, Insert, Delete
(참고) Mybatis > RowBounds 의 고찰
(참고) mybatis resultHandler
(참고) mybatis

profile
하이

0개의 댓글