现在的网站在注册提交步骤中,由于后台要处理大量信息,造成响应变慢,在前端页面提交信息之前,等待后端响应,此时如果用户
再点一次提交按钮,后台会保存多份用户信息。为解决此问题,借鉴了struts2的token思路,在springmvc下实现token。
实现思路:
在springmvc配置文件中加入拦截器的配置,拦截两类请求,一类是到页面的,一类是提交表单的。当转到页面的请求到来时,生成token的名字和token值,一份放到session中(如果有集群部署需要放到redis缓存中,保证token的存储介质是全局线程安全的),一份放传给页面表单的隐藏域。
当表单请求提交时,拦截器得到参数中的tokenName和token,然后到缓存中去取token值,如果能匹配上,请求就通过,不能匹配上就不通过。这里的tokenName生成时使用了GUID也是随机的,每次请求都不一样。而从缓存中取token值时,会立即将其删除(删与读是原子的,无线程安全问题)。
Before proceeding to the implementation details few things to notice:
- I assume (and this is the way it should be!) that only POST requests change the application state
- When discussing CSRF we often hear a sentence like: "but the attacker can use JavaScript to read your form structure and understand what the secret token is – so this CSRF token is actually useless". In practice the browser's same origin policy makes it very difficult for the attacker to read the CSRF token using JavaScript originated from his site.
Implementation Overview
I would like include a session private CSRF token in any form rendered to the UI and to enforce the existence and validity of that token on each POST request arriving to the application – so we basically have two components: out-bound form enrichment and in-bound request validation. The solution is fully automatic: once configured into the application all forms and POST requests will be CSRF secured without the need for any explicit action to be taken by application developers. In my solution the CSRF token will be HTTP session scoped - each session will have its own CSRF token valid to the entire session.
CSRFTokenManager.java
This is a utility class, used by both the in-bound and out-bound components. The class is responsible for managing the CSRF token for HTTP sessions. The key method in the class is getTokenForSession.
The getTokenForSession method checks for the existence of a CSRF token as an attribute on an HTTP session, if one exists it returns its value otherwise it generates the session token, store it on the session and returns the token value to the caller. The method must synchronize on the session otherwise we might end with a caller getting a token which is no longer valid for the session (if more than one request trying to access the method concurrently and a token was not generated for the session yet). In my usage the token is a random GUID but any other random value is valid.
public class CSRFTokenManager {
/**
* The token parameter name
*/
static final String CSRF_PARAM_NAME = "CSRFToken";
/**
* The location on the session which stores the token
*/
private final static String CSRF_TOKEN_FOR_SESSION_ATTR_NAME = CSRFTokenManager.class.getName() + ".tokenval";
static String getTokenForSession (HttpSession session) {
String token = null;
// I cannot allow more than one token on a session - in the case of two requests trying to
// init the token concurrently
synchronized (session) {
token = (String) session.getAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME);
if (null==token) {
token=UUID.randomUUID().toString();
session.setAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME, token);
}
}
return token;
}
/**
* Extracts the token value from the session
* @param request
* @return
*/
static String getTokenFromRequest(HttpServletRequest request) {
return request.getParameter(CSRF_PARAM_NAME);
}
private CSRFTokenManager() {};
}
Form Rendering (out-bound)
I have to make sure that any form rendered using my Spring MVC based application will include the CSRF token as a hidden field. I do this by implementing the getExtraHiddenFields() method of the org.springframework.web.servlet.support.RequestDataValueProcessor interface (remember since Spring 3.1). By implementing this method my class gets the opportunity to add hidden fields to any form rendered using Spring's form tag (<form:form....), obviously I will add a field with the CSRF token.
public class CSRFRequestDataValueProcessor implements RequestDataValueProcessor {
...
@Override
public Map<String,String> getExtraHiddenFields(HttpServletRequest request) {
Map<String,String> hiddenFields = new HashMap<String,String>();
hiddenFields.put(CSRFTokenManager.CSRF_PARAM_NAME,
CSRFTokenManager.getTokenForSession(request.getSession()));
return hiddenFields;
}
}
use spring form tag in page that will be submit to controller:
<form:form modelAttribute="customer" action="save">
Still not done, for the processor to be invoked by Spring it has to be registered to Spring's RequestContext, the easiest way of doing that is to register an instance of my CSRFRequestDataValueProcessor as bean named 'requestDataValueProcessor' in the Bean Factory.
<!-- Data Value Processor -->
<beans:bean name="requestDataValueProcessor" class="com.gl.csrf.processor.CSRFRequestDataValueProcessor"/>
Enforcing CSRF Token Validity for Incoming POST Request (in-bound)
The last part is to make sure that each incoming POST request includes a valid CSRF token for the session to which the request belongs. Usually the first approach JEE developers would adopt is to use a Servlet filter which checks to see if the current request is a POST one and if so it validates the existence of the CSRF token and its content. The issue with that approach is the fact that the Servlet filter processing takes place before the request is routed to Spring's DispatcherServlet. In a multipart encoded forms (multipart/form-data) use case this would be proven wrong: since Spring has its own strategy to process multipart requests (look for MultipartResolver and MultipartHttpServletRequest in Spring's source) processing the request before Spring does will collide with Spring.
A more 'Spring like' way of doing so is using a HandlerInterceptor. Spring handler interceptors can be registered to add common pre or post processing to controllers. Unlike the Servlet filter those interceptors are a part of the Spring MVC request life cycle and it is fully synchronized with both multipart and simple (application/x-www-form-urlencoded) forms.
public class CSRFHandlerInterceptor extends HandlerInterceptorAdapter {
...
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (!request.getMethod().equalsIgnoreCase("POST") ) {
// Not a POST - allow the request
return true;
} else {
// This is a POST request - need to check the CSRF token
String sessionToken = CSRFTokenManager.getTokenForSession(request.getSession());
String requestToken = CSRFTokenManager.getTokenFromRequest(request);
if (sessionToken.equals(requestToken)) {
return true;
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad or missing CSRF value");
return false;
}
}
}
The last step is to register the interceptor into Spring's processing chain:
<!-- Interceptor handlers -->
<mvc:interceptors>
<beans:bean class="com.gl.csrf.processor.CSRFHandlerInterceptor"/>
</mvc:interceptors>
for all source code link is here.