在Spring MVC应用中实施CSRF防御,一般会采用
EYAL
LUPU的方案,该方案的基本思路是在生成表单时在其中插入一个随机数作为签名,在表单提交后对其中的签名进行验证,根据验证的结果区分该表单是否是经由应用签署的合法表单。如果签名不正确或不存在签名,则说明请求可能已被劫持。
EYAL LUPU方案的巧妙之处在于,通过使用HandlerInterceptorAdapter和Spring3.1中新引入的ReuqestDataValueProcessor这一对组合,使得签名和验证的过程无缝地集成到现有应用中。Controller或Model层的对象可以仍然只关心自己的业务逻辑,完全不必考虑CSRF过程的存在;唯一的限制是在View层,必须使用Spring的<form>标签来渲染表单。
对请求的验证在拦截器的preHandle方法中,当验证通过后,方法返回true,请求将沿着处理链继续传递;但如果验证失败,方法返回false,请求将被截停,并发送一个HTTP 400的状态代码作为响应。
如果没有在web.xml中使用error-page为应用自定义错误页,400状态码将直接被发送给客户端浏览器,浏览器会显示一个缺省的错误页面,到目前为之一切都很完美。
但如果使用error-page指定了错误页面,问题来了,Servlet容器会首先根据响应状态码把原始的请求转发(forward)给具体的错误页面,然后该错误页面才被发送到客户端浏览器。需要注意的是,由于我们使用了拦截器,这次forward请求会再次被拦截,preHandle方法中的验证过程也会再次被触发,如果不加处理的话,会再次验证失败,因为具体的请求仍然是最初的请求。
解决的方案如下:
1. 在Spring的dispatcher-servlet中加入mvc:default-servlet-handler。
这样做的目的是确保未被Dispatcher实际处理的请求,例如向错误页面的转发或对静态资源的访问等,会被传回给Servlet容器。
2. 修改preHandle方法,在方法的开始检测方法的第三个参数的类型。
该参数代表处理链中即将处理请求的下一个对象,如果该参数是DefaultServletHttpRequestHandler类的实例,则说明请求即将交由Servlet容器处理,对于此类请求直接放行即可。
3. 最后,确保所有errro-page中声明的URL不会被任何Controller处理,也不会被ResourceHttpRequestHandler处理。
被Controller处理过就没机会沿处理链将请求交给Servlet容器了。另外,应用一般会在dispatcher中用mvc:resources声明静态资源,如果把错误页面也包含进来,请求会在被交给DefaultServletHttpRequestHanlder前先被ResourceHttpRequestHandler匹配到,后者为了优化对静态资源的处理,默认是只支持GET和HEAD方法的,而此时的请求是从表单POST来的,因此会引发一个Request
method 'POST' not supported错误,而浏览器端只能得到一个405响应,无法显示预期的错误页页面。
调整后的preHandle方法类似下面这样:
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof DefaultServletHttpRequestHandler) {
return true;
}
if (!request.getMethod().equalsIgnoreCase(
WebContentGenerator.METHOD_POST)) {
// 忽略非POST请求
return true;
} else {
// 验证CSRF签名
//if (passed)
// return true;
//else {
// response.sendError(HttpServletResponse.SC_BAD_REQUEST,
// "Bad or missing CSRF value");
// return false;
//}
}
}
作者:alphafox 发表于2013-5-19 16:55:04
原文链接