JSP

 

이전 글을 참조하고  깃 허브에 RSATest    프로젝트 파일을  참조에서 jsp 로그인 보안처리를 해보자.

csrf 보안처리까지 해야 하므로 다음과 같이 변경 하였다.

 

 

LoginFormAction

package net.macaronics.web.controll;

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.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import net.macaronics.web.controll.action.Action;

public class LoginFormAction implements Action {

	public static final int KEY_SIZE = 1024;

	@Override
	public void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String url = "member/loginForm.jsp";

		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(url).forward(request, response);
		} catch (Exception ex) {
			throw new ServletException(ex.getMessage(), ex);
		}

	}

}

 

LoginFormAction 을 코딩하였으면 현재 프로젝트에서 ActionFactory  에서  url 파라미터에 맞는 객체 생성을 한다.

 

 

loginForm.jsp

EL 로 코드를 변경 하였으며 js  improt 는 필수이다.

<!-- 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>

 

자바스크립트 처리하기 때문에 아이값과  폼에서 id="username"   id="password" 로 해야 한다.

또한,  아래  히든값 처리 아이디와 name 을 동일하게 처리한다.

        <input type="hidden" id="rsaPublicKeyModulus" value="${publicKeyModulus}" />
        <input type="hidden" id="rsaPublicKeyExponent" value="${publicKeyExponent}" />
	<csrf:form  id="securedLoginForm" name="securedLoginForm" 
		 action="${request.getContextPath() }/member/loginproc.jsp" method="post" style="display: none;">
	       <input type="hidden" name="securedUsername" id="securedUsername" value="" />
           <input type="hidden" name="securedPassword" id="securedPassword" value="" />

	</csrf:form> 

 

 

<%@page import="java.net.URLEncoder"%>
<%@page import="config.GetIpAddress"%>
<%@page import="java.util.Enumeration"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.owasp.org/index.php/Category:OWASP_CSRFGuard_Project/Owasp.CsrfGuard.tld" prefix="csrf" %>    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>  
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>  
<!DOCTYPE html >
<html>
<head>
<jsp:include page="../include/Header.jsp" />  


<!-- 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>
<jsp:include page="../include/HeaderMenu.jsp" />




<div class="row">
	
	<div class="col-xs-12 col-sm-12">
	<h2>&nbsp;</h2>	
	<h2 class="text-center">로그인</h2>
	</div>		
	
	<div class="col-xs-3 col-sm-3"></div>	

		
		
		<form class="form-horizontal">
		<div class="col-xs-8 col-sm-8">
	 	<p>&nbsp;</p>
		 <div class="form-group">
		 	<div class="col-sm-2 control-label">
		 		<label for="id">아이디</label>
		 	</div>
		 	<div class="col-sm-6 text-left">
		 		<input type="text" class="form-control" name="id" id="username" >
		 		<p style="color:red;">${idError}</p>
		 	</div>
		 </div>
		 
		 <div class="form-group">
		 	<div class="col-sm-2 control-label">
		 		<label id="pwd">패스워드</label>
		 	</div>
		 	<div class="col-sm-6">
		 		<input type="password" class="form-control" name="pwd" id="password">
		 		<p style="color:red;">${pwdError}</p>
		 	</div>
		 </div>
		</div>
		
		
		  <div class="col-xs-12 col-sm-12">
		 <div class="form-group text-center" >
		    <div class="col-xs-12 col-sm-12" style="color:red;">
		    <c:if test="${param.msg=='failed' }">
		    	<script>
		    		alert("아이디 또는 비밀번호가 일치하지 않습니다.");
		    		location.href="/MacaronicsServlet?command=login_form";
		    	
		    	</script>
		    	
		    </c:if>
		 	 
		 	</div>
		 </div>	
		 </div>
		
		
	  <div class="col-xs-12 col-sm-12">
		 <div class="form-group text-center" >
		    <div class="col-xs-12 col-sm-12">
		 	<input type="button" value="로그인" class="btn btn-primary" onclick="validateEncryptedForm(); return false;">
		 	<input type="reset" value="취소" class="btn btn-warning">
		 	</div>
		 </div>	
		 </div>
	</form> 
		 
		 
		 
        <input type="hidden" id="rsaPublicKeyModulus" value="${publicKeyModulus}" />
        <input type="hidden" id="rsaPublicKeyExponent" value="${publicKeyExponent}" />
	<csrf:form  id="securedLoginForm" name="securedLoginForm" 
		 action="${request.getContextPath() }/member/loginproc.jsp" method="post" style="display: none;">
	       <input type="hidden" name="securedUsername" id="securedUsername" value="" />
           <input type="hidden" name="securedPassword" id="securedPassword" value="" />

	</csrf:form> 

	

</div>

<h2>&nbsp;</h2>
<h2>&nbsp;</h2>
<h2>&nbsp;</h2>
<h2>&nbsp;</h2>



<jsp:include page="../include/Footer.jsp" />



   

 

 

 

LoginServlet

package config;

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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import net.macaronics.web.dao.MemberDAO;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

public class LoginServlet {
	
	final static Logger logger =LogManager.getLogger(LoginServlet.class);
    
     // 암호화된 비밀번호를 복호화 한다.
    
