티스토리 뷰

기본적으로 Client와 Server간의 데이터 암호화는 SSL을 이용하는게 정석이다.

부득이한 사정으로 인하여 SSL을 이용하지 못할경우 대안방법으로 RSA 알고리즘을 이용하여 암/복호화를 한다.

(SSL 대신 이용해도 된다는 뜻은 아님.)

 

 

- 필요한 라이브러리

Server : 없음

Client : jsbn.js, prng4.js, rng.js, rsa.js

         (출처 : http://www-cs-students.stanford.edu/~tjw/jsbn/)

 

 

- 프로세스

Client Server
 1. 로그인 페이지 요청   
   2-1. 개인키 생성 후 session에 저장
 2-2. 공개키 생성 후 view 페이지로 전달
 3-1. 아이디/비밀번호 입력 후 submit
 3-2. submit event 발생시 Server에서 받은 공개키로 아이디/비밀번호 암호화
 3-3. 암호화된 아이디/비밀번호를 Server로 전송
 
   4-1. session에 개인키가 있는지 확인(없으면 error)
 4-2. session에서 얻은 개인키로 parameter로 넘어온 암호화된 아이디/비밀번호 복호화
 4-3. 로그인 로직 실행

 

 

- 기능 구현

 

1. Server : RSA키 생성 및 복호화 Util class

package com.gt.board.util;

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;

import javax.crypto.Cipher;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gt.board.vo.other.RSA;

/** Client -> Server 데이터 전송간 암호화 기능을 담당 **/
public class RSAUtil {
    private static final Logger logger = LoggerFactory.getLogger(RSAUtil.class);

    private KeyPairGenerator generator;
    private KeyFactory keyFactory;
    private KeyPair keyPair;
    private Cipher cipher;

    public RSAUtil() {
        try {
            generator = KeyPairGenerator.getInstance("RSA");
            generator.initialize(1024);
            keyFactory = KeyFactory.getInstance("RSA");
            cipher = Cipher.getInstance("RSA");
        } catch (Exception e) {
            logger.warn("RSAUtil 생성 실패.", e);
        }
    }

    /** 새로운 키값을 가진 RSA 생성
     *  @return vo.other.RSA **/
    public RSA createRSA() {
        RSA rsa = null;
        try {
            keyPair = generator.generateKeyPair();

            PublicKey publicKey = keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();

            RSAPublicKeySpec publicSpec = keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
            String modulus = publicSpec.getModulus().toString(16);
            String exponent = publicSpec.getPublicExponent().toString(16);
            rsa = new RSA(privateKey, modulus, exponent);
        } catch (Exception e) {
            logger.warn("RSAUtil.createRSA()", e);
        }
        return rsa;
    }

    /** 개인키를 이용한 RSA 복호화
     *  @param privateKey session에 저장된 PrivateKey
     *  @param encryptedText 암호화된 문자열 **/
    public String getDecryptText(PrivateKey privateKey, String encryptedText) throws Exception {
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = cipher.doFinal(hexToByteArray(encryptedText));
        return new String(decryptedBytes, "UTF-8");
    }

    // 16진수 문자열을 byte 배열로 변환
    private byte[] hexToByteArray(String hex) {
        if (hex == null || hex.length() % 2 != 0) {
            return new byte[] {};
        }

        byte[] bytes = new byte[hex.length() / 2];
        for (int i = 0; i < hex.length(); i += 2) {
            byte value = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
            bytes[(int) Math.floor(i / 2)] = value;
        }
        return bytes;
    }

}

 

2. Server : RSA 개인키/공개키를 담는 VO

package com.gt.board.vo.other;

import java.security.PrivateKey;

public class RSA {
    private PrivateKey privateKey;
    private String modulus;
    private String exponent;

    public RSA() {
    }

    public RSA(PrivateKey privateKey, String modulus, String exponent) {
        this.privateKey = privateKey;
        this.modulus = modulus;
        this.exponent = exponent;
    }

    public PrivateKey getPrivateKey() {
        return privateKey;
    }

    public void setPrivateKey(PrivateKey privateKey) {
        this.privateKey = privateKey;
    }

    public String getModulus() {
        return modulus;
    }

    public void setModulus(String modulus) {
        this.modulus = modulus;
    }

    public String getExponent() {
        return exponent;
    }

    public void setExponent(String exponent) {
        this.exponent = exponent;
    }

}

 

3. Server : Client의 로그인 페이지 요청시 키 발급 및 저장/전달

    // 로그인 페이지 진입
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String loginForm(HttpSession session, Model model) {
        // RSA 키 생성
        PrivateKey key = (PrivateKey) session.getAttribute("RSAprivateKey");
        if (key != null) { // 기존 key 파기
            session.removeAttribute("RSAprivateKey");
        }
        RSA rsa = rsaUtil.createRSA();
        model.addAttribute("modulus", rsa.getModulus());
        model.addAttribute("exponent", rsa.getExponent());
        session.setAttribute("RSAprivateKey", rsa.getPrivateKey());
        return "login";
    }

 

4. Client : Server로부터 받은 공개키를 이용하여 parameter를 암호화 후 submit

	<!-- 유저가 입력하는 form -->
	<form action="/login" method="post" id="loginForm">
		<fieldset>
			<legend class="screen_out">로그인 폼</legend>

			<label for="email">이메일</label>
			<input type="text" id="email" name="email" autofocus autocomplete="off" required />

			<label for="password">비밀번호</label>
			<input type="password" id="password" name="password" autocomplete="off" required />

			<button type="submit">
				<i class="fa fa-sign-in"></i> 로그인
			</button>
		</fieldset>
	</form>

	<!-- 실제 서버로 전송되는 form -->
	<form action="/login" method="post" id="hiddenForm">
		<fieldset>
			<input type="hidden" name="email" />
			<input type="hidden" name="password" />
		</fieldset>
	</form>

	<!-- javascript lib load -->
	<script src="/resources/js/jquery.min.js"></script>
	<script src="/resources/js/rsa/jsbn.js"></script>
	<script src="/resources/js/rsa/prng4.js"></script>
	<script src="/resources/js/rsa/rng.js"></script>
	<script src="/resources/js/rsa/rsa.js"></script>

	<!-- 유저 입력 form의 submit event 재정의 -->
	<script>
		var $email = $("#hiddenForm input[name='email']");
		var $password = $("#hiddenForm input[name='password']");

		// Server로부터 받은 공개키 입력
		var rsa = new RSAKey();
		rsa.setPublic("${modulus}", "${exponent}");

		$("#loginForm").submit(function(e) {
			// 실제 유저 입력 form은 event 취소
			// javascript가 작동되지 않는 환경에서는 유저 입력 form이 submit 됨
			// -> Server 측에서 검증되므로 로그인 불가
			e.preventDefault();

			// 아이디/비밀번호 암호화 후 hidden form으로 submit
			var email = $(this).find("#email").val();
			var password = $(this).find("#password").val();
			$email.val(rsa.encrypt(email)); // 아이디 암호화
			$password.val(rsa.encrypt(password)); // 비밀번호 암호화
			$("#hiddenForm").submit();
		});
	</script>

 

5. Server : Client로부터 받은 암호화된 아이디/비밀번호 복호화 후 로그인 로직 실행

    // 로그인 처리
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(User user, HttpSession session, RedirectAttributes ra) {
        // 개인키 취득
        PrivateKey key = (PrivateKey) session.getAttribute("RSAprivateKey");
        if (key == null) {
            ra.addFlashAttribute("resultMsg", "비정상 적인 접근 입니다.");
            return "redirect:/login";
        }

		// session에 저장된 개인키 초기화
        session.removeAttribute("RSAprivateKey");

		// 아이디/비밀번호 복호화
        try {
            String email = rsaUtil.getDecryptText(key, user.getEmail());
            String password = rsaUtil.getDecryptText(key, user.getPassword());

			// 복호화된 평문을 재설정
            user.setEmail(email);
            user.setPassword(password);
        } catch (Exception e) {
            ra.addFlashAttribute("resultMsg", "비정상 적인 접근 입니다.");
            return "redirect:/login";
        }

        // 로그인 로직 실행
		// userService.login(user);
    }

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday