JSP

 

https 로 접근하는 페이지는 SSL 이 적용된 페이지 이다.

보안상 SSL 로 웹페이지를 암호화를 해야 한다. 그래야 해커들이 패킷감시로 데이터를 가로채지 못한다. 

그러나  SSL 인증서 구매 할 돈이 없다.  그럼 다음과 같은 방법으로 보안 처리 해보자.

출처는 다음과 RSA 기반 웹페이지 암호화 로그인  에서 by 권남 이 만든 소스이다.

 

소스파일은 아래 클릭 또는 현재 프로젝트내에 RSATest  프로젝트 로 존재한다.

RSATest.war

 

LoginFormServlet

package rsatest;

import java.io.IOException;
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.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


// @author kwon37xi

public class LoginFormServlet extends HttpServlet {

    public static final int KEY_SIZE = 1024;

    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
            generator.initialize(KEY_SIZE);
            
            KeyPair keyPair = generator.genKeyPair();
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");

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

            HttpSession session = request.getSession();
            // 세션에 공개키의 문자열을 키로하여 개인키를 저장한다.
            session.setAttribute("__rsaPrivateKey__", privateKey);

            // 공개키를 문자열로 변환하여 JavaScript 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);

            request.getRequestDispatcher("/WEB-INF/views/loginForm.jsp").forward(request, response);
        } catch (Exception ex) {
            throw new ServletException(ex.getMessage(), ex);
        }
    }

    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

 
    @Override
    public String getServletInfo() {
        return "Short description";
    }// </editor-fold>
}

 

 LoginServlet

package rsatest;

import java.io.IOException;
import java.math.BigInteger;
import java.security.PrivateKey;
import javax.crypto.Cipher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

public class LoginServlet extends HttpServlet {

    
     // 암호화된 비밀번호를 복호화 한다.
     
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String securedUsername = request.getParameter("securedUsername");
        String securedPassword = request.getParameter("securedPassword");

        
        // 파라미터로 넘어온 값
        System.out.println("securedUsername:  "+securedUsername + " -  securedPassword : " +securedPassword);
        
        
        HttpSession session = request.getSession();
        PrivateKey privateKey = (PrivateKey) session.getAttribute("__rsaPrivateKey__");
        session.removeAttribute("__rsaPrivateKey__"); // 키의 재사용을 막는다. 항상 새로운 키를 받도록 강제.

        if (privateKey == null) {
            throw new RuntimeException("암호화 비밀키 정보를 찾을 수 없습니다.");
        }
        try {
            String username = decryptRsa(privateKey, securedUsername);
            String password = decryptRsa(privateKey, securedPassword);
            request.setAttribute("username", username);
            request.setAttribute("password", password);
            request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response);
        } catch (Exception ex) {
            throw new ServletException(ex.getMessage(), ex);
        }
        
        
        // 파라미터로 넘어온 setAttribute 로 가져오기 값
        System.out.println("request.getAttribute(username):  "+request.getAttribute("username") +
        		" -  request.getAttribute(password) : " +request.getAttribute("password"));
    }

    private String decryptRsa(PrivateKey privateKey, String securedValue) throws Exception {
        System.out.println("will decrypt : " + securedValue);
        Cipher cipher = Cipher.getInstance("RSA");
        byte[] encryptedBytes = hexToByteArray(securedValue);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        String decryptedValue = new String(decryptedBytes, "utf-8"); // 문자 인코딩 주의.
        return decryptedValue;
    }

  
     // 16진 문자열을 byte 배열로 변환한다.
    
    public static 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;
    }

   
    //BigInteger를 사용해 hex를 byte[] 로 바꿀 경우 음수 영역의 값을 제대로 변환하지 못하는 문제가 있다.
  
    @Deprecated
    public static byte[] hexToByteArrayBI(String hexString) {
        return new BigInteger(hexString, 16).toByteArray();
    }

        public static String base64Encode(byte[] data) throws Exception {
        BASE64Encoder encoder = new BASE64Encoder();
        String encoded = encoder.encode(data);
        return encoded;
    }

    public static byte[] base64Decode(String encryptedData) throws Exception {
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] decoded = decoder.decodeBuffer(encryptedData);
        return decoded;
    }

    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }


    @Override
    public String getServletInfo() {
        return "Short description";
    }// </editor-fold>
}

 

 

