- 사용자 검색 시 암호화 필드 처리 로직 개선: 부분검색 제거 및 정확일치 검색 추가.
eapim-admin CI / build (push) Has been cancelled

- UI에 암호화 필드 검색 제한 안내 추가.
- 개인정보 마스킹 규칙 확대: 이름/ID/전화번호 모두 적용.
This commit is contained in:
Rinjae
2026-06-22 19:21:08 +09:00
parent fe201b88cd
commit c521835633
3 changed files with 40 additions and 12 deletions
@@ -398,15 +398,16 @@
<td><input type="text" name="searchUserName"></td>
<th style="width:10%; min-width:100px;">
<%= localeMessage.getString("portalUser.userId") %>
<%= localeMessage.getString("portalUser.userId") %><span style="color:#dc3545;">(*)</span>
</th>
<td><input type="text" name="searchUserId" value="${param.searchUserId}"></td>
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %>
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %><span style="color:#dc3545;">(*)</span>
</th>
<td><input type="text" name="searchMobileNumber" value="${param.searchMobileNumber}"></td>
</tr>
</tbody>
</table>
<div style="color:#dc3545; font-size:12px; margin:4px 2px;">(*) 암호화 항목 · 부분검색 불가(전체 일치)</div>
<table id="grid"></table>
<div id="pager"></div>
@@ -38,7 +38,22 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
}
if (StringUtils.isNotBlank(portalUserUISearch.getSearchUserId())) {
predicate.and(qPortalUser.loginId.containsIgnoreCase(portalUserUISearch.getSearchUserId()));
// login_id / email_addr 는 PersonalDataEncryptConverter 로 암호화 저장되는 컬럼이다.
// 암호화는 부분문자열을 보존하지 않으므로 LIKE/contains 검색은 절대 매칭되지 않는다.
// 결정적(deterministic) 암호화라 정확일치(eq)만 가능하므로, login_id 와 email_addr 둘 다 정확일치로 OR 검색한다.
String searchUserId = portalUserUISearch.getSearchUserId().trim();
BooleanBuilder loginIdPredicate = new BooleanBuilder()
.or(qPortalUser.loginId.eq(searchUserId))
.or(qPortalUser.emailAddr.eq(searchUserId));
// 이메일 형식 값(예: user+1@unit-test.com)은 form-urlencoded 디코딩 과정에서
// '+' 가 공백으로 치환되어 전달될 수 있다(프록시/컨테이너 단계라 앱에서 인코딩만으로 막기 어려움).
// login_id/email 에는 공백이 올 수 없으므로, 공백 포함 검색어는 '+' 치환 값도 함께 매칭한다.
if (searchUserId.indexOf(' ') >= 0) {
String plusRestored = searchUserId.replace(' ', '+');
loginIdPredicate.or(qPortalUser.loginId.eq(plusRestored))
.or(qPortalUser.emailAddr.eq(plusRestored));
}
predicate.and(loginIdPredicate);
}
if (StringUtils.isNotBlank(portalUserUISearch.getSearchUserName())) {
@@ -49,7 +64,9 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
predicate.and(qPortalUser.portalOrg.id.eq(portalUserUISearch.getSearchOrgId()));
}
if (StringUtils.isNotBlank(portalUserUISearch.getSearchMobileNumber())) {
predicate.and(qPortalUser.mobileNumber.contains(portalUserUISearch.getSearchMobileNumber()));
// mobile_number 도 암호화 저장 컬럼이라 부분문자열이 보존되지 않는다.
// LIKE/contains 는 매칭되지 않으므로 정확일치(eq)로만 검색한다.
predicate.and(qPortalUser.mobileNumber.eq(portalUserUISearch.getSearchMobileNumber().trim()));
}
if (StringUtils.isNotBlank(portalUserUISearch.getSearchRoleCode())) {
@@ -44,7 +44,7 @@ public class PortalUserManService extends BaseService {
/** PortalProperty 그룹명 */
private static final String PORTAL_PROPERTY_GROUP = "Portal";
/** 사용자 별 휴대폰 번호 중복 허용 프로퍼티 키 (Y:허용, N:허용안함) */
/** 사용자 별 휴대폰 번호 중복 허용 프로퍼티 키 (true:허용, false:허용안함, 기본 false) */
private static final String PROP_MOBILE_DUPLICATE_ALLOW = "user.mobile.duplicate.allow";
private final PortalUserService portalUserService;
@@ -66,10 +66,18 @@ public class PortalUserManService extends BaseService {
return portalUser.map(entity -> {
PortalUserUI ui = convertToUIWithMaskOption(entity, false);
// 목록의 휴대폰번호는 항상 마스킹 (중복 조회 모드에서만 컬럼이 노출되며 개인정보 보호)
if (ui != null && StringUtils.isNotBlank(ui.getMobileNumber())) {
// 목록의 개인정보(사용자명/이메일아이디/휴대폰번호)는 항상 마스킹하여 노출 (상세 조회에서만 마스킹 해제)
if (ui != null) {
if (StringUtils.isNotBlank(ui.getUserName())) {
ui.setUserName(MaskingUtils.maskName(ui.getUserName()));
}
if (StringUtils.isNotBlank(ui.getLoginId())) {
ui.setLoginId(MaskingUtils.maskEmailId(ui.getLoginId()));
}
if (StringUtils.isNotBlank(ui.getMobileNumber())) {
ui.setMobileNumber(MaskingUtils.maskPhoneNumber(ui.getMobileNumber()));
}
}
if (entity.getPortalOrg() != null) {
setPortalOrgUI(entity, ui);
@@ -80,7 +88,7 @@ public class PortalUserManService extends BaseService {
/**
* 사용자 별 휴대폰 번호 중복 허용 프로퍼티(Portal/user.mobile.duplicate.allow)를 확인한다.
* 프로퍼티가 없으면 기본값 'N'(허용안함)으로 생성하며, '허용안함'일 때 중복 휴대폰번호 사용자 수를 함께 반환한다.
* 프로퍼티가 없으면 기본값 'false'(허용안함)으로 생성하며, '허용안함'일 때 중복 휴대폰번호 사용자 수를 함께 반환한다.
*
* @return enabled(boolean): 중복 조회 기능 활성 여부, count(long): 중복 휴대폰번호 사용자 수
*/
@@ -88,10 +96,12 @@ public class PortalUserManService extends BaseService {
String allowValue = portalPropertyService.getOrCreateProperty(
PORTAL_PROPERTY_GROUP,
PROP_MOBILE_DUPLICATE_ALLOW,
"N",
"사용자 별 휴대폰 번호 중복 허용 여부 (Y:허용, N:허용안함). 허용안함일 때 사용자 관리 목록에 중복 휴대폰번호 사용자 조회 기능을 노출");
"false",
"사용자 별 휴대폰 번호 중복 허용 여부 (true:허용, false:허용안함). 허용안함일 때 사용자 관리 목록에 중복 휴대폰번호 사용자 조회 기능을 노출");
boolean checkEnabled = "N".equalsIgnoreCase(allowValue);
// 레거시 Y/N 값도 허용으로 함께 인식 (true/Y → 허용, 그 외 → 허용안함)
boolean allowed = "true".equalsIgnoreCase(allowValue) || "Y".equalsIgnoreCase(allowValue);
boolean checkEnabled = !allowed;
long duplicateUserCount = checkEnabled ? portalUserService.countDuplicateMobileUsers() : 0L;
Map<String, Object> info = new HashMap<>();