<게시판만들기>-RSA 암호화를 이용하여 로그인하기
[RSA 암호화란?]
RSA는 두 개의 키를 사용한다. 여기서 키란 메시지를 열고 잠그는 상수(constant)를 의미한다. 일반적으로 많은 공개키 알고리즘의 공개키(public key)는 모두에게 알려져 있으며 메시지를 암호화(encrypt)하는데 쓰이며, 암호화된 메시지는 개인키(private key)를 가진 자만이 복호화(decrypt)하여 열어볼 수 있다. 하지만 RSA 공개키 알고리즘은 이러한 제약조건이 없다. 즉 개인키로 암호화하여 공개키로 복호화할 수도 있다.
공개키 알고리즘은 누구나 어떤 메시지를 암호화할 수 있지만, 그것을 해독하여 열람할 수 있는 사람은 개인키를 지닌 단 한 사람만이 존재한다는 점에서 대칭키 알고리즘과 차이를 가진다.
[구현 흐름도]
- 1) 서버 -> 서버측에서 RSA 공개키와 개인키(비밀키)를 생성하여, 개인키는 세션에 저장하고 공개키는 자바스크립트 로그인 폼이 있는 페이지로 출력한다.
- 2) 클라이언트 -> 로그인폼은 자바스크립트가 실행되지 않는 환경에서는 발행(submit)이 되면 안된다.
-
3) 클라이언트 -> 로그인폼에 사용자의 ID와 비밀번호를 넣고 발행을 누르면 자바스크립트가 이를 가로챈다.
- 4) 클라이언트 -> 사용자 ID를 RSA로 암호화하여 화면에 안보이는 새로운 폼에 저장한다.
- 5) 클라이언트 -> 비밀번호를 RSA로 암호화하여 화면에 안보이는 새로운 폼에 저장한다.
- 6) 서버 -> 이제 화면에 안보이는 해당 폼을 서버로 발행한다.
- 7) 서버 -> 서버측에서 세션에 저장된 RSA개인키로 암호화된 사용자ID와 비밀번호를 복호화한다.
- 8) 서버 -> 데이터베이스/혹은 기타 저장소에 저장된 사용자 ID와 비밀번호가 일치하는지 확인한다.
<CryptoGenerator.java>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
package kr.or.ddit.utiles;
import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; import java.util.HashMap; import java.util.Map;
import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.servlet.http.HttpSession;
public class CryptoGenerator{ // 암, 복호화 : 공개키 = 비밀키 ( 생성시 동반 생성, 1회 활용하고 폐기) // 반환값 : 공개키 (가수부, 지수부 구분해서 맵에 저장후 반환) // 파라메터 : 비밀키 세션 저장 public static Map<String, String> generatePairKey(HttpSession session){
//공개키 + 비밀키 생성 KeyPairGenerator keyGenerator = null;
//취득, 생성된 공개키 + 비밀키 KeyPair keyPair = null;
//공개키 PublicKey publickKey = null;
//비밀키 PrivateKey privateKey = null;
//공개키 = 가수부 + 지수부로 나누주는 역할 KeyFactory keyFactory = null;
Map<String, String> publicKeyMap = new HashMap<String, String>();
try{ //공개키와 비밀키를 생성할 수 있는 키페어 인스턴스를 얻어낸다. keyGenerator = KeyPairGenerator.getInstance("RSA"); //RSA 암호 알고리즘을 사용
//공개키, 비밀키 생성시 사이즈 설정 : byte 단위 ( 반드시 짝수) keyGenerator.initialize(2048); //반드시 짝수로 파라미터 (사이즈설정)
//생성된 공개키, 비밀키 취득 keyPair = keyGenerator.generateKeyPair();
//공개키 취득 publickKey = keyPair.getPublic();
//비밀키 취득 privateKey = keyPair.getPrivate();
//비밀키 세션에 저장 session.setAttribute("privateKey", privateKey);
//공개키(Double Type) : 가수부와 지수부로 구분해준다. => 클라이언트에 제공 keyFactory = keyFactory.getInstance("RSA");
RSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publickKey, RSAPublicKeySpec.class);
//공개키 가수부 String publicModulus = publicKeySpec.getModulus().toString(16); //16진수로 바꾼다
//공개키 지수부 String publicExponent = publicKeySpec.getPublicExponent().toString(16); //16진수로 바꾼다
//공개키 가수부 지수부 나누어 Map에 저장 publicKeyMap.put("publicModulus", publicModulus); publicKeyMap.put("publicExponent", publicExponent);
}catch(NoSuchAlgorithmException e){ e.printStackTrace(); }catch (InvalidKeySpecException e1) { e1.printStackTrace(); }
return publicKeyMap; }
//암호문을 평문으로 복호화 public static String decryptRSA(HttpSession session, String secureValue){ //평문을 반환하도록 변수 선언 String returnValue = ""; PrivateKey privateKey = (PrivateKey)session.getAttribute("privateKey");
//평문으로 바꿀때 사용 try{ Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); //복호화 할 때 사용하는것 : Cipher.DECRYPT_MODE
//암호문은 짝수 단위로 바이너리 코드가 존재 -> 평문으로 바꾸기 위한 바이트로 바꾸어 버린다. byte[] targetByte = hextoByteArray(secureValue); //String으로 변경하기 전 byte byte[] beforeString = cipher.doFinal(targetByte);
returnValue = new String(beforeString, "UTF-8"); //평문으로 바뀜
}catch(NoSuchAlgorithmException e){ e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return returnValue; }
//바이너리 -> 바이트 단위로 바꾸기 private static byte[] hextoByteArray(String secureValue){
//암호문이 널이거나 짝수가 아니면 암호문이 잘못 암호화 된것이기 때문에 처리해주어야한다. if(secureValue == null || secureValue.length() %2 !=0){ return new byte[]{}; }
byte[] bytes = new byte[secureValue.length()/2];
for(int i=0; i<secureValue.length(); i+=2){ byte value = (byte)Integer.parseInt(secureValue.substring(i, i+2), 16); bytes[(int)Math.floor(i/2)] = value; } return bytes; }
} |
cs |
<header.jsp>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
<%@page import="kr.or.ddit.utiles.CryptoGenerator"%> <%@page import="java.util.Map"%> <%@ page language="JAVA" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<% //비밀키 세션에 저장하고, 공개키 반환하는 함수 호출. Map<String, String> publicKeyMap = CryptoGenerator.generatePairKey(session); %>
<c:set var="publicKeyMap" value="<%=publicKeyMap %>"></c:set>
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Insert title here</title> <!--Loading KAdmin css 시작 --> <link type="text/css" rel="stylesheet" href="http://fonts.googleapis.com/css?family=Open+Sans:400italic,400,300,700"> <link type="text/css" rel="stylesheet" href="http://fonts.googleapis.com/css?family=Oswald:400,700,300"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/jquery-ui-1.10.4.custom.min.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/font-awesome.min.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/bootstrap.min.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/animate.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/all.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/main.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/style-responsive.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/zabuto_calendar.min.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/pace.css"> <link type="text/css" rel="stylesheet" href="${pageContext.request.contextPath}/01/styles/jquery.news-ticker.css"> <!--Loading KAdmin css 끝 --> <!-- summernote 에디터 스타일 파일 시작 --> <link href="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.7.1/summernote.css" rel="stylesheet"> <!-- summernote 에디터 스타일 파일 끝 --> <!-- 부트스트랩 다이얼로그 스타일 파일 시작 --> <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap3-dialog/1.34.7/css/bootstrap-dialog.min.css" rel="stylesheet" type="text/css" /> <!-- 부트스트랩 다이얼로그 스타일 파일 끝 --> </head> <body > <div id="header-topbar-option" class="page-header-topbar"> <nav id="topbar" role="navigation" style="margin-bottom: 0;" data-step="3" class="navbar navbar-default navbar-static-top"> <div class="navbar-header"> <button type="button" data-toggle="collapse" data-target=".sidebar-collapse" class="navbar-toggle"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a id="logo" href="http://www.ddit.or.kr/images/footer.jpg" class="navbar-brand"> <span class="logo-text text-yellow" style="font-size: 16px;">(재단법인)대덕인재개발원</span> </a> </div> <div class="topbar-main"> <a id="menu-toggle" href="#" class="hidden-xs"> <i class="fa fa-bars"></i> </a> <form id="topbar-search" action="" method="" class="hidden-sm hidden-xs"> <div class="text-white"> <input type="text" name="search_keyword" placeholder="검색어 입력" class="form-control text-yellow"/> </div> </form>
<c:if test="${!empty LOGIN_MEMBERINFO }"> <div class="logoutForm"> <ul class="nav navbar navbar-top-links navbar-right mbn"> <li class="dropdown"> <a data-hover="dropdown" href="#" class="dropdown-toggle"> <i class="fa fa-bell fa-fw"></i> <span class="badge badge-green">3</span> </a> </li> <li class="dropdown"> <a data-hover="dropdown" href="#" class="dropdown-toggle"> <i class="fa fa-envelope fa-fw"></i> <span class="badge badge-orange">7</span> </a> </li> <li class="dropdown"> <a data-hover="dropdown" href="#" class="dropdown-toggle"> <i class="fa fa-tasks fa-fw"></i> <span class="badge badge-yellow">8</span> </a> </li> <li class="dropdown"> <a data-hover="dropdown" href="#" class="dropdown-toggle"> <img src="${pageContext.request.contextPath }/image/disk.png" alt="" class="img-responsive img-circle" /> <span class="hidden-xs">${LOGIN_MEMBERINFO.mem_name }</span> <span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> <li><a href="#"><i class="fa fa-user"></i>프로필관리</a></li> <li><a href="#"><i class="fa fa-calendar"></i>스케줄관리</a></li> <li><a href="#"><i class="fa fa-envelope"></i>쪽지관리 <font color="red">3</font></a></li> <li><a href="#"><i class="fa fa-tasks"></i>메일관리 <font color="red">5</font></a></li> <li class="divider"></li> <li><a href="${pageContext.request.contextPath }/01/logout.jsp"><i class="fa fa-key"></i>로그아웃</a></li> </ul> </li> </ul> </div> </c:if>
<c:if test="${empty LOGIN_MEMBERINFO }"> <div class="loginForm nav navbar navbar-top-links navbar-right" style="padding: 10px;"> <ul> <li class="dropdown"> <label for="inputName" class="control-label text-yellow">아이디 :</label> <input type="text" name="mem_id" placeholder="아이디를 입력해주세요." class="text-yellow" /> </li> <li> </li> <li class="dropdown"> <label for="inputName" class="control-label text-yellow">패스워드 :</label> <input type="text" name="mem_pass" placeholder="패스워드를 입력해주세요." class="text-yellow" /> </li> <li> </li> <li class="dropdown"> <button type="button" class="btn btn-warning btn-sm" id="logninBtn">로그인</button> <button type="button" class="btn btn-warning btn-sm" id="joinBtn">회원가입</button> </li> </ul> </div> </c:if>
</div> </nav> </div> </body> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script src="${pageContext.request.contextPath}/01/script/jquery-1.8.3.js"></script> <script src="${pageContext.request.contextPath}/01/script/jquery-ui.js"></script> <script src="${pageContext.request.contextPath}/01/script/bootstrap.min.js"></script> <script src="${pageContext.request.contextPath}/01/script/bootstrap-hover-dropdown.js"></script> <script src="${pageContext.request.contextPath}/01/script/html5shiv.js"></script> <script src="${pageContext.request.contextPath}/01/script/respond.min.js"></script> <script src="${pageContext.request.contextPath}/01/script/jquery.metisMenu.js"></script> <script src="${pageContext.request.contextPath}/01/script/jquery.slimscroll.js"></script> <script src="${pageContext.request.contextPath}/01/script/jquery.cookie.js"></script> <script src="${pageContext.request.contextPath}/01/script/icheck.min.js"></script> <script src="${pageContext.request.contextPath}/01/script/custom.min.js"></script> <script src="${pageContext.request.contextPath}/01/script/jquery.news-ticker.js"></script> <script src="${pageContext.request.contextPath}/01/script/jquery.menu.js"></script> <script src="${pageContext.request.contextPath}/01/script/pace.min.js"></script> <script src="${pageContext.request.contextPath}/01/script/holder.js"></script> <script src="${pageContext.request.contextPath}/01/script/responsive-tabs.js"></script> <script src="${pageContext.request.contextPath}/01/script/zabuto_calendar.min.js"></script> <script src="${pageContext.request.contextPath}/01/script/main.js"></script> <!-- summernote 에디터 js 파일 시작 --> <script src="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.7.1/summernote.js"></script> <!-- summernote 에디터 js 파일 끝 --> <!-- 부트스트랩 다이얼로그 js 파일 시작 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap3-dialog/1.34.7/js/bootstrap-dialog.min.js"></script> <!-- 부트스트랩 다이얼로그 js 파일 끝 -->
<script type="text/javascript" src= "${pageContext.request.contextPath }/js/common/validation.js"></script> <script type="text/javascript" src = '${pageContext.request.contextPath }/js/crypto/jsbn.js'></script> <script type="text/javascript" src = '${pageContext.request.contextPath }/js/crypto/rsa.js'></script> <script type="text/javascript" src = '${pageContext.request.contextPath }/js/crypto/prng4.js'></script> <script type="text/javascript" src = '${pageContext.request.contextPath }/js/crypto/rng.js'></script> <script type="text/javascript" src = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/core.js'></script> <script type="text/javascript" src = 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/sha256.js'></script>
<script type="text/javascript"> $(function(){ $('#logninBtn').click(function(){ var mem_id = $('input[name=mem_id]').val(); if(!mem_id.validationID()){ alert('아이디를 바르게 입력해주세요'); return false; }
var mem_pass = $('input[name=mem_pass]').val(); if(!mem_pass.validationPWD()){ alert('비밀번호를 바르게 입력해주세요'); return false; }
var modulus = '${publicKeyMap["publicModulus"]}'; var exponent = '${publicKeyMap["publicExponent"]}';
var rsaObject = new RSAKey(); //js/crypto/rsa.js rsaObject.setPublic(modulus, exponent);
var encryptID = rsaObject.encrypt($('input[name=mem_id]').val()); var encryptPWD = rsaObject.encrypt($('input[name=mem_pass]').val());
var $frm = $('<form action="${pageContext.request.contextPath}/01/loginCheck.jsp" method="post"></form>'); var $inputID = $('<input type="hidden" value="'+ encryptID+'" name="mem_id" />'); var $inputPWD = $('<input type="hidden" value="'+ encryptPWD+'" name="mem_pass" />');
$frm.append($inputID); $frm.append($inputPWD); $(document.body).append($frm); $frm.submit(); }); }); </script> </html> |
cs |
<loginCheck.jsp>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<%@page import="kr.or.ddit.member.service.IMemberServiceImpl"%> <%@page import="kr.or.ddit.member.service.IMemberService"%> <%@page import="kr.or.ddit.vo.MemberVO"%> <%@page import="java.net.URLEncoder"%> <%@page import="java.util.HashMap"%> <%@page import="java.util.Map"%> <%@page import="kr.or.ddit.listener.SessionManager"%> <%@page import="kr.or.ddit.utiles.CryptoGenerator"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% String mem_id = request.getParameter("mem_id"); String mem_pass = request.getParameter("mem_pass");
//평문으로 만들어주기 mem_id = CryptoGenerator.decryptRSA(session, mem_id); mem_pass = CryptoGenerator.decryptRSA(session, mem_pass);
SessionManager.getInstance().loginDuplicationCheck(session.getId(), mem_id);
Map<String, String> params = new HashMap<String, String>();
params.put("mem_id", mem_id); params.put("mem_pass", mem_pass);
IMemberService service = IMemberServiceImpl.getInstance(); MemberVO memberInfo = new MemberVO(); memberInfo = service.memberInfo(params); String message = "";
if(memberInfo == null){ message = URLEncoder.encode("회원이 아닙니다","UTF-8"); }else{ session.setAttribute("LOGIN_MEMBERINFO", memberInfo); } response.sendRedirect(request.getContextPath()+"/01/main.jsp?message="+message); %> |
cs |
<logout.jsp>
1 2 3 4 5 6 7 8 9 10 |
<%@page import="java.net.URLEncoder"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% session.invalidate();
String message = URLEncoder.encode("로그아웃되었습니다.", "UTF-8"); response.sendRedirect(request.getContextPath() + "/01/main.jsp?message=" + message); %>
|
cs |
<validation.js>
1 2 3 4 5 6 7 8 9 10 |
/** * */
String.prototype.validationID = function(){ return /^[a-z][0-9]{3}$/.test(this); }; String.prototype.validationPWD = function(){ return /^[a-z0-9]{4,15}$/.test(this); }; |
cs |
crypto -> jsbn.js/prng4.js/rng.js/rsa.js
memberDAO , SERVICE 함수만들기 -> public MemberVO memberInfo(Map<String, String> params)
member.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<sqlMap namespace="member"> <typeAlias alias="memberVO" type="kr.or.ddit.vo.MemberVO"/>
<sql id="selectAll"> SELECT * FROM MEMBER WHERE MEM_DELETE = 'n' </sql>
<select id ="memberInfo" parameterClass="map" resultClass="memberVO"> <include refid="selectAll"/> AND MEM_ID = #mem_id# <dynamic prepend="AND"> <isNotEmpty property="mem_pass"> MEM_PASS = #mem_pass# </isNotEmpty> </dynamic> </select>
</sqlMap> |
cs |