Spring Security 源码之 Filter Part
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:保存
SecurityContext
到HttpSession
中。 - 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);
}
}
doHeadersBefore
和doHeadersAfter
方法最终都会调用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
代理类型,包含一个requestMatcher
和headerWriter
,如果请求可以被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被访问的是否才会保存。