Mybatis Interceptor는 XML 파일에 작성한 SQL의 메타정보들을 얻을 수 있고 파라미터를 가공해서 쿼리를 실행할 수 있다.
실제 운영에서 회원 관리를 위한 자료를 Insert한다고 하자. 유저들은 마음에 안들겠지만 개인들의 주민번호(인증서)를 이용해서 회원가입에 이용하기도 하고, 전화번호, 카드정보를 DB에 담아두기도 한다. 그렇다고 이런자료를 직원들이 다 볼 수 있게 평문으로 저장하는건 바람직하지 않다.
그래서 그러한 민감한 자료를 저장할 땐 암호화하여 저장하고, 유저가 웹페이지를 서칭 할 때는 다시 평문으로 복호화하여 이용하는데 불편함이 없게 해야한다.
첫번째로 암호화 관련 유틸클래스를 만들어 이 객체를 통해 암 복호화 가능한 기능을 구현하자.
암복호화 관련 알고리즘은 '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;
}
}
CryptoManager를 static 변수로 선언하였고, @Component에 의해 생성된 객체가 @PostConstruct를 통해 INSTANCE에 저장되었다.
@PostConstruct는 생성자처럼 @Bean이 첫 로드된 직후 한번 실행된다. 이제 이 녀석은 프로젝트에서 어느 공간에서나 @Resource로 불러오기만하면 싱글톤으로 사용가능하다. (생각해보니 생성자에 담아서 그냥 사용해도 되겠다...)
추가로 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 설명은 생략하겠다.
Sql 쿼리문을 실행하면
를 스캔하여 중간에 가로채서 원하는 작업을 하게금 명령할 수 있게된다.
아래는 순서대로 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);
}
}
@Signature에 대한 설명이다.
속성값 | 설명 |
---|---|
type | Executer라는 인터페이스는 Mybatis의 XML 파일에 작성된 SQL을 실행한다. 해당 인터페이스 내부를 보면 각 mybatis method 시그니처 정보를 볼 수 있다 type은 Executer말고도 정말많은데, "open Declaration"을 클릭해서 해당 .jar안에 컴파일된 interface들을 참고하면 다양한 type와 오버로딩된 args들을 확인가능하다. |
method | insert or update or delete가 실행되면 Executer의 update 라는 메소드를 호출하며 select는 query 라는 메소드를 호출한다. |
args | mapper에 작성된 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일때 한개만 반환되므로 암 복호화 없이 실행하게 했는데, 원한다면 또 조건주어서 관리해도 된다.
그럼이제 쿼리도 만들고 페이지도 만들어서 테스트 해보면 된다.
<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