SecurityFilterChain
讲过滤器[Security Filter] 之前,首先要提到SecurityFilterChain 。
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}
SecurityFilterChain 包含了两个方法。
- matches(HttpServletRequest request):主要是提供给
FilterChainProxy 进行请求的路由。 - List getFilters(): 提供
SecurityFilterChain 中的Security Filter 。
FilterChainProxy
FilterChainProxy 继承于GenericFilterBean ,是spring容器中的一个过滤器。
它主要做了两个工作:
- 内置了一个防火强
HttpFirewall 。对ServletRequest 中的HttpMethod 、ContextPath 、Hostname 、uri 等做了限制。具体内容可以查看StrictHttpFirewall::getFirewalledRequest 。 - 执行内部的
SecurityFilterChain ,直到SecurityFilterChain 中的Security Filter 执行完毕后,继续执行Servlet Container 中的剩余的Filter 。详见FilterChainProxy.VirtualFilterChain::doFilter :
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
}
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
return;
}
this.currentPosition++;
Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(),
this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
内置SecurityFilter
ChannelProcessingFilter
ChannelProcessingFilter 主要负责检测当前请求的安全通道secure channel 是否符合配置要求。
主要有三种配置:
REQUIRES_INSECURE_CHANNEL : 当前请求没有使用了安全通道,例如https请求。REQUIRES_SECURE_CHANNEL : 当前请求使用了安全通道,例如http请求。ANY_CHANNEL : 没有限制。
配置用法举例:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin-api").access("REQUIRES_SECURE_CHANNEL")
.antMatchers("/actuator").access("ANY_CHANNEL");
}
WebAsyncManagerIntegrationFilter
WebAsyncManagerIntegrationFilter 主要是用于集成WebAsyncManager ,支持在异步线程中管理SecurityContext 。
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter 主要实现了对SecurityContext 的在Session 中的持久化和存取功能。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
}
}
默认提供了3种对SecurityContext 线程持有策略,可以通过设置系统参数spring.security.strategy 来实现切换:
- MODE_THREADLOCAL: 只能获取当前线程的
SecurityContext 。 - MODE_INHERITABLETHREADLOCAL: 能够获取父线程和当前线程的
SecurityContext 。 - MODE_GLOBAL: 能够获全局应用的
SecurityContext 。
HeaderWriterFilter
HeaderWriterFilter 通过一系列HeaderWriter ,往HttpServletResponse 的Http Header中写入值。可以通过设置shouldWriteHeadersEagerly ,来控制前置写入还是后置写入。
CorsFilter
CorsFilter 通过配置CorsConfiguration ,设置CORS Http Header。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;
}
filterChain.doFilter(request, response);
}
CsrfFilter
CsrfFilter 用于防止CSRF攻击。
CSRF 英文全称是 Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事
Spring Security 防止CSRF攻击的方法:
- 在浏览器向服务器发起请求时,服务器生成一个 CSRF Token。CSRF Token 其实就是服务器生成的字符串,然后将该字符串植入到返回的页面中。
- 在浏览器端如果要发起转账的请求,那么需要带上页面中的 CSRF Token,然后服务器会验证该 Token 是否合法。如果是从第三方站点发出的请求,那么将无法获取到 CSRF Token 的值,所以即使发出了请求,服务器也会因为 CSRF Token 不正确而拒绝请求。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
this.logger.debug(
LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}
配置用法举例:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin-api").access("REQUIRES_SECURE_CHANNEL")
.antMatchers("/actuator").access("ANY_CHANNEL");
}
LogoutFilter
CsrfFilter 处理登出请求。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Logging out [%s]", auth));
}
this.handler.logout(request, response, auth);
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
配置用法举例:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests ->
authorizeRequests.antMatchers("/**").hasRole("USER"))
.formLogin(withDefaults())
.logout(logout ->
logout.deleteCookies("remove")
.invalidateHttpSession(false)
.logoutUrl("/custom-logout")
.logoutSuccessUrl("/logout-success")
);
}
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter 通过用户名密码进行认证。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
ConcurrentSessionFilter
ConcurrentSessionFilter 主要负责对SessionInformation 的维护工作。
- 正常情况下,对
Request 的所对应的SessionInformation 进行续期。 - 如果检测到
SessionInformation 已过期,则根据过期策略进行过期处理。
ConcurrentSessionFilter 通常配合着SessionManagementFilter 一起使用,可以实现同一用户在不同客户端的并发登录限制。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
if (session != null) {
SessionInformation info = sessionRegistry.getSessionInformation(session
.getId());
if (info != null) {
if (info.isExpired()) {
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " has expired.");
}
doLogout(request, response);
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
else {
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}
chain.doFilter(request, response);
}
BearerTokenAuthenticationFilter
BearerTokenAuthenticationFilter 通过Bearer Token 进行认证。
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final boolean debug = this.logger.isDebugEnabled();
String token;
try {
token = this.bearerTokenResolver.resolve(request);
} catch ( OAuth2AuthenticationException invalid ) {
this.authenticationEntryPoint.commence(request, response, invalid);
return;
}
if (token == null) {
filterChain.doFilter(request, response);
return;
}
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
try {
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
filterChain.doFilter(request, response);
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed!", failed);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
}
}
BasicAuthenticationFilter
BasicAuthenticationFilter 类似于用户名密码模式的认证,区别在于用户名密码信息不是用表单提交,而是通过Base64编码存在Http Header中。例如: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
RequestCacheAwareFilter
RequestCacheAwareFilter 用于获取记录在session中的包装过的HttpServletReqeust ,传给下一个过滤器。如果没有,则继续把当前HttpServletRequest 传下去。可以在登录后跳转到原页面的场景中用到。
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
RememberMeAuthenticationFilter
RememberMeAuthenticationFilter 主要实现了在之前的登录中选中’记住我’的选项后,本次就无需再做登录操作,可直接进入系统。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
}
catch (AuthenticationException authenticationException) {
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response,
authenticationException);
}
}
chain.doFilter(request, response);
}
else {
chain.doFilter(request, response);
}
}
再看下RememberMeAuthenticationFilter::doFilter 中的核心逻辑方法RememberMeServices::autoLogin
@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);
UserDetails user = null;
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
}
cancelCookie(request, response);
return null;
}
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter 对未认证的请求,会封装成一个匿名AnonymousAuthenticationToken 。
SessionManagementFilter
SessionManagementFilter 主要是对Session相关的管理,例如,防止会话固定攻击Session Fixation Attack ,支持多端登录踢下线等功能。
ExceptionTranslationFilter
ExceptionTranslationFilter 用于处理 AccessDeniedException 和 AuthenticationException 的异常。
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
FilterSecurityInterceptor
FilterSecurityInterceptor 主要负责授权Authorization 任务。
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
|