    public boolean processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String securedUsername = request.getParameter("securedUsername");
        String securedPassword = request.getParameter("securedPassword");

        
        // 파라미터로 넘어온 값
        logger.info("securedUsername: {}  ",securedUsername );
        logger.info("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);
            
            // 파라미터로 넘어온 setAttribute 로 가져오기 값
            logger.info("request.getAttribute(): {}, {}   ",username,  password);     
            return confirm(username,password);
           
        } catch (Exception ex) {
            throw new ServletException(ex.getMessage(), ex);
        }        
    
    }

     //비밀번호 아이디 확인
     private boolean confirm(String id , String pwd){
    	 MemberDAO dao =MemberDAO.getInstance();
    	 return dao.checkIdAndPwd(id, pwd); 
     }
    
    
    
    
    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;
    }

  
}

 

 

loginproc.jsp

<%@page import="net.macaronics.web.dto.MemberVO"%>
<%@page import="net.macaronics.web.dao.MemberDAO"%>
<%@page import="config.LoginServlet"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
	LoginServlet login =new LoginServlet();
	if(login.processRequest(request, response)){
		//로그인 성공시
		MemberDAO dao =MemberDAO.getInstance();
		MemberVO   vo=dao.getMember((String)request.getAttribute("username"));
		session.setAttribute("loginUser", vo);
		response.sendRedirect("/index.html");
	}else{
		//로그인 실패
		//request.setAttribute("msg", "아이디 또는 비밀번호가 일치하지 않습니다.");
		//request.getRequestDispatcher("/member/loginForm.jsp").forward(request, response);
		response.sendRedirect("/member/loginForm.jsp?msg=failed");
	}
	
%>

 

 

Mybatis 

mybatis 에서 전달될 파라미터 값이 두개 이상일경우는 무조건 Map 처리를 해야 한다.

MemberDAO

	
	//아이디와 비밀번화 체크
	public boolean checkIdAndPwd(String id, String pwd){
		int confirm=0;
		try{
			sqlSession=MybatisService.getFactory().openSession();
			Map<String, Object> map =new HashMap<>();
			map.put("id", id);
			map.put("pwd",pwd);
			confirm=sqlSession.selectOne("member.checkIdAndPwd", map);
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			MybatisService.sessionClose(sqlSession);
		}
		//0보다 크면 로그인 성공
		return confirm >0 ? true :false;
	}

 

 

아래그림은  CSRF 보안 쿠폰이 적용된 모습이고.

아이디와 패스워드가  다음과 같이 암호화 된다.

항상 새로운 키값으로 새롭게 암호화 처리된다.  

19:47:45.004 [http-nio-8090-exec-5] INFO  config.LoginServlet - securedUsername: 61b5c1999464352377ec09e8a899cf1cf9ececb1e2294c08844c7fd19c83be81a17fc59ca948695e17f1a394789b2ce37b3945b7618b974fdc9eced530a320609284619b5a958755d15f84fe48a4b19d35af25245c46cf2c3e6c8bd43e38e88132ae215c896fd4088f17a063ad7649abe91f2e9ecc288b88519bed08ae956f89  
19:47:45.004 [http-nio-8090-exec-5] INFO  config.LoginServlet - securedPassword: 75dacb4c058d8d5e902df7385e2e8d3101ba9bed254b753cec938f284827364e5479950c8e511ebc0c84b778874275df2d589cd5b58aae0827fa2dfe8b703b6bc0a1fc6a374df2f49a386c7125594067bd6d32bca6b3093a25c604241b4a36138c5172216c54121c3b5c053615ea603a4f3bd26d0fb2941ae117dc2cb5177ebf
will decrypt : 61b5c1999464352377ec09e8a899cf1cf9ececb1e2294c08844c7fd19c83be81a17fc59ca948695e17f1a394789b2ce37b3945b7618b974fdc9eced530a320609284619b5a958755d15f84fe48a4b19d35af25245c46cf2c3e6c8bd43e38e88132ae215c896fd4088f17a063ad7649abe91f2e9ecc288b88519bed08ae956f89
will decrypt : 75dacb4c058d8d5e902df7385e2e8d3101ba9bed254b753cec938f284827364e5479950c8e511ebc0c84b778874275df2d589cd5b58aae0827fa2dfe8b703b6bc0a1fc6a374df2f49a386c7125594067bd6d32bca6b3093a25c604241b4a36138c5172216c54121c3b5c053615ea603a4f3bd26d0fb2941ae117dc2cb5177ebf

 

 

 

 

member.xml

	<!--  비밀번호 체크 --> 	
	<select id="checkIdAndPwd" resultType="int">
	<![CDATA[
		select count(*) from TBL_MEMBER where id=#{id} and pwd=#{pwd}
	]]>
	</select>

 

실행화면

 

실패시

 

 

성공시

 

 

제작 : macaronics.net - Developer  Jun Ho Choi

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

 

${request.getContextPath() } 처리를 안한 부분이 있으므로 

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

 

 

 

 

about author

PHRASE

Level 60  라이트

나라를 다스리는 어진 재상이 되지 못할 바에는 사람과 병을 다스리는 명의가 되겠다. -허준

댓글 ( 4)

댓글 남기기

작성