일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- Eclipse
- PLSQL
- phonegap
- 전자정부프레임워크
- jQuery
- ibsheet
- dock
- MFC
- rowspan
- Ajax
- PHP
- jsr 296
- Android
- WebLogic
- Spring
- node.js
- oracle
- Struts
- GPS
- JSON
- JDOM
- 가우스
- MySQL
- appspresso
- 선택적조인
- iBATIS
- swingx
- sencha touch
- Google Map
- tomcat
Archives
- Today
- Total
Where The Streets Have No Name
이중 submit막기 본문
Database를 다루다 보면 종종 이중 중복되어 insert되는 경우가 있다. 아니면 쇼핑몰에서 이중으로 주문이 되는 경우가 있다. 이는 사이트에서 전체 프로세스를 개발자가 100% 컨트롤할 수 없고 사용자가 interrupt할 수 있기때문에 발생하는 문제이다.
즉 사용자가 F5를 누르는 것을 우리는 컨트롤할 수 없다.
그러나 이를 극복하기 위해서는 사용자가 F5를 누르거나 Back 버튼을 눌렀을 때 이중으로 처리되지 않게 처리하면 된다. 그 방법은 간단하다. 데이터를 submit하기 전에 간단한 유일한 문자열 token을 심어주고 성공적인 처리 후 이 token을 다른 값으로 바꾼다. 그리고 submit을 처리하는 순간 두 값을 비교하여 다른 경우 처리를 하지 않게 하는 것이다.
여기서는 하나의 문자열을 서블릿으로 submit하고 서블릿에서 jsp 문서로 forward하여 "Hello~ 문자열"을 출력할 것이다. 만일 F5를 눌러 재시도시 "Hello~ 문자열" 대신 "Hellow~ theclub"을 출력할 것이다.
파일은 모두 5개이다.
1. TokenFormjava : Token을 설정하고 TokenForm.jsp으로 forward한다.
1. TokenForm.jsp : 문자열을 넘길 form을 가진 jsp 문서
2. Hellow.java : 문자열을 받아 Token의 유효 여부에 따라 name 변수를 request에 설정한다.
3. Hellow.jsp : "Hellow~ 문자열" 또는 "Hellow~ theclub"을 출력한다.
4. Token.java : Token을 셋팅하고 두개의 token이 일치하는지 여부를 결정하는 메쏘드가 있다.
우선 TokenForm.java은 다음과 같다.
===== TokenForm.java =====
import javax.servlet.*;
import javax.servlet.http.*;
import TokenTool.Token;
public class TokenForm extends HttpServlet{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException{
Token.set(request);
String target="/TokenForm.jsp";
RequestDispatcher dispatcher;
dispatcher = getServletContext().getRequestDispatcher(target);
try {
dispatcher.forward(request, response);
}catch(Exception e){
System.err.println(e);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException{
doGet(request, response);
}
}
=========================
일단 TokenTool.Token 이라는 핵심 클래스를 import한다. 이 클래스는 나중에 자세히 다룰 것이다.
Token.set(request);
위의 줄은 Token 클래스의 set 매소드를 실행시키는 것으로 SESSIONID와 시간을 이용해서 유일한 문자열을 생성한 후 request와 session에 각각 token이라는 변수명으로 저장한다.
아래의 코드 중 target이라는 String 변수는 TokenForm.jsp라는 form을 통한 문자열을 넘기기 위해서 마련된 파일이다. 이 파일로 RequestDispatcher를 사용하여 forward시킨다. 이 forward는 redirect와 달라 브라우저의 url이 바뀌지 않는다. 그러므로 inclue와 비슷한 효과를 낼 수 있다. RequestDispatcher를 사용한 이유는 request와 response를 넘기기 위해서 이다. RequestDispatcher를 이용하면 request와 response를 넘길 수 있다.
String target="/TokenForm.jsp";
RequestDispatcher dispatcher;
dispatcher = getServletContext().getRequestDispatcher(target);
try {
dispatcher.forward(request, response);
}catch(Exception e){
System.err.println(e);
}
이렇게 하면 브라우저의 url은 TokenForm이면서 화면에는 name을 입력하는 화면이 나올 것이다.
===== TokenForm.jsp =====
<html>
<head>
<title>JspGeek.com</title>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
</head>
<body>
<form method="post" action="Hellow">
name : <input type="text" name="name">
<input type="hidden" name="token" value="<%=request.getAttribute("token")%>">
<input type="submit" value="submit">
<%=request.getAttribute("token")%>
</form>
</body>
</html>
=========================
여기서는 별 게 없고 단지 TokenForm.java의 Token.set(request)을 통하여 request에 저장한 token의 값을 <%=request.getAttribute("token")%>으로 가져와, Token이라는 hidden 필드에 값을 넣는다. <%=request.getAttribute("token")%>는 현재 설정된 token의 값을 알아 보기 위한 것이다. 이 화면에서 F5를 누르게 되면 이 token 값이 변하는 것을 알 수 있는데 그 이유는 token의 값을 생성할 경우 시간을 이용하므로 시간이 변하는 만큼 값도 변하게 된다.
여기서 이름을 넣고 submit버튼을 눌러 보자. 그러면 action="Hellow"에 따라 Hellow.java 파일로 데이터가 넘어가게 된다.
===== Hellow.java =====
import javax.servlet.*;
import javax.servlet.http.*;
import TokenTool.Token;
public class Hellow extends HttpServlet{
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException{
if(Token.isValid(request)){
request.setAttribute("name", request.getParameter("name"));
Token.set(request);
}
else {
request.setAttribute("name", "theclub");
}
String target="/Hellow.jsp";
RequestDispatcher dispatcher;
dispatcher = getServletContext().getRequestDispatcher(target);
try {
dispatcher.forward(request, response);
}catch(Exception e){
System.out.println(e);
}
}
}
=======================
여기서도 역시 TokenTool.Token를 import한다.
Token.isValid(request)의 유효성에 따라 request에 name 변수의 값을 다르게 지정한다. 즉 Token.isValid(request)가 true인 경우 위 form에서 입력한 문자열을 request에 저장한다. 다음 코드를 사용하면 된다.
request.setAttribute("name", request.getParameter("name"));
그리고 Token.isValid(request)가 false인 경우, 즉 F5를 눌러 submit된 정보를 다시 submit하려고 할 경우에 request의 name 변수에 값으로 theclub을 저장해서, 한번 submit된 이후에는 항상 "Hellow~ theclub"이 나오도록 한다. 그러므로 Token.isValid(request)는 처음 submit일 경우 true를 두 번째 submit 부터는 false를 반환한다.
그 이후 부분은 위에서 설명한 것처럼 "Hellow~ 문자열"을 보여 줄 Hellow.jsp로 RequestDispatcher한다.
* 핵심 포인트 *
이 코드에서 아주 중요한 줄이 있는데 다음과 같다.
Token.set(request);
Token.isValid(request)는 첫번째 submit인지 두번째부터 submit인지를 판단한다고 하였다. 그 원리는 간단하다. TokenForm.java에서 Token.set(request);에 의해서 token을 만들고 이 token을 TokenForm.jsp에서 hidden으로 넘긴다. 그러므로 여기까지는 session에 있는 token 값이나 hidden으로 넘어가는 token값이 같게 된다. 이럴 경우는 첫번째 submit으로 판단한다. 그러므로 이 두개의 값이 다를 경우 이 submit을 첫 번째 submit으로 판단하지 않는 것이다. 그러므로 다시 submit을 하려고 할 경우 즉 form의 hidden에 의해서 token을 넘어 가고 있을 경우 session에 있는 token 값이 다르다면 첫 번째 submit이 아님을 알 수 있다. 그러므로 첫번째 submit 순간에는 session에 있는 token 값이나 hidden에 있는 token 값이나 같을지라도, request에 name이라는 변수에 값을 넣는 바로 다음에(아래 코드를 이용해서)
request.setAttribute("name", request.getParameter("name"));
session에 있는 token 값을 Update한다. 그 코드가 바로 다음 코드이다.
Token.set(request);
database에 insert할 경우 성공한 바로 다음에 이 작업을 해 주어야 한다.
만일 이 작업이 이루어지지 않을 경우 hidden에 있는 token 값이나 session에 있는 token 값이나 같으므로 계속 submit 할 수 있게 된다.
===== Hellow.jsp =====
<html>
<head>
<title>JspGeek.com</title>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
</head>
<body>
<%
if(request.getAttribute("name")!=null){
out.println("Hello~ "+request.getAttribute("name")+"<br>");
}
else {
out.println("No Names");
}
%>
</body>
</html>
======================
if(request.getAttribute("name")!=null)는 request에 name이라는 변수가 있는지 확인 한다. 즉 !=null로 null이 아닌 경우를 말한다. 그 경우 아래 코들 통하여 "Hello~ 문자열"로 인사를 하게 된다.
out.println("Hello~ "+request.getAttribute("name")+"<br>");
그러나 name이라는 문자열이 없는 경우 out.println("No Names");로 No Names 문자열을 출력한다.
아래의 Token.java를 덧붙인다. 이 클래스에 대해서는 다음 시간에 자세히 다루도록 할 것이다.
==== Token.java =====
package TokenTool;
import javax.servlet.http.*;
import java.security.*;
public class Token {
public static void set(HttpServletRequest req) {
HttpSession session = req.getSession(true);
long systime = System.currentTimeMillis();
byte[] time = new Long(systime).toString().getBytes();
byte[] id = session.getId().getBytes();
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(id);
md5.update(time);
String token = toHex(md5.digest());
req.setAttribute("token", token);
session.setAttribute("token", token);
}
catch (Exception e) {
System.err.println("Unable to calculate MD5 Digests");
}
}
public static boolean isValid(HttpServletRequest req) {
HttpSession session = req.getSession(true);
String requestToken = req.getParameter("token");
String sessionToken = (String)session.getAttribute("token");
if (requestToken == null || sessionToken == null)
return false;
else
return requestToken.equals(sessionToken);
}
private static String toHex(byte[] digest) {
StringBuffer buf = new StringBuffer();
for (int i=0; i < digest.length; i++)
buf.append(Integer.toHexString((int)digest[i] & 0x00ff));
return buf.toString();
}
}
=====================
즉 사용자가 F5를 누르는 것을 우리는 컨트롤할 수 없다.
그러나 이를 극복하기 위해서는 사용자가 F5를 누르거나 Back 버튼을 눌렀을 때 이중으로 처리되지 않게 처리하면 된다. 그 방법은 간단하다. 데이터를 submit하기 전에 간단한 유일한 문자열 token을 심어주고 성공적인 처리 후 이 token을 다른 값으로 바꾼다. 그리고 submit을 처리하는 순간 두 값을 비교하여 다른 경우 처리를 하지 않게 하는 것이다.
여기서는 하나의 문자열을 서블릿으로 submit하고 서블릿에서 jsp 문서로 forward하여 "Hello~ 문자열"을 출력할 것이다. 만일 F5를 눌러 재시도시 "Hello~ 문자열" 대신 "Hellow~ theclub"을 출력할 것이다.
파일은 모두 5개이다.
1. TokenFormjava : Token을 설정하고 TokenForm.jsp으로 forward한다.
1. TokenForm.jsp : 문자열을 넘길 form을 가진 jsp 문서
2. Hellow.java : 문자열을 받아 Token의 유효 여부에 따라 name 변수를 request에 설정한다.
3. Hellow.jsp : "Hellow~ 문자열" 또는 "Hellow~ theclub"을 출력한다.
4. Token.java : Token을 셋팅하고 두개의 token이 일치하는지 여부를 결정하는 메쏘드가 있다.
우선 TokenForm.java은 다음과 같다.
===== TokenForm.java =====
import javax.servlet.*;
import javax.servlet.http.*;
import TokenTool.Token;
public class TokenForm extends HttpServlet{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException{
Token.set(request);
String target="/TokenForm.jsp";
RequestDispatcher dispatcher;
dispatcher = getServletContext().getRequestDispatcher(target);
try {
dispatcher.forward(request, response);
}catch(Exception e){
System.err.println(e);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException{
doGet(request, response);
}
}
=========================
일단 TokenTool.Token 이라는 핵심 클래스를 import한다. 이 클래스는 나중에 자세히 다룰 것이다.
Token.set(request);
위의 줄은 Token 클래스의 set 매소드를 실행시키는 것으로 SESSIONID와 시간을 이용해서 유일한 문자열을 생성한 후 request와 session에 각각 token이라는 변수명으로 저장한다.
아래의 코드 중 target이라는 String 변수는 TokenForm.jsp라는 form을 통한 문자열을 넘기기 위해서 마련된 파일이다. 이 파일로 RequestDispatcher를 사용하여 forward시킨다. 이 forward는 redirect와 달라 브라우저의 url이 바뀌지 않는다. 그러므로 inclue와 비슷한 효과를 낼 수 있다. RequestDispatcher를 사용한 이유는 request와 response를 넘기기 위해서 이다. RequestDispatcher를 이용하면 request와 response를 넘길 수 있다.
String target="/TokenForm.jsp";
RequestDispatcher dispatcher;
dispatcher = getServletContext().getRequestDispatcher(target);
try {
dispatcher.forward(request, response);
}catch(Exception e){
System.err.println(e);
}
이렇게 하면 브라우저의 url은 TokenForm이면서 화면에는 name을 입력하는 화면이 나올 것이다.
===== TokenForm.jsp =====
<html>
<head>
<title>JspGeek.com</title>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
</head>
<body>
<form method="post" action="Hellow">
name : <input type="text" name="name">
<input type="hidden" name="token" value="<%=request.getAttribute("token")%>">
<input type="submit" value="submit">
<%=request.getAttribute("token")%>
</form>
</body>
</html>
=========================
여기서는 별 게 없고 단지 TokenForm.java의 Token.set(request)을 통하여 request에 저장한 token의 값을 <%=request.getAttribute("token")%>으로 가져와, Token이라는 hidden 필드에 값을 넣는다. <%=request.getAttribute("token")%>는 현재 설정된 token의 값을 알아 보기 위한 것이다. 이 화면에서 F5를 누르게 되면 이 token 값이 변하는 것을 알 수 있는데 그 이유는 token의 값을 생성할 경우 시간을 이용하므로 시간이 변하는 만큼 값도 변하게 된다.
여기서 이름을 넣고 submit버튼을 눌러 보자. 그러면 action="Hellow"에 따라 Hellow.java 파일로 데이터가 넘어가게 된다.
===== Hellow.java =====
import javax.servlet.*;
import javax.servlet.http.*;
import TokenTool.Token;
public class Hellow extends HttpServlet{
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException{
if(Token.isValid(request)){
request.setAttribute("name", request.getParameter("name"));
Token.set(request);
}
else {
request.setAttribute("name", "theclub");
}
String target="/Hellow.jsp";
RequestDispatcher dispatcher;
dispatcher = getServletContext().getRequestDispatcher(target);
try {
dispatcher.forward(request, response);
}catch(Exception e){
System.out.println(e);
}
}
}
=======================
여기서도 역시 TokenTool.Token를 import한다.
Token.isValid(request)의 유효성에 따라 request에 name 변수의 값을 다르게 지정한다. 즉 Token.isValid(request)가 true인 경우 위 form에서 입력한 문자열을 request에 저장한다. 다음 코드를 사용하면 된다.
request.setAttribute("name", request.getParameter("name"));
그리고 Token.isValid(request)가 false인 경우, 즉 F5를 눌러 submit된 정보를 다시 submit하려고 할 경우에 request의 name 변수에 값으로 theclub을 저장해서, 한번 submit된 이후에는 항상 "Hellow~ theclub"이 나오도록 한다. 그러므로 Token.isValid(request)는 처음 submit일 경우 true를 두 번째 submit 부터는 false를 반환한다.
그 이후 부분은 위에서 설명한 것처럼 "Hellow~ 문자열"을 보여 줄 Hellow.jsp로 RequestDispatcher한다.
* 핵심 포인트 *
이 코드에서 아주 중요한 줄이 있는데 다음과 같다.
Token.set(request);
Token.isValid(request)는 첫번째 submit인지 두번째부터 submit인지를 판단한다고 하였다. 그 원리는 간단하다. TokenForm.java에서 Token.set(request);에 의해서 token을 만들고 이 token을 TokenForm.jsp에서 hidden으로 넘긴다. 그러므로 여기까지는 session에 있는 token 값이나 hidden으로 넘어가는 token값이 같게 된다. 이럴 경우는 첫번째 submit으로 판단한다. 그러므로 이 두개의 값이 다를 경우 이 submit을 첫 번째 submit으로 판단하지 않는 것이다. 그러므로 다시 submit을 하려고 할 경우 즉 form의 hidden에 의해서 token을 넘어 가고 있을 경우 session에 있는 token 값이 다르다면 첫 번째 submit이 아님을 알 수 있다. 그러므로 첫번째 submit 순간에는 session에 있는 token 값이나 hidden에 있는 token 값이나 같을지라도, request에 name이라는 변수에 값을 넣는 바로 다음에(아래 코드를 이용해서)
request.setAttribute("name", request.getParameter("name"));
session에 있는 token 값을 Update한다. 그 코드가 바로 다음 코드이다.
Token.set(request);
database에 insert할 경우 성공한 바로 다음에 이 작업을 해 주어야 한다.
만일 이 작업이 이루어지지 않을 경우 hidden에 있는 token 값이나 session에 있는 token 값이나 같으므로 계속 submit 할 수 있게 된다.
===== Hellow.jsp =====
<html>
<head>
<title>JspGeek.com</title>
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
</head>
<body>
<%
if(request.getAttribute("name")!=null){
out.println("Hello~ "+request.getAttribute("name")+"<br>");
}
else {
out.println("No Names");
}
%>
</body>
</html>
======================
if(request.getAttribute("name")!=null)는 request에 name이라는 변수가 있는지 확인 한다. 즉 !=null로 null이 아닌 경우를 말한다. 그 경우 아래 코들 통하여 "Hello~ 문자열"로 인사를 하게 된다.
out.println("Hello~ "+request.getAttribute("name")+"<br>");
그러나 name이라는 문자열이 없는 경우 out.println("No Names");로 No Names 문자열을 출력한다.
아래의 Token.java를 덧붙인다. 이 클래스에 대해서는 다음 시간에 자세히 다루도록 할 것이다.
==== Token.java =====
package TokenTool;
import javax.servlet.http.*;
import java.security.*;
public class Token {
public static void set(HttpServletRequest req) {
HttpSession session = req.getSession(true);
long systime = System.currentTimeMillis();
byte[] time = new Long(systime).toString().getBytes();
byte[] id = session.getId().getBytes();
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(id);
md5.update(time);
String token = toHex(md5.digest());
req.setAttribute("token", token);
session.setAttribute("token", token);
}
catch (Exception e) {
System.err.println("Unable to calculate MD5 Digests");
}
}
public static boolean isValid(HttpServletRequest req) {
HttpSession session = req.getSession(true);
String requestToken = req.getParameter("token");
String sessionToken = (String)session.getAttribute("token");
if (requestToken == null || sessionToken == null)
return false;
else
return requestToken.equals(sessionToken);
}
private static String toHex(byte[] digest) {
StringBuffer buf = new StringBuffer();
for (int i=0; i < digest.length; i++)
buf.append(Integer.toHexString((int)digest[i] & 0x00ff));
return buf.toString();
}
}
=====================