在SpringMVC中使用拦截器(interceptor)拦截CSRF攻击

(1)登录页面:

1
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
<%@page import="java.security.SecureRandom"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>    
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<base href="<%=basePath%>">
<title>SpringMVC Cookie Demo</title>
<%
    SecureRandom random = new SecureRandom();
    random.setSeed(8738);
    double _csrf = random.nextDouble();
    session.setAttribute("_csrf", _csrf);
%>
</head>
<body>
    <div align="center">
        <h2>SpringMVC Cookie Demo</h2>
        <form action="check.html" method="post">
            <table>
                <tr>
                    <td>用户名:</td>
                    <td><input type="text" name="username" /></td>
                </tr>
                <tr>
                    <td>密码:</td>
                    <td><input type="password" name="password" /></td>
                </tr>
                <tr>
                    <td><input name="remember-me" type="checkbox">30天内自动登录</input></td>
                </tr>
                <tr>
                    <td colspan="2" align="center"><input type="submit" value="登录" />
                        <input type="reset" value="重置" /></td>
                </tr>        
            </table>
            <input type="hidden" name="_csrf" value="<%=_csrf %>" />
        </form>
         
    </div>
</body>
</html>

从上面的代码可知,为了防止CSRF攻击,因此在form表单里添加了一个隐藏字段“_csrf”,其值是生成的一个随机小数

(2)在SpringMVC的配置文件中添加拦截器:

1
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
<?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:cache="http://www.springframework.org/schema/cache"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/cache  
       http://www.springframework.org/schema/cache/spring-cache-4.0.xsd  
       http://www.springframework.org/schema/mvc 
       http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"
       default-lazy-init="true">
     
    <mvc:annotation-driven />
    <!-- 组件扫描 -->
    <context:component-scan base-package="cn.zifangsky.controller" />
    <context:component-scan base-package="cn.zifangsky.manager.impl"/>      
    <!-- 配置直接转发的页面 -->
    <mvc:view-controller path="/login.html" view-name="login" />
    <mvc:view-controller path="/user/callback.html" view-name="user/callback" />
     
    <!-- 拦截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- 对登录操作进行拦截 -->
            <mvc:mapping path="/check.html"/>
            <bean class="cn.zifangsky.interceptor.LoginInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 对/user/**的请求进行拦截 -->
            <mvc:mapping path="/user/**"/>
            <bean class="cn.zifangsky.interceptor.UserInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>
     
    <!-- 视图解析 -->
    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

从上面的代码知道,在这个文件中添加了一个 mvc:interceptors 标签,表示一系列的拦截器集合,然后下面定义了对登录时form表单提交地址“/check.html”进行拦截。下面一行的bean属性就是定义了自定义拦截器的类所在的路径

注:后面那个拦截器这里不用管,我在写后面的文章时才会用到

(3)自定义拦截器LoginInterceptor:

1
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
package cn.zifangsky.interceptor;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
 
public class LoginInterceptor extends HandlerInterceptorAdapter {
    /**
     * 用于在登录前验证 _csrf 参数
     * */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        HttpSession session = request.getSession();
        String _csrfByForm = request.getParameter("_csrf");  //表单中的值
        String _csrfBySession = String.valueOf(session.getAttribute("_csrf"));  //session中的值
        session.removeAttribute("_csrf");  //使用之后从session中删掉
 
        //验证是否存在CSRF攻击
        if(StringUtils.isNotBlank(_csrfByForm) && StringUtils.isNotBlank(_csrfBySession) && _csrfByForm.equals(_csrfBySession)){
            return true;
        }else{
            response.setContentType("text/html;charset=utf-8");
            response.setStatus(403);
 
            //页面友好提示信息
            OutputStream oStream = response.getOutputStream();
            oStream.write("请不要重复提交请求,返回原始页面刷新后再次尝试!!!".getBytes("UTF-8"));
             
            return false;
        }      
    }
 
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }
 
}

这个自定义拦截器的逻辑很简单,就是把form表单隐藏域“_csrf”中的值和session中的“_csrf”值进行比较。如果二者相同,则说明该请求是从前台form表单中传进来的,而不是其他网站的伪造请求(PS:因为这种方式没法向session中定义“_csrf”参数);同时也防止form表单的重复提交(PS:因为第一次验证过后session中的“_csrf”就已经被移除了,除非前台刷新页面才会重新生成),避免了**撞库等安全隐患。当然,为了进一步降低安全隐患,这里的form表单还应该添加复杂的动态验证码。我这里是由于为了让示例更简洁,因此就把这一步给省略了

(4)验证:

第一次提交表单,发现可以正常到达后台进行验证

第二次点击浏览器的“返回键”,返回到表单页面之后重复提交,可以发现直接被拦截了。效果如下:

在SpringMVC中使用拦截器(interceptor)拦截CSRF攻击



本文转自 pangfc 51CTO博客,原文链接:http://blog.51cto.com/983836259/1877586,如需转载请自行联系原作者