loginForm.jsp

<%@page contentType="text/html;charset=utf-8" pageEncoding="UTF-8"%><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
<%
            String publicKeyModulus = (String) request.getAttribute("publicKeyModulus");
            String publicKeyExponent = (String) request.getAttribute("publicKeyExponent");
%>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Login Form</title>

        <!-- script 태그에서 가져오는 자바스크립트 파일의 순서에 주의해야한다! 순서가 틀릴경우 자바스크립트 오류가 발생한다. -->
        <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/jsbn.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/rsa.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/prng4.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath()%>/js/rsa/rng.js"></script>
        <script type="text/javascript" src="<%=request.getContextPath()%>/js/login.js"></script>
    </head>
    <body>
        <div>
            <label for="username">사용자ID : <input type="text" id="username" size="16"/></label>
            <label for="password">비밀번호 : <input type="password" id="password" size="16" /></label>
            <input type="hidden" id="rsaPublicKeyModulus" value="<%=publicKeyModulus%>" />
            <input type="hidden" id="rsaPublicKeyExponent" value="<%=publicKeyExponent%>" />
            <a href="<%=request.getContextPath()%>/loginFailure.jsp" onclick="validateEncryptedForm(); return false;">로그인</a>
        </div>
        <form id="securedLoginForm" name="securedLoginForm" action="<%=request.getContextPath()%>/login" method="post" style="display: none;">
            <input type="hidden" name="securedUsername" id="securedUsername" value="" />
            <input type="hidden" name="securedPassword" id="securedPassword" value="" />
        </form>
    </body>
</html>

 

 

login.jsp

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>로그인 결과</title>
    </head>
    <body>
        <ul>
            <li>사용자ID : <%=request.getAttribute("username")%></li>
            <li>비밀번호 : <%=request.getAttribute("password")%></li>
        </ul>
    </body>
</html>

 

 

자바스크립트 파일이 필요하며,  자바스크립트를 제거하고 실행시에는  로그인 실패처리로 나오게 개발하였다.

 

사용자의 로그인 정보를 받아서 실제 사용자가 맞는지 확인하는쪽 컨트롤러이다. 여기서는 그냥 사용자의 입력을 화면에 다시 출력해주도록 만들었다.

HTTP 세션에서 앞서 저장한 개인키를 읽는다. 만약 변조된 공개키로 암호화를 했거나 개인키가 존재하지 않는 상황이라면 물론 오류가 발생한다.

그리고 세션에서 개인키를 지워버린다. 절대로 동일 개인키로 두 번이상 로그인 할 수 없게 만들었다.

 

어떻게 전송되나

아래 이미지는 실제 전송되는 데이터를 Charles 라는 HTTP 프록시 툴로 살펴본 것이다. 사용자의 ID와 비밀번호가 모두 암호화되어 원형을 알아 볼 수 없음을 볼 수 있다.

 

 

 

 

이상이 끝. 관련 파일을 모두 함께 올려둔다. WAR 는 실행하고서 http://localhost:8080/RSATest 로 접속하면 된다.

 

 

 

제작 : macaronics.net - Developer  Jun Ho Choi

소스 :  https://github.com/braverokmc79/jsp_sin

루트 설정( http://macaronics.net/index.php/m01/jsp/view/1352)    및 server.xml  에서 DB 컨넥션 설정은 필수 설정이다.

 

 

about author

PHRASE

Level 60  라이트

지신에 붙이고 성주에 붙인다 , 가뜩이나 적은 것을 이리저리 벼르고 뜯기어 남는 것이 없다는 말.

댓글 ( 4)

댓글 남기기

작성