Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 13 additions & 27 deletions src/main/java/life/mosu/mosuserver/infra/kmc/KmcCryptoManager.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
package life.mosu.mosuserver.infra.kmc;

import com.icert.comm.secu.IcertSecuManager;
import java.text.SimpleDateFormat;
import java.util.Date;
import life.mosu.mosuserver.infra.kmc.dto.KmcCertRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@Component
@RequiredArgsConstructor
public class KmcCryptoManager {

private final KmcProperties kmcProperties;
private final IcertSecuManager secuManager;

private static final String EXTEND_VAR = "0000000000000000";
private static final String DELIMITER = "/";
private final KmcProperties kmcProperties;
private final IcertSecuManager secuManager;

/**
* KMC 본인인증 요청 데이터(tr_cert)를 암호화
*/
public String encryptRequestData(KmcCertRequest request, String certNum) {
String currentDate = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String plusInfo = request.serviceTerm();

String rawData = String.join(DELIMITER,
kmcProperties.getCpId(), kmcProperties.getUrlCode(), certNum, currentDate, "M",
String rawData = String.join(
DELIMITER,
kmcProperties.getCpId(),
kmcProperties.getUrlCode(),
certNum,
currentDate,
"M",
"", "", "", "", "", "",
plusInfo, EXTEND_VAR
);
Expand All @@ -38,38 +39,23 @@ public String encryptRequestData(KmcCertRequest request, String certNum) {
return secuManager.getEnc(enc_tr_cert_1 + DELIMITER + hmacMsg + DELIMITER + EXTEND_VAR, "");
}

/**
* KMC 응답 데이터(rec_cert)를 복호화하고 무결성을 검증
* @return 2차 복호화까지 완료된 최종 사용자 정보 문자열
*/
public String decryptResponseData(String recCert) {
try {
// 1차 복호화
String firstDecrypted = decrypt(recCert);

// 데이터와 HMAC(무결성 검증 값) 분리
int firstIdx = firstDecrypted.indexOf(DELIMITER);
String encPara = firstDecrypted.substring(0, firstIdx);
String receivedHmac = firstDecrypted.substring(firstIdx + 1, firstDecrypted.lastIndexOf(DELIMITER));

// 무결성 검증
String receivedHmac = firstDecrypted.substring(firstIdx + 1,
firstDecrypted.lastIndexOf(DELIMITER));
String generatedHmac = secuManager.getMsg(encPara);
if (!generatedHmac.equals(receivedHmac)) {
throw new SecurityException("KMC 데이터의 위변조가 의심됩니다.");
}

// 2차 복호화하여 최종 데이터 반환
return decrypt(encPara);
} catch (Exception e) {
throw new RuntimeException("KMC 인증 결과를 처리하는 중 오류가 발생했습니다.", e);
}
}
Comment on lines 42 to 57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Javadocs and explanatory inline comments for decryptResponseData have been removed. While the code might be clear now, this documentation is very valuable for future maintainers to understand the multi-step decryption and HMAC verification process. Please consider restoring the comments and Javadoc to improve code maintainability.


/**
* KMC에서 받은 암호화된 데이터를 복호화
* @param encryptedData KMC로부터 받은 암호화된 데이터
* @return 복호화된 문자열
*/
public String decrypt(String encryptedData) {
return secuManager.getDec(encryptedData, "");
}
Expand Down
134 changes: 43 additions & 91 deletions src/main/java/life/mosu/mosuserver/infra/kmc/KmcDataMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,100 +6,52 @@
import life.mosu.mosuserver.infra.kmc.dto.KmcUserInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base32;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Slf4j
@Component
@RequiredArgsConstructor
public class KmcDataMapper {

private final KmcCryptoManager kmcCryptoManager;
private final OneTimeTokenProvider tokenProvider;

private static final String DELIMITER = "/";
private static final int MIN_FIELD_COUNT = 18;

// KMC 응답 필드 인덱스를 상수로 정의하여 매직 넘버 제거
private static final int CERT_NUM_INDEX = 0;
private static final int DATE_INDEX = 1;
private static final int CI_INDEX = 2;
private static final int PHONE_NO_INDEX = 3;
private static final int PHONE_CORP_INDEX = 4;
private static final int BIRTH_INDEX = 5;
private static final int GENDER_INDEX = 6;
private static final int NAME_INDEX = 8;
private static final int RESULT_SUCCESS_INDEX = 9; // 성공여부
private static final int PLUS_INFO_INDEX = 16;
private static final int DI_INDEX = 17;

/**
* 복호화된 KMC 최종 데이터 문자열을 KmcUserInfo DTO로 변환합니다.
*/
public KmcUserInfo mapToUserInfo(String finalDecryptedData) {
String[] tokens = finalDecryptedData.split(DELIMITER, -1);
if (tokens.length < MIN_FIELD_COUNT) {
throw new CustomRuntimeException(ErrorCode.INVALID_TOKEN);
}

if (!tokens[RESULT_SUCCESS_INDEX].equals("Y")) {
throw new CustomRuntimeException(ErrorCode.VERIFICATION_FAILED);
}

logDecryptedData(tokens);

String name = tokens[NAME_INDEX];
String birth = tokens[BIRTH_INDEX];
String phoneNo = tokens[PHONE_NO_INDEX];
String gender = tokens[GENDER_INDEX];

String servieTerm = tokens[PLUS_INFO_INDEX];
String signUpToken = tokenProvider.generateOneTimeToken(tokens[CERT_NUM_INDEX]);

return KmcUserInfo.of(name, birth, phoneNo, gender, servieTerm, signUpToken);
}

// production 시 삭제
private void logDecryptedData(String[] tokens) {
if (!log.isInfoEnabled()) return;

String ci = decryptCiDiValue(tokens[CI_INDEX]);
String di = decryptCiDiValue(tokens[DI_INDEX]);

log.info("[KMCIS] Parsed User Information:");
log.info(" - CertNum: {}", tokens[CERT_NUM_INDEX]);
log.info(" - Name: {}", tokens[NAME_INDEX]);
log.info(" - PhoneNo: {}", tokens[PHONE_NO_INDEX]);
log.info(" - Decrypted CI: {}", ci);
log.info(" - Decrypted DI: {}", di);
log.info(" - PlusInfo: {}", tokens[PLUS_INFO_INDEX]);

logPlusInfoDetail(tokens[PLUS_INFO_INDEX]);
@Slf4j
@Component
@RequiredArgsConstructor
public class KmcDataMapper {

private static final String DELIMITER = "/";
private static final int MIN_FIELD_COUNT = 18;
private static final int CERT_NUM_INDEX = 0;
private static final int PHONE_NO_INDEX = 3;
private static final int BIRTH_INDEX = 5;
private static final int GENDER_INDEX = 6;
private static final int NAME_INDEX = 8;
private static final int RESULT_SUCCESS_INDEX = 9; // 성공여부
private static final int PLUS_INFO_INDEX = 16;
private final OneTimeTokenProvider tokenProvider;

/**
* 복호화된 KMC 최종 데이터 문자열을 KmcUserInfo DTO로 변환합니다.
*/
public KmcUserInfo mapToUserInfo(String finalDecryptedData) {
String[] tokens = finalDecryptedData.split(DELIMITER, -1);
if (tokens.length < MIN_FIELD_COUNT) {
throw new CustomRuntimeException(ErrorCode.INVALID_TOKEN);
}

// production 시 삭제
private void logPlusInfoDetail(String plusInfo) {
if (!StringUtils.hasText(plusInfo) || !plusInfo.contains("@")) return;

String[] parts = plusInfo.split("@", -1);
if (parts.length == 3) {
byte[] decodedBytes = new Base32().decode(parts[1]);
String decodedPassword = new String(decodedBytes);
log.info(" - Parsed PlusInfo -> loginId: {}, password(decoded): {}, term: {}", parts[0], decodedPassword, parts[2]);
}
if (!tokens[RESULT_SUCCESS_INDEX].equals("Y")) {
throw new CustomRuntimeException(ErrorCode.VERIFICATION_FAILED);
}

// CI/DI 복호화 유틸리티
private String decryptCiDiValue(String encryptedValue) {
if (StringUtils.hasText(encryptedValue)) {
try {
return kmcCryptoManager.decrypt(encryptedValue);
} catch (Exception e) {
log.warn("Failed to decrypt CI/DI value.", e);
return "DECRYPTION_FAILED";
}
}
return "EMPTY";
}
}
String name = tokens[NAME_INDEX];
String birth = tokens[BIRTH_INDEX];
String phoneNo = tokens[PHONE_NO_INDEX];
String gender = tokens[GENDER_INDEX];
String serviceTerm = tokens[PLUS_INFO_INDEX];
String signUpToken = tokenProvider.generateOneTimeToken(tokens[CERT_NUM_INDEX]);

// production시 삭제
log.info("[KMCIS] Parsed User Information:");
log.info(" - CertNum: {}", tokens[CERT_NUM_INDEX]);
log.info(" - Name: {}", tokens[NAME_INDEX]);
log.info(" - PhoneNo: {}", tokens[PHONE_NO_INDEX]);
log.info(" - PlusInfo: {}", tokens[PLUS_INFO_INDEX]);
log.info(" - PlusInfo -> {}", tokens[PLUS_INFO_INDEX]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This log statement appears to be a duplicate of the one on the previous line and seems like a leftover from debugging. It should be removed to keep the code clean.

Suggested change
log.info(" - PlusInfo -> {}", tokens[PLUS_INFO_INDEX]);
log.info(" - PlusInfo -> {}", tokens[PLUS_INFO_INDEX]);


return KmcUserInfo.of(name, birth, phoneNo, gender, serviceTerm, signUpToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -27,7 +28,7 @@ public class AdminApplicationController {
private final AdminApplicationService adminApplicationService;

@GetMapping()
// @PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Page<ApplicationListResponse>>> getAll(
@Valid @ModelAttribute ApplicationFilter filter,
@PageableDefault(size = 10) Pageable pageable
Expand All @@ -43,6 +44,7 @@ public ResponseEntity<ApiResponseWrapper<Page<ApplicationListResponse>>> getAll(
fileName = "신청 목록.xlsx",
dtoClass = ApplicationExcelDto.class
)
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public List<ApplicationExcelDto> downloadApplicationInfo() {
return adminApplicationService.getExcelData();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -24,22 +25,25 @@ public class AdminBannerController {

private final AdminBannerService adminBannerService;

@PostMapping()
@PostMapping
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> create(
@RequestBody BannerRequest request) {
adminBannerService.create(request);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "배너 등록 성공"));
}

@GetMapping()
@GetMapping
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<List<BannerResponse>>> getAll() {
List<BannerResponse> responses = adminBannerService.getAll();
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "배너 전체 조회 성공", responses));
}

@GetMapping("/{bannerId}")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<BannerInfoResponse>> getByBannerId(
@PathVariable("bannerId") Long bannerId
) {
Expand All @@ -48,6 +52,7 @@ public ResponseEntity<ApiResponseWrapper<BannerInfoResponse>> getByBannerId(
}

@DeleteMapping("/{bannerId}")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> deleteByBannerId(
@PathVariable("bannerId") Long bannerId
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -17,7 +18,8 @@ public class AdminDashboardController {

private final AdminDashboardService adminDashboardService;

@GetMapping()
@GetMapping
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<DashBoardResponse>> getAll() {
DashBoardResponse response = adminDashboardService.getAll();
return ResponseEntity.ok(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import life.mosu.mosuserver.global.annotation.ExcelDownload;
import life.mosu.mosuserver.presentation.admin.dto.RecommendationExcelDto;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -22,9 +23,8 @@ public class AdminRecommendationController {
fileName = "추천인 목록.xlsx",
dtoClass = RecommendationExcelDto.class
)
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public List<RecommendationExcelDto> downloadApplicationInfo() {
return adminRecommendationService.getExcelData();
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -23,8 +24,8 @@ public class AdminRefundController {

private final AdminRefundService adminRefundService;

@GetMapping()
// @PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
@GetMapping
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Page<RefundListResponse>>> getAll(
@PageableDefault(size = 15) Pageable pageable
) {
Expand All @@ -33,13 +34,12 @@ public ResponseEntity<ApiResponseWrapper<Page<RefundListResponse>>> getAll(
ApiResponseWrapper.success(HttpStatus.OK, "환불 신청 수 조회 성공", responses));
}


@GetMapping(value = "/excel", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@ExcelDownload(
fileName = "환불 목록.xlsx",
dtoClass = RefundExcelDto.class
)
// @PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public List<RefundExcelDto> downloadRefundInfo() {
return adminRefundService.getExcelData();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -27,7 +28,7 @@ public class AdminStudentController {
private final AdminStudentService adminStudentService;

@GetMapping()
//@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Page<StudentListResponse>>> getAll(
@Valid @ModelAttribute StudentFilter filter,
@PageableDefault(size = 10) Pageable pageable
Expand All @@ -37,6 +38,7 @@ public ResponseEntity<ApiResponseWrapper<Page<StudentListResponse>>> getAll(
}

@GetMapping("/excel")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
@ExcelDownload(
fileName = "학생 목록.xlsx",
dtoClass = StudentExcelDto.class
Expand Down
Loading