Synchronized Token

Anyframe Web에서는 Synchronized Token 이라는 진보된 방법을 통해 선언적으로 Form submit 중복으로 인한 오 동작을 방지할 수 있는 기능을 제공한다.

Form Submit 중복은 다음과 같은 다양한 경우에 발생할 수 있다.
Form Submit 중복의 사례
  • Browser Refresh Button을 사용한 경우
  • Browser Back Button을 사용하여 이전 Page로 이동 후 다시 Form Submit 하는 경우
  • Browser History을 사용하여 이전 Page로 이동 후 다시 Form Submit 하는 경우
  • 서버에 영향을 주도록 고의적으로 악의적인 Form Submit을 하는 경우
  • submit Button을 한번 이상 Click 하는 경우
Browser Refresh Button을 사용한 경우에 submit이 다시 발생하는 이유는 Form Submit 이후에 Browse URL bar에 보이는 URL에 있다. "<form name=test action='/submitForm.do'>" 와 같은 경우를 생각해 보면, 이 Form이 전송된 이후에 Browse URL Bar에 '/submitForm.do' 가 남아있는 것을 볼 수가 있다. 이 상황에서 Refresh Button을 누르면 동일한 URL이 재전송 되는 것이다. 이러한 상황을 막는 가장 손쉬운 방법은 Form Submit 후에 HTTP Redirect 기능을 사용하는 것이다. 만일 위의 Form 전송 후에 보여 주는 Page가 success.jsp 라고 하면, HTTP Redirect를 사용할 경우 Browse URL bar에 success.jsp가 보일 것이다. 이 경우에는 Refresh Button을 눌러도 success.jsp가 다시 로드 된다.
<forward name="success" path="/Success.jsp" redirect="true" /> 이렇게 하므로써 우연히 Browser Refresh Button을 눌렀을 때의 동작 오류를 방지 할 수 있다. 하지만 Browser Back Button 등을 사용한 Form Resubmit을 근원적으로 막지는 못한다.
Anyframe Web 는 DefaultActionMapping 클래스를 통해 선언적으로 Synchronzed Token을 설정할 수 있으며 DefaultActionSupport 를 상속하여 구현한 Action 클래스에는 Synchronzed Token 처리가 기본으로 포함되어 있어 Form Submit 중복의 문제를 효과적이고 쉽게 해결할 수 있다.
다음은 Token 처리를 통한 중복 Submit 방지의 개념도이다.

일반적인 Token 처리 직접 구현 시
일반적으로 Action 클래스에서 다음과 같은 로직을 통해 Form 중복 submit을 체크한다.
<<Action>>
boolean valid = isTokenValid(request, true);
if (valid) {
    // TODO: submit 할 때 수행할 로직을 넣을 것
    System.out.println("status: performed");
} else {
// TODO: init / reload 할 때 수행할 로직을 넣을 것
    System.out.println("status: initialized or reloaded");
}
saveToken(request);
<<JSP>>
<input type="hidden" name="org.apache.struts.taglib.html.TOKEN" 
value="<%= session.getAttribute(org.apache.struts.Globals.TRANSACTION_TOKEN_KEY) %>">
UI(JSP, HTML)에서는 "org.apache.struts.taglib.html.TOKEN"를 Key로 하는 Hidden Field를 통해 Token 값을 서버로 전송하고, 해당 Action에서는 이 값을 isTokenValid (…) method를 통해Session에 저장된 값과 비교하여 유효성을 검사한다.
중복 Submit 방지처리가 필요한 모든 Action과 JSP에서 동일한 처리를 해주어야 하는 이 부분에 대하여 Anyframe Web에서는 DefaultActionSupport 클래스와 DefaultActionMapping 클래스를 통해 선언적으로 Sychronized Token을 사용할 수 있는 기능을 제공한다.

ActionMapping 클래스

    DefaultActionMapping 클래스는 ActionMapping 클래스를 확장 하여 해당 Action의 Synchronized Token필요 여부를 struts-config.xml에서 설정 할 수 있도록 한다. DefaultActionMapping의 소스코드는 다음과 같다.
    public class DefaultActionMapping extends ActionMapping {
    boolean validateToken = false;
    boolean resetToken = false;
    boolean saveToken = false;
    
    public DefaultActionMapping() {
            super();
            setScope("request");
    }
    
    public boolean isResetToken() { return resetToken; }
    public boolean isValidateToken() {return validateToken;}
    public void setResetToken(boolean b) {resetToken = b;}
    public void setValidateToken(boolean b) {validateToken = b; }
    public boolean isSaveToken() {return saveToken;}
    public void setSaveToken(boolean b) {saveToken = b;}
    }

