关于CSRF是什么东西,请参见我的博文:《浅析CSRF》。本文将给出一种在 SpringMVC3+Velocity 的框架下防御CSRF攻击的解决方案。主要思想参考 EYAL LUPU[1] 的解决方案,但使用Velocity宏来进行Token值的传递,而不是使用spring form标签。
一、方案概要
在服务器端生成私有的会话级token,使用Velocity的宏命令在客户端的Form表单中插入这个token;在表单提交后对,服务器端对token值进行校验,根据结果来区分该次请求是否合法:正确就放行,不正确就就阻断请求。
在这里,我们约定:凡是更新资源的操作,都通过POST向服务器端发送。这也是符合HTTP规范的约定。
二、Token生成
使用一个 CSRFTokenManager 类来进行token生成的工作,代码如下:
CSRFTokenManager.javacode view1
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
54package 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中,然后返回。
三、Token依附到Form
由于我的前端使用 Velocity 进行渲染,并且不使用 Spring form标签,所以为了将token依附到Form表单中,我使用 Velocity 的宏指令进行渲染工作。
SpringMVC 中的配置,主要是配置 Velocity Tools:
mvc-context.xmlcode view1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
utf-8
utf-8
class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
.vm
Toolbox.xml中定义生成 CSRF token 的 tool:
toolbox.xmlcode view1
2
3
4
5
6
7
csrfTool
application
com.javan.util.CSRFTool
工具类CSRFTool.java:
CSRFTool.xmlcode view1
2
3
4
5
6
7
8
9
10
11package 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.vmcode view1
2
3
4
5
6
7
8
9#** CSRFToken
* Generate a input field of type 'hidden' of token to avoid CSRF attack
*#
#macro( CSRFToken $id)
#if(!$id || $id == "")
#set($id="xToken")
#end
#end
在前端的Form表单中添加token值:
1
2
3
4
#CSRFToken()
...
四、验证Token
当请求提交后,在服务器端验证token值,在这里定义一个拦截器CSRFHandlerInterceptor:
CSRFHandlerInterceptor.javacode view1
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
43package 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 HandlerInterceptor
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:
*
*
*
*
*
* @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配置文件中配置:
1
2
3
4
五、注意事项
如果项目web.xml中指定了error-page错误页面(比如403状态码对应的页面等),那么需要注意CSRFHandlerInterceptor拦截的问题。
如果验证成功,请求将沿着处理链继续传递;如果验证失败,请求将被暂停,发出一个HTTP 403的状态代码作为响应。但是问题来了,如果设置了error-page,Servlet 容器会先根据响应状态码将原始请求转发(forward)到具体的错误页面,然后发送到客户端浏览器。由于使用了拦截器,这次forward请求会再次被拦截,验证方法也会被触发,会再次验证失败。前端看不到403的错误提示页面。具体解决方案参见这篇文章[2]。
源码已放到Github上,有需要的童鞋请移步