美文网首页程序员Java
Spring Security 源码之 Filter Part

Spring Security 源码之 Filter Part

作者: AlienPaul | 来源:发表于2020-11-20 11:35 被阅读0次

    ChannelProcessingFilter

    判断哪些请求适用于HTTPS或HTTP协议,或者不受约束,并自动跳转到配置约定的通道。

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        // FilterInvocation是一个容器类。包装了request,response和filterChain
        FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
        // 返回这个request是否要求是SECURE_CHANNEL还是INSECURE_CHANNEL,通过RequestMatcher匹配request
        // securityMetadataSource,包含了requestMatcher和ConfigAttribute的映射关系
        // 这里获取匹配这个request的requestMatcher所对应的ConfigAttribute
        // ConfigAttribute包含ANY_CHANNEL,REQUIRES_SECURE_CHANNEL和REQUIRES_INSECURE_CHANNEL
        // 分别表示任意通道(不切换),要求安全通道和要求非安全通道
        Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(filterInvocation);
        // 如果这个request有相关配置
        if (attributes != null) {
            this.logger.debug(LogMessage.format("Request: %s; ConfigAttributes: %s", filterInvocation, attributes));
            // 判断filterInvocation是否满足配置
            // ChannelDecisionManager包含了ChannelProcessor
            // ChannelProcessor有InsecureChannelProcessor和SecureChannelProcessor两个子类
            // channelProcessor会根据request的配置,跳转到HTTPS或者是HTTP通道
            this.channelDecisionManager.decide(filterInvocation, attributes);
            // 如果response已经提交,直接返回
            if (filterInvocation.getResponse().isCommitted()) {
                return;
            }
        }
        chain.doFilter(request, response);
    }
    

    CurrentSessionFilter

    用于判断session是否超时(expired),对于超时的session,将这个用户登出。主要用于限制同一个用户多次登陆的场景。

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 获取session
        HttpSession session = request.getSession(false);
        // 如果获取到了session
        if (session != null) {
            // 根据session id获取session详细信息
            SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
            if (info != null) {
                // 如果session已过期
                if (info.isExpired()) {
                    // Expired - abort processing
                    this.logger.debug(LogMessage
                            .of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
                    // 登出用户
                    doLogout(request, response);
                    // 告诉sessionInformationExpiredStrategy处理session超时事件
                    // 重定向到session过期URL
                    this.sessionInformationExpiredStrategy
                            .onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
                    return;
                }
                // Non-expired - update last request date/time
                // 如果没过期,刷新session的最后访问时间
                this.sessionRegistry.refreshLastRequest(info.getSessionId());
            }
        }
        chain.doFilter(request, response);
    }
    

    SecurityContextPersistenceFilter

    请求到来的时候负责从repository(默认存储在HttpSession)读取securityContext(包含认证信息),存储到SecurityContextHolder中,请求完成的时候再清理掉SecurityContextHolder保存的securityContext。

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
    
        // 检查请求是否已经被这个filter处理过
        // 确保只处理一次
        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);
            return;
        }
    
        final boolean debug = logger.isDebugEnabled();
    
        // 设置属性,标记请求已经被此filter处理过
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    
        // 如果需要强制创建session
        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();
    
            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }
    
        // 包装request和response
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        // 从repository中读取securityContext
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    
        try {
            // 设置SecurityContext到SecurityContextHolder中
            SecurityContextHolder.setContext(contextBeforeChainExecution);
    
            // 继续下一个filter
            chain.doFilter(holder.getRequest(), holder.getResponse());
    
        }
        finally {
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            // 请求处理完毕后,清空SecurityContextHolder
            SecurityContextHolder.clearContext();
            // 保存SecurityContext
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            // 清除经过此filter处理的标记
            request.removeAttribute(FILTER_APPLIED);
    
            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }
    

    SecurityContextRepository

    该接口负责持久化保存SecurityContext
    它有如下子类:

    • HttpSessionSecurityContextRepository:保存SecurityContextHttpSession中。
    • NullSecurityContextRepository:空实现,什么也不做。

    HeadWriterFilter

    用于向HTTP响应添加一些header。

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 是否需要在filterChain.doFilter之前添加header
        if (this.shouldWriteHeadersEagerly) {
            doHeadersBefore(request, response, filterChain);
        }
        else {
            doHeadersAfter(request, response, filterChain);
        }
    }
    

    doHeadersBeforedoHeadersAfter方法最终都会调用writeHeaders方法。
    writeHeaders方法遍历每一个HeaderWriter改写HTTP header。

    void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        for (HeaderWriter writer : this.headerWriters) {
            writer.writeHeaders(request, response);
        }
    }
    

    HeaderWriter

    它是所有改写HTTP header的接口,只有一个方法writerHeaders
    根据添加header的不同,下面分别介绍它的实现类。这些实现类基本都是为response添加安全增强的HTTP header。

    CacheControlHeadersWriter

    添加"Cache-Control:no-cache, no-store, max-age=0, must-revalidate","Pragma:no-cache"和"Expires:0"。即禁用掉所有的缓存,必须向原服务器发送验证请求。

    ClearSiteDataHeaderWriter

    添加Clear-Site-Data header。用于清除浏览器数据。可使用如下值:

    • cache:清除缓存
    • cookie:清除cookie
    • storage:清除所用的DOM存储,例如LocalStorage等
    • executionContexts:重载所有浏览上下文,类似于刷新
    • 通配符(*):清除以上所有内容

    CompositeHeaderWriter

    包含多个HeaderWriter,是一个复合类。

    ContentSecurityPolicyHeaderWriter

    添加CSP header。用于告诉浏览器需要加载资源的范围,哪些资源可以加载,哪些资源禁止加载。

    DelegatingRequestMatcherHeaderWriter

    代理类型,包含一个requestMatcherheaderWriter,如果请求可以被requestMatcher匹配,使用headerWriter改写header

    FeaturePolicyHeaderWriter

    添加Feature-Policy header。用于禁用或者启用浏览器特性。多用于限制周边设备比如加速度感应器,电池信息和摄像头等。

    HpkpHeaderWriter

    添加Public-Key-Pins header。响应头将特定的加密公钥与特定的 Web服务器相关联,以降低伪造证书对 MITM 攻击的风险。该header目前已废弃不建议使用

    HstsHeaderWriter

    添加Strict-Transport-Security header。强制浏览器使用HTTPS通道访问服务器。包含如下配置项:

    • max-age:多长时间内浏览器只会使用HTTPS通道
    • includeSubDomains:是否包含子域名
    • preload:使用浏览器的预载列表

    ReferrerPolicyHeaderWriter

    添加Referrer-Policy header。Referer是一个请求头,用于告诉服务器用户是从哪个页面跳转来的。这个字段包含的内容可能会泄漏用户敏感信息。Referrer-Policy用于限制浏览器发送referer的内容,有如下几个配置项:

    • No Referrer:任何情况下都不发送Referrer信息。
    • No Referrer When Downgrade:仅当协议降级(如HTTPS页面引入HTTP资源)时不发送Referrer信息。是大部分浏览器默认策略。
    • Origin Only:发送只包含host部分的referrer。
    • Origin When Cross-origin:仅在发生跨域访问时发送只包含host的Referer,同域下还是完整的。与Origin Only的区别是多判断了是否Cross-origin。协议、域名和端口都一致,浏览器才认为是同域。
    • Unsafe URL:全部都发送Referrer信息。最宽松最不安全的策略。

    这一段描述内容来自:https://www.cnblogs.com/amyzhu/p/9716493.html。如有侵权可联系删除。

    StaticHeadersWriter

    包含header列表,将header列表中所有header写入response。

    XContentTypeOptionsHeaderWriter

    添加"X-Content-Type-Options:nosniff"。作用为资源的MIME类型不可被更改,阻止浏览器自动推断MIME类型。防止基于 MIME 类型混淆的攻击。

    XFrameOptionsHeaderWriter

    添加X-Frame-Options header。用于限制页面是否可以在iframe中展示,防止clickjack攻击。有如下配置项:

    • DENY:不允许在iframe中展示
    • SAMEORIGIN:只允许在同源页面的iframe中展示
    • ALLOW-FROM uri:可以在指定url页面的iframe中展示

    XssProtectionHeaderWriter

    添加X-XSS-Protection header,用于防范XSS攻击。如果加入了mode=block。浏览器会在检测到XSS攻击后,停止渲染页面。

    CsrfFilter

    此Filter负责防御CSRF攻击。负责生成和校验csrf token。

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
    
        // 使用tokenRepository获取token
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        final boolean missingToken = csrfToken == null;
        // 如果缺失csrf token,重新生成一个
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        // 将csrf token放入request
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
    
        // 判断请求是否要求csrf保护,即除了GET,HEAD,TRACE,OPTIONS之外的请求需要CSRF保护
        // 如果不需保护,运行后面的filter
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }
    
        // 获取request header中的实际csrf token
        String actualToken = request.getHeader(csrfToken.getHeaderName());
        if (actualToken == null) {
            // 如果header中没有,从请求参数中获取csrf token
            actualToken = request.getParameter(csrfToken.getParameterName());
        }
        // 如果实际的token和要求的token不一致,发生了csrf攻击,拒绝访问
        if (!csrfToken.getToken().equals(actualToken)) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invalid CSRF token found for "
                        + UrlUtils.buildFullRequestUrl(request));
            }
            if (missingToken) {
                this.accessDeniedHandler.handle(request, response,
                        new MissingCsrfTokenException(actualToken));
            }
            else {
                this.accessDeniedHandler.handle(request, response,
                        new InvalidCsrfTokenException(csrfToken, actualToken));
            }
            return;
        }
    
        // csrf校验通过,允许访问
        filterChain.doFilter(request, response);
    }
    

    CsrfTokenRepository

    CSRF token的生成,保存和获取不由CsrfFilter直接负责,而是交给了CsrfTokenRepository。该接口有如下3个方法:

    public interface CsrfTokenRepository {
    
        // 创建一个CSRF token,通常为一个UUID
        CsrfToken generateToken(HttpServletRequest request);
    
        // 保存CSRF token
        // 如果token为null,相当于删除这个token
        void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
    
        // 从request加载token
        CsrfToken loadToken(HttpServletRequest request);
    
    }
    

    该接口有如下3个实现类:

    • CookieCsrfTokenRepository:保存token到cookie中,名字为XSRF-TOKEN
    • HttpSessionCsrfTokenRepository:保存token到HttpSession。
    • LazyCsrfTokenRepository:一个代理的CsrfTokenRepository,直到生成的token被访问的是否才会保存。

    相关文章

      网友评论

        本文标题:Spring Security 源码之 Filter Part

        本文链接:https://www.haomeiwen.com/subject/uakdiktx.html