Struts框架

JSP使用<s:token/>标签,在struts配置文件中增加token拦截器
页面代码:

<s:form action="reg" theme="simple">
    username:<s:textfield name="username"></s:textfield><br />
    password:<s:password name="password"></s:password><br />
    <s:token></s:token>
    <s:submit value="注册"></s:submit>
</s:form>

在struts.xml中配置:

<package name="test" namespace="/test" extends="struts-default">  
    <interceptors> //配置拦截器  
        <interceptor-stack name="tokenStack"> //命名拦截器栈,名字随便  
            <interceptor-ref name="token"/> //此拦截器为token拦截器,struts已经实现  
            <interceptor-ref name="defaultStack" /> //默认拦截器,注意顺序,默认拦截器放在最下面  
        </interceptor-stack>  
    </interceptors>  
    <default-interceptor-ref name="tokenStack" /> //让该包中所有action都是用我们配置的拦截器栈,名字和上面的对应   
</package>

流程说明:<br />

  1. 客户端申请token
  2. 服务器端生成token,并存放在session中,同时将token发送到客户端
  3. 客户端存储token,在请求提交时,同时发送token信息
  4. 服务器端统一拦截同一个用户的所有请求,验证当前请求是否需要被验证(不是所有请求都验证重复提交)
  5. 验证session中token是否和用户请求中的token一致,如果一致则放行
  6. session清除会话中的token,为下一次的token生成作准备
  7. 并发重复请求到来,验证token和请求token不一致,请求被拒绝

SpringMVC

具体思路:<br />

  1. 跳转页面前生成随机token,并存放在session中
  2. form中将token放在隐藏域中,保存时将token放头部一起提交
  3. 获取头部token, 与session中的token比较,一致则通过
  4. 生成新的token,并传给前端

一: 配置拦截器

<mvc:interceptors>
    <!-- csrf攻击防御 -->
    <mvc:interceptor>
        <!-- 需拦截的地址 -->
        <mvc:mapping path="/**" />
        <!-- 需排除拦截的地址 -->
        <mvc:exclude-mapping path="/resources/**" />
        <bean class="com.cnpc.framework.interceptor.CSRFInterceptor" />
    </mvc:interceptor>
</mvc:interceptors>

二: 拦截器实现 CSRFInterceptor.java

import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.alibaba.fastjson.JSONObject;
import com.cnpc.framework.annotation.RefreshCSRFToken;
import com.cnpc.framework.annotation.VerifyCSRFToken;
import com.cnpc.framework.base.pojo.ResultCode;
import com.cnpc.framework.constant.CodeConstant;
import com.cnpc.framework.utils.CSRFTokenUtil;
import com.cnpc.framework.utils.StrUtil;

/**
 * CSRFInterceptor 防止跨站请求伪造拦截器
 */
