关于CSRF是什么东西,请参见我的博文:《浅析CSRF》。本文将给出一种在 SpringMVC3+Velocity 的框架下防御CSRF攻击的解决方案。主要思想参考 EYAL LUPU[1] 的解决方案,但使用Velocity宏来进行Token值的传递,而不是使用spring form标签。
在服务器端生成私有的会话级token,使用Velocity的宏命令在客户端的Form表单中插入这个token;在表单提交后对,服务器端对token值进行校验,根据结果来区分该次请求是否合法:正确就放行,不正确就就阻断请求。
在这里,我们约定:凡是更新资源的操作,都通过POST向服务器端发送。这也是符合HTTP规范的约定。
使用一个 CSRFTokenManager 类来进行token生成的工作,代码如下:
CSRFTokenManager.java
package com.javan.security; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * A manager for the CSRF token for a given session. The {@link #getTokenForSession(HttpSession)} should used to obtain * the token value for the current session (and this should be the only way to obtain the token value). */ public final class CSRFTokenManager { /** * The token parameter name */ static final String CSRF_PARAM_NAME = "xToken"; /** * The location on the session which stores the token */ public final static String CSRF_TOKEN_FOR_SESSION_ATTR_NAME = CSRFTokenManager.class.getName() + ".tokenval"; public static String getTokenForSession(HttpSession session) { String token = null; // 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 */ public static String getTokenFromRequest(HttpServletRequest request) { String token = request.getParameter(CSRF_PARAM_NAME); if (token == null || "".equals(token)) { token = request.getHeader(CSRF_PARAM_NAME); } return token; } private CSRFTokenManager() { }; }
getTokenForSession方法用于检查HTTP session中是否存在CSRF token:存在则返回;不存在则生成并存储到session中,然后返回。
由于我的前端使用 Velocity 进行渲染,并且不使用 Spring form标签,所以为了将token依附到Form表单中,我使用 Velocity 的宏指令进行渲染工作。
SpringMVC 中的配置,主要是配置 Velocity Tools:
mvc-context.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd"> <mvc:annotation-driven /> <context:component-scan base-package="com.javan.web.controller" /> <mvc:resources mapping="/resources/**" location="/resources/" cache-period="31556926" /> <mvc:default-servlet-handler /> <!-- CSRF Interceptor handlers --> <mvc:interceptors> <bean class="com.javan.security.CSRFHandlerInterceptor" /> </mvc:interceptors> <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer"> <property name="resourceLoaderPath" value="/WEB-INF/views/" /> <property name="velocityProperties"> <props> <prop key="input.encoding">utf-8</prop> <prop key="output.encoding">utf-8</prop> </props> </property> <property name="configLocation" value="/WEB-INF/velocity.properties" /> </bean> <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver"> <property name="cache" value="true" /> <property name="contentType" value="text/html;charset=utf-8" /> <property name="exposeSpringMacroHelpers" value="true" /> <property name="suffix"> <value>.vm</value> </property> <property name="toolboxConfigLocation" value="WEB-INF/toolbox.xml" /> <property name="exposeSessionAttributes" value="true" /> </bean> </beans>
Toolbox.xml中定义生成 CSRF token 的 tool:
toolbox.xml
<?xml version="1.0" encoding="UTF-8"?> <toolbox> <tool> <key>csrfTool</key> <scope>application</scope> <class>com.javan.util.CSRFTool</class> </tool> </toolbox>
工具类CSRFTool.java:
CSRFTool.java
package com.javan.util; import javax.servlet.http.HttpServletRequest; import com.javan.security.CSRFTokenManager; public class CSRFTool { public static String getToken(HttpServletRequest request) { return CSRFTokenManager.getTokenForSession(request.getSession()); } }
页面渲染CSRF token的宏命令:
macros-default.vm code view
#** * CSRFToken * Generate a input field of type 'hidden' of token to avoid CSRF attack *# #macro( CSRFToken $id) #if(!$id || $id == "") #set($id="xToken") #end <input type="hidden" id="$id" name="xToken" value="$csrfTool.getToken($request)"/> #end
在前端的Form表单中添加token值:
<form id="userForm" action="/xxx.do" method="post"> #CSRFToken() ... </form>
四、验证Token
当请求提交后,在服务器端验证token值,在这里定义一个拦截器CSRFHandlerInterceptor:
CSRFHandlerInterceptor.java code view
package com.javan.security; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; /** * A Spring MVC <code>HandlerInterceptor</code> which is responsible to enforce CSRF token validity on incoming posts * requests. The interceptor should be registered with Spring MVC servlet using the following syntax: * * <pre> * <mvc:interceptors> * <bean class="com.eyallupu.blog.springmvc.controller.csrf.CSRFHandlerInterceptor"/> * </mvc:interceptors> * </pre> * * @see CSRFRequestDataValueProcessor */ public class CSRFHandlerInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof DefaultServletHttpRequestHandler) { return true; } if (request.getMethod().equalsIgnoreCase("GET")) { // GET - 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; } } } }
在bean配置文件中配置:
<!-- CSRF Interceptor handlers --> <mvc:interceptors> <bean class="com.javan.security.CSRFHandlerInterceptor" /> </mvc:interceptors>
如果项目web.xml中指定了error-page错误页面(比如403状态码对应的页面等),那么需要注意CSRFHandlerInterceptor拦截的问题。
如果验证成功,请求将沿着处理链继续传递;如果验证失败,请求将被暂停,发出一个HTTP 403的状态代码作为响应。但是问题来了,如果设置了error-page,Servlet 容器会先根据响应状态码将原始请求转发(forward)到具体的错误页面,然后发送到客户端浏览器。由于使用了拦截器,这次forward请求会再次被拦截,验证方法也会被触发,会再次验证失败。前端看不到403的错误提示页面。具体解决方案参见这篇文章[2]。
源码已放到Github上,有需要的童鞋请移步 这里。
参考资料
[1] EYAL LUPU 的解决方案: http://blog.eyallupu.com/2012/04/csrf-defense-in-spring-mvc-31.html
[2] 再谈Spring MVC中对于CSRF攻击的防御:http://blog.csdn.net/alphafox/article/details/8947117
原始地址: http://JavanLu.github.io/blog/2013/09/29/springmvc-csrf-defense/
written by JavanLu posted at http://JavanLu.github.io