웹에서 로그인 정보를 전송할 때 SSL(HTTPS) 없이 아이디와 비밀번호를 보내면, 패킷을 가로채는 것만으로도 평문 비밀번호가 그대로 노출될 수 있습니다.
하지만 내부 운영 사이트이거나, 비용 문제로 인증서를 당장 적용하기 어려운 환경이라면 어떻게 해야 할까요?
이번 글에서는 Java + JavaScript 기반 RSA 공개키 암호화 로그인 방식을 구현하는 방법을 정리해보겠습니다.
⚠️ 먼저 강조합니다.
가장 안전한 방법은 HTTPS를 사용하는 것입니다.
아래 방식은 "비용 없이 최소한의 보호"를 구현하기 위한 대안입니다.
🔐 기본 개념 정리
RSA는 비대칭키 암호화 방식입니다.
- 공개키(Public Key) → 누구나 볼 수 있음 (암호화용)
- 개인키(Private Key) → 서버만 보관 (복호화용)
흐름 요약
- 서버에서 RSA 키쌍 생성
- 공개키는 로그인 페이지로 전달
- 개인키는 세션에 저장
- 사용자가 로그인 시 JS로 ID/비밀번호를 공개키로 암호화
- 서버에서 세션에 저장된 개인키로 복호화
중간에서 패킷을 가로채더라도 원문을 해석할 수 없습니다.
🧠 전체 동작 구조
1️⃣ 서버 측
- RSA 공개키 / 개인키 생성
- 개인키는 세션에 저장
- 공개키는 JSP에 전달
2️⃣ 클라이언트 측
- JS가 로그인 버튼을 가로챔
- ID/비밀번호를 RSA로 암호화
- 암호화된 값을 hidden form에 넣어 전송
3️⃣ 서버 측 복호화
- 세션의 개인키로 복호화
- DB 인증 수행
- 개인키는 즉시 삭제 (재사용 방지)
⚙️ 서버에서 RSA 키 생성 (Java)
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);
KeyPair keyPair = generator.genKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
HttpSession session = request.getSession();
session.setAttribute("__rsaPrivateKey__", privateKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpec publicSpec =
(RSAPublicKeySpec) keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
String publicKeyModulus = publicSpec.getModulus().toString(16);
String publicKeyExponent = publicSpec.getPublicExponent().toString(16);
request.setAttribute("publicKeyModulus", publicKeyModulus);
request.setAttribute("publicKeyExponent", publicKeyExponent);
✔ 공개키는 16진수 문자열로 변환하여 JSP로 전달
✔ 개인키는 세션에 저장
🖥️ 로그인 폼 (JSP)
핵심 포인트는 폼을 2개로 분리하는 것입니다.
왜냐하면, submit 버튼을 쓰면 JS 없이도 폼이 전송되기 때문입니다.
<input type="text" id="username" />
<input type="password" id="password" />
<input type="hidden" id="rsaPublicKeyModulus" value="${publicKeyModulus}" />
<input type="hidden" id="rsaPublicKeyExponent" value="${publicKeyExponent}" />
<a href="#" onclick="validateEncryptedForm(); return false;">로그인</a>
<form id="securedLoginForm" method="post" action="/login" style="display:none;">
<input type="hidden" name="securedUsername" id="securedUsername" />
<input type="hidden" name="securedPassword" id="securedPassword" />
</form>
✔ JS를 반드시 타도록 설계
✔ JS가 없으면 로그인 불가
🧩 JavaScript 암호화 처리
function validateEncryptedForm() {
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
if (!username || !password) {
alert("ID/비밀번호를 입력해주세요.");
return false;
}
var modulus = document.getElementById("rsaPublicKeyModulus").value;
var exponent = document.getElementById("rsaPublicKeyExponent").value;
submitEncryptedForm(username, password, modulus, exponent);
}
function submitEncryptedForm(username, password, modulus, exponent) {
var rsa = new RSAKey();
rsa.setPublic(modulus, exponent);
var securedUsername = rsa.encrypt(username);
var securedPassword = rsa.encrypt(password);
var form = document.getElementById("securedLoginForm");
form.securedUsername.value = securedUsername;
form.securedPassword.value = securedPassword;
form.submit();
}
⚠️ BASE64 문제
사용한 JS RSA 라이브러리의 BASE64 인코더는 Firefox에서 오동작했습니다.
따라서:
- 암호화 결과 (byte 배열)
- → 16진수 문자열(hex)로 변환
- → 서버로 전송
이 방식이 더 안전하게 동작했습니다.
🔓 서버에서 복호화 (Java)
private String decryptRsa(PrivateKey privateKey, String securedValue) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
byte[] encryptedBytes = hexToByteArray(securedValue);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, "UTF-8");
}
세션에서 개인키를 꺼낸 뒤:
PrivateKey privateKey =
(PrivateKey) session.getAttribute("__rsaPrivateKey__");
session.removeAttribute("__rsaPrivateKey__"); // 재사용 방지
✔ 개인키는 반드시 제거
✔ 같은 키 재사용 금지
✔ 메모리 누수 방지
📡 실제 전송 데이터
패킷을 HTTP 프록시 툴(Charles)로 확인해보면:
securedUsername = 4af1b2c9...
securedPassword = a8c43d0f...원문을 전혀 알아볼 수 없습니다.
💡 보안상 주의점
이 방식의 한계:
- HTTPS보다 안전하지 않음
- 중간자 공격(MITM)에 완전히 안전하지 않음
- JS 변조 가능성 존재
- 세션 관리 실패 시 취약
따라서:
내부 시스템용 보조 수단으로만 사용하세요.
🔐 원래 목표였던 구조 (하이브리드 암호화)
이상적인 구조는 다음과 같습니다.
- AES로 ID/비밀번호 암호화
- AES 키를 RSA 공개키로 암호화
- 서버에서 RSA 개인키로 AES 키 복호화
- AES로 최종 데이터 복호화
즉, 하이브리드 암호화 구조
하지만 구현 복잡도가 높아,
이번에는 RSA 단일 암호화로 마무리했습니다.
🏁 결론
✔ SSL이 가장 안전
✔ 하지만 내부 사이트라면 RSA 기반 JS 암호화도 대안
✔ 개인키 재사용 금지
✔ BASE64 대신 hex 권장
✔ 세션 메모리 누수 주의
📌 최종 정리
| 항목 | 방식 |
|---|---|
| 암호화 알고리즘 | RSA |
| 암호화 위치 | 브라우저 JS |
| 복호화 위치 | 서버 |
| 키 저장 | 개인키는 세션 |
| 전송 형태 | hex 문자열 |
| 보안 등급 | HTTPS보다는 낮음 |
ChatGPT, 블록체인, 자바, 맥북, 인터넷, 컴퓨터 정보를 공유합니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!