前后端分离解决CSRF问题

个人记录,如有错误,敬请指出

项目环境:spring boot+shiro+jwt

简单方式直接通过nginx对Referer进行校验拦截即可,本文不做讲解

1、创建CsrfFilter

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class CsrfFilter extends OncePerRequestFilter {

    private static final String CSRF_TOKEN = "X-Csrf-Token";
    /**
     * 需要排除的接口
     */
    private static final List ignoreCsrfList = new ArrayList();

    private static final List accessRequestList = Arrays.asList(new String[]{"GET", "HEAD", "TRACE", "OPTIONS"});

    private Collection domains;

    static {
        ignoreCsrfList.add("/sys/login");
        ignoreCsrfList.add("/sys/logout");
    }

    public CsrfFilter(Collection domains) {
        this.domains = domains;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String csrfToken = null;
        //获取cookie中Csrf-Token的值
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(CSRF_TOKEN)) {
                    csrfToken = cookie.getValue();
                }
            }
        }

        boolean missingToken = csrfToken == null;

        // GET 等方式不用提供Token,自动放行,不能用于修改数据。修改数据必须使用 POST、PUT、DELETE、PATCH 方式并且Referer要合法。
        if (accessRequestList.contains(request.getMethod())) {
            filterChain.doFilter(request, response);
            return;
        }

        String uri = request.getRequestURI();
        if (uri == null || verifyIgnoreApi(uri)) {
            filterChain.doFilter(request, response);
            return;
        }

        if (!domains.isEmpty() && !verifyDomains(request)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF Protection: Referer Illegal");
            return;
        }

        if (!verifyToken(request, csrfToken)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, missingToken ? "CSRF Token Missing" : "CSRF Token Invalid");
            return;
        }

        filterChain.doFilter(request, response);
    }

    private boolean verifyDomains(HttpServletRequest request) {
        // 从 HTTP 头中取得 Referer 值
        String referer = request.getHeader("Referer");
        // 判断 Referer 是否以 合法的域名 开头。
        if (referer != null) {
            if (referer.indexOf("://") > 0) {
                referer = referer.substring(referer.indexOf("://") + 3);
            }
            if (referer.indexOf("/") > 0) {
                referer = referer.substring(0, referer.indexOf("/"));
            }
            if (referer.indexOf(":") > 0) {
                referer = referer.substring(0, referer.indexOf(":"));
            }
            for (String domain : domains) {
                if (referer.endsWith(domain)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean verifyToken(HttpServletRequest request, String token) {
        if (token == null) {
            return false;
        }
        //与前端约定的加密方式
        int csrfToken = MD5Util.MD5Encode(token, "utf-8").hashCode();
        String hToken = request.getHeader(CSRF_TOKEN);
        String rToken = request.getParameter(CSRF_TOKEN);
        if (hToken != null && Integer.parseInt(hToken) == csrfToken) {
            return true;
        }
        if (rToken != null && Integer.parseInt(rToken) == csrfToken) {
            return true;
        }
        return false;
    }

    private boolean verifyIgnoreApi(String uri) {
        for (String ignoreApi : ignoreCsrfList) {
            if (uri.endsWith(ignoreApi)) {
                return true;
            }
        }
        return false;
    }

2、注册Filter,在ShiroConfig类中添加如下配置

    //配置文件中配置允许的Referer
    @Value("${access.referer}")
    private List accessReferer;

    @Bean
    public FilterRegistrationBean csrfFilter() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        // 这里使用的是配置文件信息,获取配置文件中的csrf.domains相关值信息
        if (accessReferer != null && accessReferer.size() > 0) {
            filterRegistration.setFilter(new CsrfFilter(accessReferer));
        } else {
            filterRegistration.setFilter(new CsrfFilter(Collections.emptyList()));
        }
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*");
        return filterRegistration;
    }

3、在登录接口生成X-Access-Token,并通过设置Cookie的方式返回前端

//配置文件中配置前后端共同的域名(如果没有共同域名,cookie无法正常传递,暂时没考虑怎么实现),如前端域名为a.test.com,后端域名为b.test.com,则配置为.test.com,前面的.不可省略
@Value("${csp.access.domain}")
private String cspDomain;

Cookie cookie = new Cookie(CSRF_TOKEN, csrfToken);
cookie.setPath("/");
if (StringUtils.isNotBlank(cspDomain)) {
    cookie.setDomain(cspDomain);
}
//cookie.setHttpOnly(true);
response.addCookie(cookie);

4、通过第三步设置后,请求登录,设置Cookie的时候会提示错误信息如下:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

There was an unexpected error (type=Internal Server Error, status=500).
An invalid domain [.test.com] was specified for this cookie

原因是spinrboot内嵌的tomcat默认不支持这种写法,但是springboot的官方文档是支持的,添加CookieConfig配置类

import org.apache.tomcat.util.http.LegacyCookieProcessor;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * cookie配置类
 *
 */
@Configuration
public class CookieConfig {

    /**
     * 解决问题:
     * There was an unexpected error (type=Internal Server Error, status=500).
     * An invalid domain [.test.com] was specified for this cookie
     *
     * @return
     */
    @Bean
    public WebServerFactoryCustomizer cookieProcessorCustomizer() {
        return (factory) -> factory.addContextCustomizers(
                (context) -> context.setCookieProcessor(new LegacyCookieProcessor()));
    }

}

5、Cookie能够正常传输,前端读取到Cookie的X-Csrf-Token后,通过约定的加密方式,在后续请求时在请求头或请求体中携带加密后的值,后端通过对比Cookie中的值是否与请求携带上来的值是否一直来判断是否是跨域伪造请求。

你可能感兴趣的:(CSRF,csrf,java,前端,前后端分离)