Spring Security解析六:CsrfConfigurer

HttpSecurity的默认配置如下:

http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
                sharedObjects);
http.
  .csrf().and()
  .addFilter(new WebAsyncManagerIntegrationFilter())
  .exceptionHandling().and()
  .headers().and()
  .sessionManagement().and()
  .securityContext().and()
  .requestCache().and()
  .anonymous().and()
  .servletApi().and()
  .apply(new DefaultLoginPageConfigurer<>()).and()
  .logout();

从上面可知,第一个安全配置是针对CSRF(跨站点请求伪造:Cross-Site Request Forgery)的。一般来讲,为了防御CSRF攻击主要有三种策略:验证 HTTP Referer 字段;在请求地址中添加 token 并验证;在 HTTP 头中自定义属性并验证。

HttpSecurity的csrf() 方法定义如下:

public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
    ApplicationContext context = getContext();
    return getOrApply(new CsrfConfigurer<>(context));
}

可见其配置类为CsrfConfigurer,其部分定义如下:

public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
        extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {
    //Csrf保护使用Token的存储库,默认使用Session保存
    private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(
            new HttpSessionCsrfTokenRepository());
    //需要Csrf保护的请求匹配规则
    private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
    //不需要Csrf保护的请求匹配规则
    private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<>();
    //会话验证策略
    private SessionAuthenticationStrategy sessionAuthenticationStrategy;
    private final ApplicationContext context;

    public CsrfConfigurer(ApplicationContext context) {
        this.context = context;
    }

    /*
    * @param http  HttpSecurity
    */
    @Override
    public void configure(H http) {
        //用于Csrf保护的拦截器(内部的执行过程便是Csrf保护的过程)
        CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
        //默认为DefaultRequiresCsrfMatcher
        RequestMatcher requireCsrfProtectionMatcher = getRequireCsrfProtectionMatcher();
        if (requireCsrfProtectionMatcher != null) {
            filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
        }
        //访问拒绝处理器。如果缺失csrf的token时会被执行
        AccessDeniedHandler accessDeniedHandler = createAccessDeniedHandler(http);
        if (accessDeniedHandler != null) {
            filter.setAccessDeniedHandler(accessDeniedHandler);
        }
        //注销时候的配置;获取注销的配置类,并添加回调,目的是注销时清除csrf的token
        LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
        if (logoutConfigurer != null) {
            logoutConfigurer
                    .addLogoutHandler(new CsrfLogoutHandler(this.csrfTokenRepository));
        }
        //访问会话管理配置,添加基于Csrf的会话认证策略
        //如果有csrf的token则先清除再重建且保存
        SessionManagementConfigurer<H> sessionConfigurer = http
                .getConfigurer(SessionManagementConfigurer.class);
        if (sessionConfigurer != null) {
            sessionConfigurer.addSessionAuthenticationStrategy(
                    getSessionAuthenticationStrategy());
        }
        //后置处理器
        //CompositeObjectPostProcessor、AutowireBeanFactoryObjectPostProcessor
        filter = postProcess(filter);
        http.addFilter(filter);
    }

    private AccessDeniedHandler createAccessDeniedHandler(H http) {
        //取得SessionManagementConfigurer里面配置的InvalidSessionStrategy
        //默认处理器,首先获取ExceptionHandlingConfigurer中配置的AccessDeniedHandler,
        //如果没有则使用默认的AccessDeniedHandlerImpl;默认响应403
        InvalidSessionStrategy invalidSessionStrategy = getInvalidSessionStrategy(http);
        AccessDeniedHandler defaultAccessDeniedHandler = getDefaultAccessDeniedHandler(
                http);
        if (invalidSessionStrategy == null) {
            return defaultAccessDeniedHandler;
        }

        //使用invalidSessionStrategy进行处理,例如对请求重定向等
        InvalidSessionAccessDeniedHandler invalidSessionDeniedHandler = new InvalidSessionAccessDeniedHandler(
                invalidSessionStrategy);

        LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers = new LinkedHashMap<>();
        //指定MissingCsrfTokenException类型的异常可被invalidSessionDeniedHandler处理
        handlers.put(MissingCsrfTokenException.class, invalidSessionDeniedHandler);
        //该处理的逻辑是,如果处理的异常为MissingCsrfTokenException则使用invalidSessionDeniedHandler处理,
        //否则使用默认的defaultAccessDeniedHandler处理
        return new DelegatingAccessDeniedHandler(handlers, defaultAccessDeniedHandler);
    }
}

CsrfFilter进行请求拦截的处理过程

public final class CsrfFilter extends OncePerRequestFilter {
    /**
     * The default {@link RequestMatcher} that indicates if CSRF protection is required or
     * not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
     * requests.
     */
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();

    private final CsrfTokenRepository tokenRepository;
    private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
    private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);

        //得到保存的token
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        final boolean missingToken = csrfToken == null;
        if (missingToken) {
            //创建新的csrf的token
            csrfToken = this.tokenRepository.generateToken(request);
            //将csrf的token进行保存
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);

        //判断是否是需要Csrf包含的请求,如果不是则执行后续的过滤器链
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        //从Http的请求头或请求参数中得到csrf的token
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        if (actualToken == null) {
            actualToken = request.getParameter(csrfToken.getParameterName());
        }
        if (!csrfToken.getToken().equals(actualToken)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid CSRF token found for "
                        + UrlUtils.buildFullRequestUrl(request));
            }
            if (missingToken) {
                //此处调用了处理器,并传递了MissingCsrfTokenException异常
                this.accessDeniedHandler.handle(request, response,
                        new MissingCsrfTokenException(actualToken));
            } else {
                //此处调用了处理器,并传递了InvalidCsrfTokenException异常
                this.accessDeniedHandler.handle(request, response,
                        new InvalidCsrfTokenException(csrfToken, actualToken));
            }
            return;
        }

        filterChain.doFilter(request, response);
    }
}

以上便是Spring Security中进行Csrf保护的大致处理过程,其思路就是生成一个csrf的token并保存到session中,然后从每次的请求中得到携带的csrf的token并与session中保存的做比对,如果不一致则验证不通过。

这个csrf的token其实就是一个uuid字符串,同时,默认只处理除了GET, HEAD, TRACE, OPTIONS方法外的其他请求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容