1. HTTPS만으로 충분할까?
일반적으로 웹 애플리케이션 보안은 SSL/TLS(HTTPS) 전송 계층 암호화와 서버 내부의 단방향 해시(Hashing) 저장 방식이면 충분하다고 생각했습니다. 하지만 최근 애플리케이션이 복잡해지고, 다양한 보안사고등으로 인해 평문 노출/유출에 대한 우려가 생겼습니다.
- 실수로 비밀번호 등을 로그로 찍는 순간, 비밀번호가 로그 파일에 남을 수 있습니다.
- 로그에 접근 가능한 내부자나, 프록시 등을 탈취 가능성을 완전히 배제할 수 없습니다.
내부 의사결정 후 브라우저에서 서버로 전송되는 구간에서도 데이터를 암호화하기로 결정했습니다.
2. 기술 검토: 대칭키 vs 비대칭키
과거 다른 프로젝트에서 세션 기반의 대칭키(AES) 방식을 통해 비밀번호를 암호화하는 것을 본 적이 있습니다.
- 기존 방식: 서버가 AES 키를 생성해 세션에 저장하고, 클라이언트에 내려주면 클라이언트가 그 키로 암호화하여 전송.
- 문제점: 대칭키 방식은 복호화 키가 브라우저에 노출됩니다. 공격자가 브라우저 메모리나 네트워크를 분석해 키를 탈취하면 복호화할 수 있습니다.
위 문제로 인해 비대칭키(RSA) 암호화 방식을 채택했습니다.
- 공개키(Public Key): 브라우저에 전송. 암호화만 가능.
- 개인키(Private Key): 서버 세션에만 저장. 복호화만 가능.
공개키가 탈취되어도 개인키 없이는 복호화가 불가능하므로 훨씬 안전합니다.
3. 구현 과정 (Spring Framework& Java)
구현의 핵심은 키 쌍(Key Pair) 생성과 AOP를 활용한 자동 복호화입니다. (로그인의 경우 스프링 시큐리티 필터를 통한 처리를 수행했습니다)
1) 키 생성 및 관리
초기 구현 단계라 static 유틸리티 메서드로 작성했으나, 추후 유연성을 위해 Bean으로 등록하여 관리할 예정입니다.
public static String generateKeyForSession(HttpSession session, int inactiveSeconds) throws Exception {
// 1. 이미 세션에 공개키가 있다면 재사용 (불필요한 연산 방지)
PublicKey savedKey = (PublicKey) session.getAttribute(RSACryptoUtils.RSA_PUBLIC_SESSION_KEY);
if (savedKey != null) {
return Base64.getEncoder().encodeToString(savedKey.getEncoded());
}
// 2. 키 쌍(KeyPair) 생성 (RSA 2048bit)
KeyPair pair = generateKeyPair();
// 3. 세션에 개인키/공개키 저장 및 만료 시간 설정
session.setAttribute(RSA_PRIVATE_SESSION_KEY, pair.getPrivate());
session.setAttribute(RSA_PUBLIC_SESSION_KEY, pair.getPublic());
session.setMaxInactiveInterval(inactiveSeconds);
// 4. 클라이언트에는 공개키만 Base64로 인코딩하여 전달
return Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
}
2) 클라이언트 암호화 수행
자바스크립트에서는 JSEncrypt를 통해서 암호화를 수행하여 전송하였습니다
const publicKey =`<c:out value="${publicKey}"/>`;
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
encryptor.encrypt($("#password").val());
3) AOP를 활용한 투명한 복호화
커스텀 어노테이션(@EncryptedPassword)과 AOP를 사용하여 비즈니스 로직 침범 없이 복호화를 수행하도록 처리했습니다.
@Around("@annotation(com.example.security.EncryptedPassword)")
public Object decryptPassword(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// ... 인자 검증 로직 ...
// 세션에서 개인키 가져오기
PrivateKey privateKey = (PrivateKey) session.getAttribute(RSA_PRIVATE_SESSION_KEY);
// 복호화 수행
String encryptedValue = (String) targetArg;
String decryptedPassword = RSACryptoUtils.decrypt(encryptedValue, privateKey);
// 원본 객체 필드에 평문 비밀번호 주입 (Reflection)
field.set(arg, decryptedPassword);
return joinPoint.proceed(args);
}
4. 주요 고려 사항 및 트러블 슈팅
Q1. 복호화 실패 시 예외 처리는?
- 로그인 시: 복호화 실패는 곧 '비밀번호 불일치'와 같습니다. 비밀번호 불일치 시에도 큰 리스크가 없어서 로그인이 되도록 했습니다.
- 회원가입/비밀번호 변경 시: 키 만료 등으로 복호화가 실패 시 비밀번호가 오입력 될 가능성도 있어서
익셉션을 발생시키고, 프론트엔드에서 키 재발급 요청 후 다시 시도하도록 유도했습니다.
Q2. 세션 만료(키 소멸) 시 UX는?
- 유저가 회원가입 페이지를 열어두고 일정 시간 자리를 비워 세션(키)가 만료될 수 있습니다. 허나 클라이언트에는 암호화 키가 남아있어 전송 시점에서 키 페어 불일치가 발생합니다.
- 사용자가 액션 시 서버에서 Exception 발생 → 내부적으로 즉시 키를 재발급하고 클라이언트에게 다시 시도를 유도 했습니다.
Q3. 성능 이슈와 발급 시점 최적화
RSA 키 생성 CPU 연산 비용이 꽤 비싼 작업입니다. 특히 Node.js 같은 싱글 스레드 환경이나 트래픽이 몰리는 상황에서는 병목이 될 수 있습니다.
- 모든 유저에게 발급? (X): 메인 페이지 접속 시 무조건 키를 발급하는 것은 낭비입니다.
- 필요할 때만 발급 (O): 로그인 모달이 뜨거나, 비밀번호 입력
input창에 포커스(Focus)가 가는 시점에 키 발급 API를 호출하도록 최적화했습니다. 이를 통해 불필요한 서버 리소스 소모를 막을 수 있었습니다.
5. 결론
SSL이 전송 구간을 지켜주지만, 애플리케이션 레벨에서의 암호화는 실수로 인한 데이터 유출을 막는 수단입니다. 물론 암호화 로직 추가는 서버 리소스 사용량 증가이 발생할 수 있지만 요즘 같은 시대에 이제는 사소한 영역 또한 모두 보안조치가 필요한 것 같습니다.
'BackEnd > JAVA' 카테고리의 다른 글
| Heap Dump (0) | 2025.12.29 |
|---|---|
| LocalDateTime 타임존에 대한 이해 (0) | 2024.11.05 |
| [자바 JVM 모니터링]VisualVM을 통한 모니터링 해보기 (0) | 2024.10.30 |
| [용어정리] JCP, JSR, JSR-310 정리 (0) | 2024.10.16 |
| 실수형 표현이 정확하지 않은 이유? (0) | 2024.10.05 |