Damo 적용 준비 코드
- KjbNotRnnoJpaAttributeConverter 삭제 및 PersonalDataEncryptConverter 추가 - damo-manager 지원을 위한 JSP 추가 및 레거시 암호화 마이그레이션 컨트롤러 구현 - 기존 엔티티 암호화 로직 변경 및 build.gradle에 damo-manager 의존성 추가
This commit is contained in:
+194
@@ -0,0 +1,194 @@
|
||||
package com.eactive.apim.portal.common.migration;
|
||||
|
||||
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.sql.DataSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* [임시] 레거시 평문 데이터를 {@link PersonalDataEncryptConverter} 규칙으로 일괄 정규화(암호화)하는 운영 도구.
|
||||
*
|
||||
* <p>배경: {@code @Convert} 컬럼이 평문으로 저장된 레거시 행은, derived query가 검색값을 암호화하면서
|
||||
* 평문 DB값과 불일치해 검색/로그인이 실패한다. 컨버터는 읽기에서 평문/암호문을 자동 구분하고
|
||||
* 쓰기에서 무조건 인코딩하므로, {@code convertToDatabaseColumn(convertToEntityAttribute(x))}는
|
||||
* 평문→인코딩, 인코딩→동일값(멱등)으로 정규화된다. 이 값이 기존과 다를 때만 UPDATE 한다.</p>
|
||||
*
|
||||
* <p>보안: 오직 127.0.0.1(localhost)에서 직접 호출한 요청만 허용한다. 기본은 dry-run(미변경)이며,
|
||||
* 실제 실행은 {@code dryRun=false}를 명시해야 한다. 작업 완료 후 이 클래스는 제거한다.</p>
|
||||
*
|
||||
* <pre>
|
||||
* # 미리보기(변경 안 함)
|
||||
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy'
|
||||
* # 실제 실행 (PII 컬럼)
|
||||
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false'
|
||||
* # audit 컬럼(created_by/last_modified_by, 15개 테이블)까지 포함
|
||||
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false&includeAudit=true'
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/internal/migration")
|
||||
public class LegacyEncryptionMigrationController {
|
||||
|
||||
/** PII 직접 컬럼 (로그인/검색에 직접 영향) */
|
||||
private static final List<TargetTable> PII_TARGETS = Arrays.asList(
|
||||
new TargetTable("PTL_USER", Arrays.asList("login_id", "email_addr", "phone_number", "mobile_number")),
|
||||
new TargetTable("PTL_MESSAGE_REQUEST", Arrays.asList("email", "phone")),
|
||||
new TargetTable("tseairm02", Arrays.asList("cphnno", "emad")),
|
||||
new TargetTable("PTL_USER_LOG", Arrays.asList("login_id")),
|
||||
new TargetTable("PTL_TWO_FACTOR_AUTH", Arrays.asList("recipient"))
|
||||
);
|
||||
|
||||
/** Auditable(@MappedSuperclass) 상속 테이블의 감사 컬럼 (옵션) */
|
||||
private static final List<String> AUDIT_TABLES = Arrays.asList(
|
||||
"PTL_USER", "ptl_faq", "ptl_org",
|
||||
"DJB_APISTATUS_INCIDENT", "DJB_APISTATUS_INCIDENT_TIMELINE", "DJB_APISTATUS_INCIDENT_API",
|
||||
"ptl_file", "PTL_MESSAGE_TEMPLATE", "ptl_notice", "ptl_terms",
|
||||
"ptl_user_privacy_policy_agreement", "ptl_approval_line",
|
||||
"PTL_INQUIRY_COMMENT", "ptl_inquiry", "ptl_partnership_application"
|
||||
);
|
||||
private static final List<String> AUDIT_COLUMNS = Arrays.asList("created_by", "last_modified_by");
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final PersonalDataEncryptConverter converter = new PersonalDataEncryptConverter();
|
||||
|
||||
public LegacyEncryptionMigrationController(@Qualifier("portalDataSource") DataSource emsDataSource) {
|
||||
// EMS(EMSAPP) 스키마 데이터소스. 컨버터 적용 테이블은 모두 EMS에 존재한다.
|
||||
this.jdbcTemplate = new JdbcTemplate(emsDataSource);
|
||||
}
|
||||
|
||||
@PostMapping("/encrypt-legacy")
|
||||
@Transactional("transactionManager")
|
||||
public Map<String, Object> encryptLegacy(HttpServletRequest request,
|
||||
@RequestParam(defaultValue = "true") boolean dryRun,
|
||||
@RequestParam(defaultValue = "false") boolean includeAudit) {
|
||||
assertLocalOnly(request);
|
||||
assertNotBypass();
|
||||
|
||||
List<TargetTable> targets = new ArrayList<>(PII_TARGETS);
|
||||
if (includeAudit) {
|
||||
for (String table : AUDIT_TABLES) {
|
||||
targets.add(new TargetTable(table, AUDIT_COLUMNS));
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
int totalChanged = 0;
|
||||
for (TargetTable target : targets) {
|
||||
for (String column : target.columns) {
|
||||
Map<String, Object> r = processColumn(target.table, column, dryRun);
|
||||
results.add(r);
|
||||
totalChanged += (int) r.get("changed");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("mode", dryRun ? "dry-run (변경 없음)" : "executed");
|
||||
response.put("includeAudit", includeAudit);
|
||||
response.put("totalChanged", totalChanged);
|
||||
response.put("results", results);
|
||||
log.info("[레거시 암호화 마이그레이션] mode={} includeAudit={} totalChanged={}",
|
||||
dryRun ? "dry-run" : "executed", includeAudit, totalChanged);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 (테이블, 컬럼)의 고유값을 정규화하고, 값이 바뀌는 경우에만 UPDATE.
|
||||
*/
|
||||
private Map<String, Object> processColumn(String table, String column, boolean dryRun) {
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("table", table);
|
||||
r.put("column", column);
|
||||
|
||||
List<String> values;
|
||||
try {
|
||||
values = jdbcTemplate.queryForList(
|
||||
"SELECT DISTINCT " + column + " FROM " + table + " WHERE " + column + " IS NOT NULL",
|
||||
String.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[마이그레이션] 조회 실패 table={} column={} : {}", table, column, e.toString());
|
||||
r.put("distinct", 0);
|
||||
r.put("changed", 0);
|
||||
r.put("error", e.getMessage());
|
||||
return r;
|
||||
}
|
||||
|
||||
int changed = 0;
|
||||
for (String value : values) {
|
||||
String normalized;
|
||||
try {
|
||||
// 평문 → 인코딩, 이미 인코딩 → 동일값 (멱등)
|
||||
normalized = converter.convertToDatabaseColumn(converter.convertToEntityAttribute(value));
|
||||
} catch (Exception e) {
|
||||
log.warn("[마이그레이션] 정규화 실패 table={} column={} : {}", table, column, e.toString());
|
||||
continue;
|
||||
}
|
||||
if (normalized != null && !normalized.equals(value)) {
|
||||
if (!dryRun) {
|
||||
jdbcTemplate.update(
|
||||
"UPDATE " + table + " SET " + column + " = ? WHERE " + column + " = ?",
|
||||
normalized, value);
|
||||
}
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
|
||||
r.put("distinct", values.size());
|
||||
r.put("changed", changed);
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* damo-manager 가 bypass 모드면 실행 자체를 거부한다.
|
||||
* bypass 에서는 {@code encrypt} 가 무변환(원문 그대로)이라 정규화가 평문→평문 no-op 이 되어
|
||||
* 마이그레이션이 의미가 없고, "완료"로 오인될 위험이 있다. real/fake 모드로 기동 후 실행해야 한다.
|
||||
*/
|
||||
private void assertNotBypass() {
|
||||
if (converter.isBypassMode()) {
|
||||
log.warn("[마이그레이션] bypass 모드 실행 거부 — 암복호화가 무변환이라 마이그레이션이 무의미함");
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"damo-manager 가 bypass 모드입니다. 암복호화가 무변환(원문 그대로)이라 마이그레이션이 무의미하므로 거부합니다. "
|
||||
+ "real/fake 모드(-Ddamo-manager.enabled=true)로 기동한 뒤 실행하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 127.0.0.1(localhost) 직접 호출만 허용. 프록시 경유(X-Forwarded-For 존재) 요청은 거부한다.
|
||||
*/
|
||||
private void assertLocalOnly(HttpServletRequest request) {
|
||||
String remote = request.getRemoteAddr();
|
||||
boolean localAddr = "127.0.0.1".equals(remote)
|
||||
|| "0:0:0:0:0:0:0:1".equals(remote)
|
||||
|| "::1".equals(remote);
|
||||
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
|
||||
if (!localAddr || viaProxy) {
|
||||
log.warn("[마이그레이션] 비로컬 접근 차단 remoteAddr={} xff={}", remote, request.getHeader("X-Forwarded-For"));
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "localhost(127.0.0.1) 직접 호출만 허용됩니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TargetTable {
|
||||
final String table;
|
||||
final List<String> columns;
|
||||
|
||||
TargetTable(String table, List<String> columns) {
|
||||
this.table = table;
|
||||
this.columns = columns;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user