Struts-Config.xml 설정

    Struts-Config.xml에서 DefaultActionMapping 클래스를 기본 action mapping 클래스로 지정하고 Double Submit을 방지하려는 각 Action Path에 synchronized Token 관련 설정을 추가한다.
  • saveToken
  • 해당 Action 수행 후, token key를 generate 하고 이 token을 Session에 저장할지에 대한 설정
  • validateToken
  • 해당 Action 수행 전, token validation check의 수행여부에 대한 설정
  • resetToken
  • token validation 후에 session에 저장된 token key값에 대하여 reset 여부 설정

    Samples

    <action-mappings type="anyframe.web.struts.common.action.DefaultActionMapping">
    <!--  사용자 회원 정보 수정 화면 이동 -->	    	    
    <action
    	input="/empListUser.do"
    	name="userForm"
    	path="/empGetUser"
    	type="com.sds.emp.user.web.GetUserAction"
    	scope="request"
    	validate="false"
    	roles="admin,user">
    	<set-property property="saveToken" value="true"/>
    	<forward name="success" path="/sample/user/updateUser.jsp"  />
    </action>	 
      
    <!-- 회원 정보 수정  -->
    <action
    	input="/sample/user/updateUser.jsp"
    	name="userForm"
    	path="/empUpdateUser"
    	type="com.sds.emp.user.web.UpdateUserAction"
    	scope="request"
    	validate="false"
    	roles="admin,user">
    	<set-property property="validateToken" value="true"/>	          
    	<set-property property="resetToken" value="true"/>
    	<forward name="success_list" path="/empListUser.do"  />
    	<forward name="success_update" path="/empGetUser.do"  />
    </action>
    … 하략 … 

    위의 세가지 프로퍼티와 DefaultActionSupport 클래스가 어떠한 매커니즘을 갖는지는 다음절에서 상세히 설명한다.

DefaultActionSupprot와의 연계 매커니즘

    Data의 저장에 대하여 Double Submit을 방지하고자 하는 경우의 매커니즘은 다음과 같다.
    대부분의 UI의 경우, 어떤 Data를 저장하려고 할 때, 그 Data의 저장하기 위하여 먼저입력화면으로 가는 Action(대부분 ForwardAction을 사용한다. )이 존재하고, 해당 Data의 실제로 저장하는 Action이 존재한다.
  • Data의 입력화면으로 가는 Action Path에 saveToken=true 설정
  • Data의 입력화면으로 가는 Action의 action path (struts-config.xml) 에
    <set-property property="saveToken" value="true"/>
    으로 설정한다.
    DefaultActionSupport의 postProcess() method에서는 위에서 지정한 saveToken이 true일 때 Action 클래스의 saveToken() method를 호출하여 token key를 생성하고 이 token key를 Session에 저장한다.
    public ActionForward postProcess(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response,
            ActionForward forward) throws Exception {
        boolean saveToken = false;
        if (mapping instanceof DefaultActionMapping) {
            DefaultActionMapping t_mapping = (DefaultActionMapping) mapping;
            saveToken = t_mapping.isSaveToken();
        }
        if (saveToken) {
            saveToken(request);
        }
        return forward;
    }
  • 해당 Data를 저장하는 Action Path에 validateToken, resetToken=true 설정
  • 그 다음으로 Data를 저장하는 Action에 대한 action path(struts-config.xml)에
    <set-property property="validateToken" value="true"/>
    <set-property property="resetToken" value="true"/>
    를 설정한다.
    DefaultActionSupport의 preProcess() method에서는 위에서 지정한 validateToken이 true이면 Action 클래스의 isTokenValid() method를 호출하여 token에 대한 validation check를 수행한다. 이때 resetToken 값이 true이면 session에 저장된 token값을 지우게 된다.
    public void preProcess(ActionMapping mapping, ActionForm form,
             HttpServletRequest request, HttpServletResponse response)
             throws Exception {
    
        boolean validateToken = false;
        boolean resetToken = false;
    
        if (mapping instanceof DefaultActionMapping) { 
            DefaultActionMapping t_mapping = (DefaultActionMapping) mapping;
            validateToken = t_mapping.isValidateToken();
            resetToken = t_mapping.isResetToken();
        }
        
        if (validateToken) {
            if (!isTokenValid(request, resetToken)) {
                throw new InvalidTokenException("common.msg.invalidtoken.error");
           }
        }
    }
  • <html:form> tag의 사용
  • 위와 같이 struts-config.xml을 설정한 후, JSP에서 Struts Tag Library <html:form> 태그를 사용하여 Form을 생성하는 경우, 별도의 설정이 필요 없이 Form Resubmit에 대한 처리를 할 수 가 있다. 만일 Struts Tag Library <html:form> 태그를 사용하지 않고 Form Submit을 하는 경우 Form 태그 내에 다음과 같은 Hidden Field를 추가해야 한다.
    <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" 
    value="<%= session.getAttribute(org.apache.struts.Globals.TRANSACTION_TOKEN_KEY) %>">
  • DefaultForwardAction의 사용
  • 단순 페이지 이동을 위해 Struts actions Package 로 미리 제공되는 ForwardAction 을 사용할 경우가 많다. 이런 경우에도 입력화면으로 가기 전에 Sychronized Token 처리를 위한 설정이 필요하므로 DefaultActionSupport를 상속하여 단순 포워드 용도로 Anyframe Web 에서 미리 만들어 제공하는 DefaultForwardAction을 사용토록 한다.
    <!--  신규 회원 등록 화면 이동 -->
    <action path="/empAddUserView"
    	type="anyframe.web.struts.common.action.DefaultForwardAction"
    	parameter="addUserView">
    	<set-property property="saveToken" value="true"/>           	           
    </action>

Resources