관리자 알림 — 탐지해 놓고 알리지 않던 것을 메일로 통보 (제안서 slide15)
제안요약서 slide15 "로그 조회 및 감사 기능"의 구성요소 중 "관리자 알림".
종전에는 무결성 위반을 에러 로그로만 남겨서, 로그를 들여다보지 않으면
아무도 모르는 상태였다 — 탐지해 놓고 알리지 않으면 탐지의 의미가 없다.
· PrivacyAdminAlertService + Impl 신설
- 무결성 위반 탐지 시(해시체인 검증 배치)
- 이상행위 탐지 시(정기점검 배치, 0건인 달은 보내지 않음)
매달 "0건" 메일이 오면 곧 아무도 열지 않는다.
· 채널은 메일 — 다만 문서 근거는 없다
RFP·제안서 어디에도 알림 채널이 명시돼 있지 않다("관리자 알림" 네 글자뿐).
ACCOUNT_MNG_POLICY.ADMIN_EMAIL 이 이미 있고 휴면 사전알림이 그 주소로
메일을 보내고 있어 같은 경로를 재사용했다. 운영에서 메일 장애를 한 곳에서
확인할 수 있어야 한다는 이유도 있다. SMS 는 문자발송모듈 연동규격이
미확정이라(SFR-003 과 같은 블로커) 붙일 수 없다.
→ 채널 확정은 발주기관 협의 항목으로 남긴다.
· 메일에 개인정보 본문을 담지 않는다
건수·기간·취급자 계정ID·요약만 넣고 상세는 화면에서 보게 한다.
메일함이 개인정보 저장소가 되면 안 된다.
· 배치를 막지 않는다
발송 실패·수신주소 미설정 모두 로그만 남기고 배치는 계속한다. 다만 미설정은
WARN 으로 구분해 남긴다 — "알림이 안 왔다"와 "보낼 주소가 없다"는 다른 문제다.
검증: mvn compile EXIT=0, 로컬 반영 후 재기동 정상(빈 생성 오류 0건 —
privacyAdminAlertService/sndngMailRegistService/accountMngDAO 배선 확인),
frt/main.do 200. 실제 발송은 월 1회 배치라 이번에 실행하지 못했다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4c7e677dec
commit
bb721fe4fa
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Class Name : PrivacyAdminAlertService.java
|
||||
* Description : 개인정보보호 관리자 알림 — 개인정보보호 기능개선 사업(제안서 slide15 "관리자 알림")
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.batch;
|
||||
|
||||
import egovframework.com.cmm.privacy.batch.HashChainVerifyResultVO;
|
||||
|
||||
/**
|
||||
* 개인정보 접속기록 관련 이상 상황을 <b>관리자에게 통보</b>한다.
|
||||
*
|
||||
* <p>제안요약서 slide15 "로그 조회 및 감사 기능"의 구성요소 중 <b>관리자 알림</b>이다.
|
||||
* 종전에는 무결성 위반을 에러 로그로만 남겨서, 로그를 들여다보지 않으면 아무도 모르는
|
||||
* 상태였다 — 탐지해 놓고 알리지 않으면 탐지의 의미가 없다.</p>
|
||||
*
|
||||
* <p><b>수신자</b>는 {@code ACCOUNT_MNG_POLICY.ADMIN_EMAIL}(계정 관리 정책 화면에서 설정)이며,
|
||||
* 발송은 휴면 사전알림({@code DormantAlertService})이 쓰는 표준 메일 모듈을 그대로 재사용한다.
|
||||
* 알림 채널을 새로 만들지 않는 이유는, 운영에서 메일 발송 경로가 하나여야 장애를 한 곳에서
|
||||
* 확인할 수 있기 때문이다.</p>
|
||||
*
|
||||
* <p><b>메일에 개인정보 본문을 담지 않는다.</b> 알림에는 건수·취급자 계정ID·기간처럼 조치에
|
||||
* 필요한 최소 정보만 넣는다. 상세는 관리자가 접속기록 화면에서 확인한다 — 메일함이
|
||||
* 개인정보 저장소가 되면 안 된다.</p>
|
||||
*
|
||||
* <p><b>배치를 막지 않는다.</b> 모든 메서드는 실패해도 예외를 올리지 않는다(발송 실패로
|
||||
* 파기·검증 배치가 중단되면 본말이 전도된다). 발송 여부와 무관하게 로그는 항상 남긴다.</p>
|
||||
*/
|
||||
public interface PrivacyAdminAlertService {
|
||||
|
||||
/**
|
||||
* 접속기록 해시체인 <b>무결성 위반</b> 통보.
|
||||
*
|
||||
* @param result 검증 결과(위반이 아니면 아무것도 보내지 않는다)
|
||||
* @return 발송 건수(0 = 미발송)
|
||||
*/
|
||||
int alertIntegrityViolation(HashChainVerifyResultVO result);
|
||||
|
||||
/**
|
||||
* <b>이상행위 탐지</b> 통보(정기점검 배치가 월 1회 호출).
|
||||
*
|
||||
* @param periodFrom 집계 시작일시
|
||||
* @param periodTo 집계 종료일시
|
||||
* @param summary 탐지 요약 문자열({@code AbnormalAccessDetectionService.summarize})
|
||||
* @param detected 탐지 합계(0이면 보내지 않는다 — 조용한 달에는 메일도 조용해야 한다)
|
||||
* @return 발송 건수(0 = 미발송)
|
||||
*/
|
||||
int alertAbnormalAccess(String periodFrom, String periodTo, String summary, int detected);
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Class Name : PrivacyAdminAlertServiceImpl.java
|
||||
* Description : 개인정보보호 관리자 알림 구현 — 개인정보보호 기능개선 사업(제안서 slide15)
|
||||
*
|
||||
* Modification Information
|
||||
*
|
||||
* 수정일 수정자 수정내용
|
||||
* ------- -------- ---------------------------
|
||||
* 2026.08.27 개인정보보호 최초 생성
|
||||
*
|
||||
* @since 2026. 08. 27
|
||||
* @version 1.0
|
||||
*/
|
||||
package egovframework.com.cmm.privacy.batch;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import egovframework.com.cmm.privacy.account.AccountMngDAO;
|
||||
import egovframework.com.cmm.privacy.account.AccountMngPolicyVO;
|
||||
import egovframework.com.cop.ems.service.EgovSndngMailRegistService;
|
||||
import egovframework.com.cop.ems.service.SndngMailVO;
|
||||
|
||||
/**
|
||||
* {@link PrivacyAdminAlertService} 구현.
|
||||
*
|
||||
* <p>발송 경로는 휴면 사전알림({@code DormantAlertServiceImpl.sendMail})과 동일하다 —
|
||||
* {@code SndngMailVO} 를 만들어 표준 메일 모듈에 적재하면 발송 배치가 실제 전송을 맡는다.</p>
|
||||
*/
|
||||
@Service("privacyAdminAlertService")
|
||||
public class PrivacyAdminAlertServiceImpl implements PrivacyAdminAlertService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PrivacyAdminAlertServiceImpl.class);
|
||||
|
||||
/** 메일 제목 접두 — 관리자 메일함에서 걸러 보기 쉽게 고정한다. */
|
||||
private static final String SUBJECT_PREFIX = "[문화품앗이/개인정보] ";
|
||||
|
||||
/** 발신자 계정 — 기존 알림과 동일. */
|
||||
private static final String SENDER = "webmaster";
|
||||
|
||||
@Resource(name = "accountMngDAO")
|
||||
private AccountMngDAO accountMngDAO;
|
||||
|
||||
@Resource(name = "sndngMailRegistService")
|
||||
private EgovSndngMailRegistService sndngMailRegistService;
|
||||
|
||||
@Override
|
||||
public int alertIntegrityViolation(HashChainVerifyResultVO result) {
|
||||
if (result == null || result.isOk()) {
|
||||
return 0;
|
||||
}
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("개인정보 접속기록의 해시체인 무결성 검증에서 불일치가 발견되었습니다.\n\n");
|
||||
body.append("- 검증 일시 : ").append(nvl(result.getVerifiedAt())).append('\n');
|
||||
body.append("- 검증 대상 : ").append(result.getTotalCount()).append("건\n");
|
||||
body.append("- 최초 불일치 : 기록번호 ").append(result.getMismatchLogId()).append('\n');
|
||||
body.append('\n');
|
||||
body.append("접속기록은 각 행이 이전 행의 해시를 물고 있어, 중간의 어떤 행이 바뀌면\n");
|
||||
body.append("그 뒤가 전부 어긋납니다. 위 기록번호부터가 검증에 실패한 지점입니다.\n\n");
|
||||
body.append("확인 경로: 시스템관리 > 권한관리 > 접속기록 조회 및 감사\n");
|
||||
body.append("※ 이 메일에는 개인정보 본문이 포함되어 있지 않습니다. 상세는 화면에서 확인하십시오.\n");
|
||||
|
||||
return send("접속기록 무결성 위반 탐지", body.toString(), "무결성 위반");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int alertAbnormalAccess(String periodFrom, String periodTo, String summary, int detected) {
|
||||
if (detected <= 0) {
|
||||
// 조용한 달에는 메일도 조용해야 한다. 매달 "0건" 메일이 오면 곧 아무도 열지 않는다.
|
||||
return 0;
|
||||
}
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("개인정보 접속기록 정기점검에서 이상행위가 탐지되었습니다.\n\n");
|
||||
body.append("- 집계 기간 : ").append(nvl(periodFrom)).append(" ~ ").append(nvl(periodTo)).append('\n');
|
||||
body.append("- 탐지 합계 : ").append(detected).append("건\n");
|
||||
body.append("- 탐지 요약 : ").append(nvl(summary)).append('\n');
|
||||
body.append('\n');
|
||||
body.append("탐지되었다는 것이 곧 위반은 아닙니다. 업무상 정당한 대량조회나 야간작업이\n");
|
||||
body.append("있을 수 있으므로, 아래 화면에서 원본 기록을 확인한 뒤 판단하십시오.\n\n");
|
||||
body.append("확인 경로: 시스템관리 > 권한관리 > 접속기록 조회 및 감사\n");
|
||||
body.append(" (목록 오른쪽 '감사' 칸의 이상/점검 표시로 해당 기록을 찾을 수 있습니다)\n");
|
||||
body.append("※ 이 메일에는 정보주체의 개인정보가 포함되어 있지 않습니다.\n");
|
||||
|
||||
return send("접속기록 이상행위 탐지", body.toString(), "이상행위 탐지");
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 메일 적재. 수신자 미설정이면 보내지 않되 <b>로그는 남긴다</b> —
|
||||
* "알림이 안 왔다"와 "보낼 주소가 없다"를 구분할 수 있어야 한다.
|
||||
*/
|
||||
private int send(String subject, String body, String kind) {
|
||||
String adminEmail = null;
|
||||
try {
|
||||
AccountMngPolicyVO policy = accountMngDAO.selectPolicy();
|
||||
if (policy != null) {
|
||||
adminEmail = policy.getAdminEmail();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("[개인정보/알림] 정책 조회 실패 — {} 알림을 보내지 못했습니다: {}", kind, e.toString());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (adminEmail == null || adminEmail.trim().length() == 0) {
|
||||
LOG.warn("[개인정보/알림] 관리자 수신 이메일 미설정(ACCOUNT_MNG_POLICY.ADMIN_EMAIL) —"
|
||||
+ " {} 알림을 보내지 못했습니다. 계정 관리 정책 화면에서 주소를 등록하십시오.", kind);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
SndngMailVO vo = new SndngMailVO();
|
||||
vo.setDsptchPerson(SENDER);
|
||||
vo.setRecptnPerson(adminEmail.trim());
|
||||
vo.setSj(SUBJECT_PREFIX + subject);
|
||||
vo.setEmailCn(body);
|
||||
vo.setAtchFileId("");
|
||||
sndngMailRegistService.insertSndngMail(vo);
|
||||
LOG.info("[개인정보/알림] {} 알림 발송 적재 완료 → {}", kind, adminEmail.trim());
|
||||
return 1;
|
||||
} catch (Exception e) {
|
||||
// 발송 실패로 파기·검증 배치가 멈추면 본말이 전도된다.
|
||||
LOG.error("[개인정보/알림] {} 알림 발송 실패 — 배치는 계속합니다: {}", kind, e.toString());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private String nvl(Object v) {
|
||||
return v == null ? "-" : String.valueOf(v);
|
||||
}
|
||||
}
|
||||
@ -105,6 +105,10 @@ public class PrivacyBatchServiceImpl implements PrivacyBatchService {
|
||||
@Resource(name = "accessLogDownloadHistoryDAO")
|
||||
private AccessLogDownloadHistoryDAO accessLogDownloadHistoryDAO;
|
||||
|
||||
/** 제안서 slide15 "관리자 알림" — 무결성 위반·이상행위 탐지 통보. */
|
||||
@Resource(name = "privacyAdminAlertService")
|
||||
private PrivacyAdminAlertService privacyAdminAlertService;
|
||||
|
||||
/** 정기점검 리포트 취급자별 처리현황 요약 최대 표기 행 수. */
|
||||
private static final int HANDLER_SUMMARY_TOP = 20;
|
||||
|
||||
@ -145,12 +149,36 @@ public class PrivacyBatchServiceImpl implements PrivacyBatchService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 무결성 위반 즉시 관리자 통보.
|
||||
* TODO: 실제 통보 채널(관리자 메일/SMS/알림함 #49) 연동. 현재는 에러 로그로 남긴다.
|
||||
* 무결성 위반 즉시 관리자 통보(제안서 slide15 "관리자 알림").
|
||||
*
|
||||
* <p>에러 로그는 그대로 남긴다 — 메일이 막혀 있어도 흔적은 서버에 있어야 한다.
|
||||
* 실제 통보는 {@link PrivacyAdminAlertService} 가 관리자 메일로 보낸다.</p>
|
||||
*/
|
||||
private void notifyAdmin(HashChainVerifyResultVO result) {
|
||||
LOG.error("[개인정보/#40] 관리자 통보 필요 — 무결성 위반 LOG_ID=" + result.getMismatchLogId()
|
||||
+ " (검증일시 " + result.getVerifiedAt() + "). TODO: 메일/SMS/알림함 연동.");
|
||||
LOG.error("[개인정보/#40] 관리자 통보 — 무결성 위반 LOG_ID=" + result.getMismatchLogId()
|
||||
+ " (검증일시 " + result.getVerifiedAt() + ").");
|
||||
privacyAdminAlertService.alertIntegrityViolation(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간 내 이상행위 탐지 합계. 알림 발송 여부를 정하는 값이라 요약 문자열을 파싱하지 않고
|
||||
* 탐지 결과의 건수를 그대로 더한다(문자열 파싱은 문구가 바뀌면 조용히 어긋난다).
|
||||
* 집계에 실패하면 0 을 돌려 알림을 보내지 않는다 — 근거 없이 알리지 않는다.
|
||||
*/
|
||||
private int countDetected(Map<String, Object> param) {
|
||||
try {
|
||||
List<egovframework.com.cmm.privacy.auditlog.AbnormalAccessResultVO> results =
|
||||
abnormalAccessDetectionService.detect(param.get("fromDt").toString(),
|
||||
param.get("toDt").toString());
|
||||
int sum = 0;
|
||||
for (int i = 0; results != null && i < results.size(); i++) {
|
||||
sum += results.get(i).getCount();
|
||||
}
|
||||
return sum;
|
||||
} catch (Exception e) {
|
||||
LOG.error("[개인정보/알림] 이상행위 건수 집계 실패 - 알림 생략: " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -234,6 +262,18 @@ public class PrivacyBatchServiceImpl implements PrivacyBatchService {
|
||||
report.setDownloadCnt(downloadCnt);
|
||||
|
||||
inspectionReportDAO.insertInspectionReport(report);
|
||||
|
||||
// [제안서 slide15 "관리자 알림"] 이상행위가 탐지된 달에만 관리자에게 통보한다.
|
||||
// 리포트를 적재한 뒤에 보내는 이유는, 메일을 받은 관리자가 화면에서 바로 근거를
|
||||
// 확인할 수 있어야 하기 때문이다(먼저 보내면 리포트가 아직 없을 수 있다).
|
||||
try {
|
||||
int detected = countDetected(param);
|
||||
privacyAdminAlertService.alertAbnormalAccess(
|
||||
report.getPeriodFrom(), report.getPeriodTo(), abnormalSummary, detected);
|
||||
} catch (Exception e) {
|
||||
LOG.error("[개인정보/알림] 이상행위 알림 처리 오류 - 리포트는 정상 생성됨: " + e.getMessage());
|
||||
}
|
||||
|
||||
LOG.info("[개인정보/#43] 정기점검 리포트 생성 — " + rptYm + " 총 " + totalCnt + "건(실패 " + failCnt
|
||||
+ "), 무결성=" + report.getIntegrityResult() + ", 취급자 "
|
||||
+ (handlers == null ? 0 : handlers.size()) + "종.");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user