공공기관 프로젝트 진행 도중,
단순 로그인 기능만으로는 보안에 취약할 수 있다는 권고 지적을 받았습니다..
확실히 일반 프로젝트에 비해서 공공기간 프로젝트가 여러 심의를 거치는 듯 했습니다.
그리고 아래와 같이 조치 권고 사항으로 2차 인증 시스템을 적용하라는 문구까지 친절히 설명되어 있습니다......
항상 느끼는 것이지만 프로젝트는 끝나도 끝이 아니네요 ㅜㅜ
(RSA 암호화 방식을 이용하여 평문 전송 이슈 해결방법을 확인하시려면 클릭해주세요)
그래서 2차 인증에 대해 고민해보다가 구글에서 제공하는 OTP 기능을 적용해보려합니다.
구현화면입니다.
일회용 비밀번호가 실시간으로 변경 되는 것을 볼 수 있습니다.
OTP란 One Time Password의 의미로써 한마디로 일회용 비밀번호입니다.
보안성에 민감한 은행,공공기관에서 많이 사용되고 있으며, 흔히 은행에서 사용하는 OTP 기능을 생각하시면 됩니다.
구현방법은 서버에서 키값과 바코드 주소값을 생성해주면
구글에서 제공하는 Google Authenticator이라는 앱에 키값을 입력을 해줍니다. (은행 OTP 기기와 동일한 기능)
그러면 앱에서 입력한 키값에 대한 일회용 비밀번호를 제공해주고, 서버에서 비밀번호를 확인해주는 방식입니다.
실전 테스트 진행
우선 POM.XML에 메이븐 설정을 해주거나,
Base 나 Hex 포맷 등으로 변환을 지원하는 라이브러리인 Commons Codec.jar 파일을 설치해줍니다.
1
2
3
4
5
6
|
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
|
cs |
Controller (OTP 입력 폼)
여기서는 램덤으로 인증키를 생성하고 바코드 주소를 생성한 다음 otp.jsp에 내보내는 역활을 합니다.
이때, 바코드URL를 만드는 메소드인 getQRBarcodeURL 에서, user와 host 값은 다른 암호와 구분하기 위한 값으로
아무 값이나 넣어도 상관없습니다.
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
|
//OTP
@RequestMapping(value = "/account/otp.do")
public String otp(
@ModelAttribute("searchVO") LoginVO searchVO,
RedirectAttributes redirectAttributes,
HttpServletRequest request,
ModelMap model) throws Exception {
byte[] buffer = new byte[5 + 5 * 5];
new Random().nextBytes(buffer);
Base32 codec = new Base32();
byte[] secretKey = Arrays.copyOf(buffer, 10); //16자 이상이여하므로 10으로 설정 필요
byte[] bEncodedKey = codec.encode(secretKey);
//인증키 생성
String encodedKey = new String(bEncodedKey);
//바코드 주소 생성
String QrUrl = getQRBarcodeURL("admin", "facbank", encodedKey);
model.addAttribute("encodedKey", encodedKey);
model.addAttribute("QrUrl", QrUrl);
return "tiles:bsite/account/login/otp";
}
//바코드 생성 함수
public static String getQRBarcodeURL(String user, String host, String secret) {
String format = "http://chart.apis.google.com/chartdfdasfdsafsdafa";
return String.format(format, user, host, secret);
}
|
cs |
|
|
|
|
|
JSP (OTP 입력 폼)
여기서는 컨트롤러에서 만들어준 인증키와 바코드 주소를 뿌려주는 역활을 합니다.
그리고 코드값을 입력하여 관리자 메인페이지로 들어갈 수 있도록 설정해주었습니다.
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
|
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<c:set var="path" value="${pageContext.request.contextPath}" />
<script type="text/javascript">
$(function(){
var errorMsg = "${errorMsg}";
if(errorMsg != ""){
alert(errorMsg);
}
});
function frmCheck() {
if ($("#code").val() == "") {
alert("코드를 입력해주세요.");
$("#code").focus();
return false;
}
}
</script>
<div id="sign-body">
<div class="form-signin">
<div class="signin-box">
<div id="member-box" class="login-wrap text-cetner">
<div class="form-box ">
<!-- login-top -->
<div class="login-top">
<h3 class="mb_10 fw-normal" style="font-weight:bold;"><i class="xi-mouse"></i>OTP인증</h3>
</div>
<!-- login-top //-->
<!-- login-form -->
<div class="login-form">
<form action ="${path}/adms/stat/status/list.do" onsubmit="return frmCheck();">
<ul class="login">
<%--
키 인증 번호 : ${encodedKey } <br>
바코드 주소 : ${url } <br><br>
--%>
<p style="font-weight:bold;">키 인증 번호 :</p>
<input type="text" class="form-control" name="encodedKey" value="${encodedKey }" readonly="readonly"/>
<p style="font-weight:bold;">바코드 주소 :</p>
<input type="text" class="form-control" value="${QrUrl}" readonly="readonly"/></br>
<p style="font-weight:bold;">코드 입력창 :</p>
<input type="text" class="form-control" id="code" name="code" placeholder="코드를 입력해주세요" />
</ul>
<input type="submit" class="btn btn-lg btn-dark" value="전송" style="margin-top:10px;">
</form>
</div>
<!-- login-form //-->
</div>
</div>
</div>
</div>
</div>
|
cs |
Controller (OTP Action(확인) 부분)
여기서는 JSP에서 입력한 코드 값과 키값을 가지고 와서 비교를 해줍니다.
check_code값이 false일 경우 다시 키값을 생성해서 otp.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
|
//OTP 확인 컨트롤러
@RequestMapping(value ="/adms/stat/status/list.do")
public String statList(@ModelAttribute("searchVO") tbl_indivdualStatVO searchVO, HttpServletRequest request, ModelMap model) throws Exception {
try{
String code = request.getParameter("code");
long codeCheck = Integer.parseInt(code);
String encodedKey = request.getParameter("encodedKey");
long l = new Date().getTime();
long ll = l / 30000;
boolean check_code = false;
check_code = check_code(encodedKey, codeCheck, ll);
if(!check_code) {
model.addAttribute("errorMsg", "코드가 일치하지 않습니다.");
byte[] buffer = new byte[5 + 5 * 5];
new Random().nextBytes(buffer);
Base32 codec = new Base32();
byte[] secretKey = Arrays.copyOf(buffer, 10);
byte[] bEncodedKey = codec.encode(secretKey);
//인증키 생성
String encodedKey2 = new String(bEncodedKey);
//바코드 주소 생성
String QrUrl = getQRBarcodeURL("admin", "facbank", encodedKey2);
model.addAttribute("encodedKey", encodedKey2);
model.addAttribute("QrUrl", QrUrl);
return "tiles:bsite/account/login/otp";
}
}catch(Exception e){
System.out.println(e.toString());
}
return "tiles:adms/stat/status/list";
}
//바코드 생성 함수
public static String getQRBarcodeURL(String user, String host, String secret) {
String format = "http://chart.apis.google.com/chart?123123213213fadsdsaf";
return String.format(format, user, host, secret);
}
//코드 체크 함수
private static boolean check_code(String secret, long code, long t) throws InvalidKeyException, NoSuchAlgorithmException {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
int window = 3;
for (int i = -window; i <= window; ++i) {
long hash = verify_code(decodedKey, t + i);
if (hash == code) {
return true;
}
}
return false;
}
//코드 확인 함수
private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException{
byte[] data = new byte[8];
long value = t;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signKey);
byte[] hash = mac.doFinal(data);
int offset = hash[20 - 1] & 0xF;
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
return (int) truncatedHash;
}
|
cs |
여기까지하면 구현 끝입니다!!
참고로 저는 Google Authenticator의 PC버전을 이용하였습니다.
Google Authenticator의 PC버전 이용 방법은 아래에 별도로 준비하였습니다.
위에는 인증키 방식으로 OTP를 발급받았던 방식이라면
아래에는 바코드 주소를 통해 OTP를 발급받은 방식입니다.
구글 OTP (Google Authenticator) PC에서 사용하는 방법
아래 주소를 클릭해서 크롬 웹 스토어로 들어가 인증 도구인 Google Authenticator를 다운로드합니다.
https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai
망치 아이콘을 눌러서 인증도구에서 핀 아이콘을 클릭하면
구글작업상태창에 바코드 아이콘으로 고정이됩니다.
클릭해보면 인증도구 화면이 뜨고, 연필 아이콘을 클릭해줍니다.
그리고 플러스를 눌러주세요.
그럼 바코드코드를 스켄하는 것과
수동입력인 키값을 입력하는 창이 뜹니다.
둘중 원하시는 것을 선택하여 작업하시면 될 듯 합니다!
수동입력값 화면
바코드입력 화면
'JAVA' 카테고리의 다른 글
[Java&Jsp] 엑셀 데이터 추출 후, 뷰페이지 확인 방법!! (0) | 2021.10.22 |
---|---|
[JAVA] RSA 암호화 방식 적용 방법 (0) | 2021.09.02 |
[JAVA] 두 날짜 데이터값 계산하는 방법 (faet.D-DAY 계산) (0) | 2021.08.01 |
[Java] 자동 계약/청구 번호/주문번호 생성 방법!!! (0) | 2021.02.23 |
[JAVA] 자바에서 날짜 포맷 변경하는 방법 (0) | 2020.10.20 |
댓글