Spring Security 源码之 Filter Part 1

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

推荐阅读更多精彩内容

  • 上一篇博客讲了如何使用Shiro和JWT做认证和授权(传送门:https://www.jianshu.com/p/...
    空挡阅读 149,182评论 37 224
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 什么是Spring Security验证? 提示用户输入用户名和密码进行登录。 该系统 (成功) 验证该用户名的密...
    _白小飞阅读 3,098评论 0 0
  • 权限控制采用 RBAC思想。简单地说,一个用户拥有若干角色,每个角色拥有一个默认的权限,每一个角色拥有若干个菜单,...
    Memory_2e2e阅读 1,528评论 0 1
  • 参考文章 spring security 极客学院spring security 博客园Spring securi...
    spilledyear阅读 2,395评论 0 7