public class CSRFInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println("---------->" + request.getRequestURI());
        System.out.println(request.getHeader("X-Requested-With"));
        // 提交表单token 校验
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        VerifyCSRFToken verifyCSRFToken = method.getAnnotation(VerifyCSRFToken.class);
        // 如果配置了校验csrf token校验,则校验
        if (verifyCSRFToken != null) {
            // 是否为Ajax标志
            String xrq = request.getHeader("X-Requested-With");
            // 非法的跨站请求校验
            if (verifyCSRFToken.verify() && !verifyCSRFToken(request)) {
                if (StrUtil.isEmpty(xrq)) {
                    // form表单提交,url get方式,刷新csrftoken并跳转提示页面
                    String csrftoken = CSRFTokenUtil.generate(request);
                    request.getSession().setAttribute("CSRFToken", csrftoken);
                    response.setContentType("application/json;charset=UTF-8");
                    PrintWriter out = response.getWriter();
                    out.print("非法请求");
                    response.flushBuffer();
                    return false;
                } else {
                    // 刷新CSRFToken,返回错误码,用于ajax处理,可自定义
                    String csrftoken = CSRFTokenUtil.generate(request);
                    request.getSession().setAttribute("CSRFToken", csrftoken);
                    ResultCode rc = CodeConstant.CSRF_ERROR;
                    response.setContentType("application/json;charset=UTF-8");
                    PrintWriter out = response.getWriter();
                    out.print(JSONObject.toJSONString(rc));
                    response.flushBuffer();
                    return false;
                }
            }

        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {

        // 第一次生成token
        if (modelAndView != null) {
            if (request.getSession(false) == null || StrUtil.isEmpty((String) request.getSession(false).getAttribute("CSRFToken"))) {
                request.getSession().setAttribute("CSRFToken", CSRFTokenUtil.generate(request));
                return;
            }
        }
        // 刷新token
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        RefreshCSRFToken refreshAnnotation = method.getAnnotation(RefreshCSRFToken.class);

        // 跳转到一个新页面 刷新token
        String xrq = request.getHeader("X-Requested-With");
        if (refreshAnnotation != null && refreshAnnotation.refresh() && StrUtil.isEmpty(xrq)) {
            request.getSession().setAttribute("CSRFToken", CSRFTokenUtil.generate(request));
            return;
        }

        // 校验成功 刷新token 可以防止重复提交
        VerifyCSRFToken verifyAnnotation = method.getAnnotation(VerifyCSRFToken.class);
        if (verifyAnnotation != null) {
            if (verifyAnnotation.verify()) {
                if (StrUtil.isEmpty(xrq)) {
                    request.getSession().setAttribute("CSRFToken", CSRFTokenUtil.generate(request));
                } else {
                    Map<String, String> map = new HashMap<String, String>();
                    map.put("CSRFToken", CSRFTokenUtil.generate(request));
                    response.setContentType("application/json;charset=UTF-8");
                    OutputStream out = response.getOutputStream();
                    out.write((",'csrf':" + JSONObject.toJSONString(map) + "}").getBytes("UTF-8"));
                }
            }
        }
    }

    /**
     * 处理跨站请求伪造 针对需要登录后才能处理的请求,验证CSRFToken校验
     * 
     * @param request
     */
    protected boolean verifyCSRFToken(HttpServletRequest request) {

        // 请求中的CSRFToken
        String requstCSRFToken = request.getHeader("CSRFToken");
        if (StrUtil.isEmpty(requstCSRFToken)) {
            return false;
        }
        String sessionCSRFToken = (String) request.getSession().getAttribute("CSRFToken");
        if (StrUtil.isEmpty(sessionCSRFToken)) {
            return false;
        }
        return requstCSRFToken.equals(sessionCSRFToken);
    }
}

ESAPI

一: 产生一个新的csrfToken,在用户第一次登陆的时候保存在session中

private String csrfToken = resetCSRFToken();

private String resetCSRFToken() {
        String csrfToken = ESAPI.randomizer().getRandomString(8,DefaultEncoder.CHAR_ALPHANUMERICS);
        return csrfToken;
}

二:对于所有需要保护的表单,增加一个隐藏的csrfToken字段。下面的addCSRFToken方法可以被所有需要保护的URL链接调用,如果是表单的话,可以增加一个字段,名字叫"ctoken",值为DefaultHTTPUtilities.getCSRFToken()。<br />
URL上:

<a href='<%=ESAPI.httpUtilities().addCSRFToken("/zhutougg/main?function=listUser")%>' >查询用户</a>

表单中:

<input type="hidden" name="ctoken" value="<%=ESAPI.DefaultHTTPUtilities.getCSRFToken() %>">

三: 在服务器端那些需要保护的方法执行之前,首先检查一下提交过来的token与session中的token是否一致,如果不一致,则被认为是一个伪造的请求(CSRF攻击)

@RequestMapping(value="/admin/login",method = RequestMethod.POST)
public String listUser(HttpServletRequest request, HttpServletResponse response,Model model){
    ESAPI.httpUtilities().verifyCSRFToken(request);
    ......
}

四: 在用户退出或者session过期的时候 移除csrfToken

ESAPI.authenticator().getCurrentUser().logout();

源链接

Hacking more

...