- SecurityFilterChain
- FilterChainProxy
- 内置SecurityFilter
- ChannelProcessingFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- CsrfFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- ConcurrentSessionFilter
- BearerTokenAuthenticationFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
讲过滤器[Security Filter]
之前,首先要提到SecurityFilterChain
。
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}
SecurityFilterChain
包含了两个方法。
- matches(HttpServletRequest request):主要是提供给
FilterChainProxy
进行请求的路由。 - List getFilters(): 提供
SecurityFilterChain
中的Security Filter
。
FilterChainProxy
继承于GenericFilterBean
,是spring容器中的一个过滤器。
它主要做了两个工作:
- 内置了一个防火强
HttpFirewall
。对ServletRequest
中的HttpMethod
、ContextPath
、Hostname
、uri
等做了限制。具体内容可以查看StrictHttpFirewall::getFirewalledRequest
。 - 执行内部的
SecurityFilterChain
,直到SecurityFilterChain
中的Security Filter
执行完毕后,继续执行Servlet Container
中的剩余的Filter
。详见FilterChainProxy.VirtualFilterChain::doFilter
:
/**
* additionalFilters 表示路由选中的SecurityFilterChain。
* currentPosition 表示当前正在执行的SecurityFilterChain中的SecurityFilter位置标记。
* this.size 表示整条SecurityFilterChain中SecurityFilter的长度
*/
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
//如果SecurityFilterChain中的最后一个SecurityFilter执行完成,则继续执行原Servlet容器中的过滤器
if (this.currentPosition == this.size) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest)));
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
return;
}
//SecurityFilterChain标识位移动到下个
this.currentPosition++;
//获取SecurityFilterChain中的下一个Security Filter
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));
}
//执行获取的这个Security Filter
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()
//访问/admin-api,需要以https开头的方式访问
.antMatchers("/admin-api").access("REQUIRES_SECURE_CHANNEL")
//访问/actuator,不受安全通道限制
.antMatchers("/actuator").access("ANY_CHANNEL");
}
WebAsyncManagerIntegrationFilter
WebAsyncManagerIntegrationFilter
主要是用于集成WebAsyncManager
,支持在异步线程中管理SecurityContext
。
SecurityContextPersistenceFilter
主要实现了对SecurityContext
的在Session
中的持久化和存取功能。
/**
* this.repo 是HttpSessionSecurityContextRepository默认实现
*/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
/**省略部分代码.... */
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//HttpSessionSecurityContextRepository 从Session中获取SecurityContext
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
//把SecurityContext注入到SecurityContextHolder,SecurityContextHolder中三种SecurityContextHolderStrategy策略,详见下文。
SecurityContextHolder.setContext(contextBeforeChainExecution);
/**省略部分代码.... */
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
//删除线程中的SecurityContext
SecurityContextHolder.clearContext();
//持久化到SecurityContextRepository
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
/**省略部分代码.... */
}
}
默认提供了3种对SecurityContext
线程持有策略,可以通过设置系统参数spring.security.strategy
来实现切换:
- MODE_THREADLOCAL: 只能获取当前线程的
SecurityContext
。 - MODE_INHERITABLETHREADLOCAL: 能够获取父线程和当前线程的
SecurityContext
。 - MODE_GLOBAL: 能够获全局应用的
SecurityContext
。
HeaderWriterFilter
通过一系列HeaderWriter
,往HttpServletResponse
的Http Header中写入值。可以通过设置shouldWriteHeadersEagerly
,来控制前置写入还是后置写入。
CorsFilter
通过配置CorsConfiguration
,设置CORS Http Header。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//从CorsConfigurationSource中获取CorsConfiguration
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
//CorsProcessor通过CorsConfiguration对Response进行设置。
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
//如果处理不成功或者当前请求是一个CORS pre-flight,则阻止当前请求继续执行。
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);
//从Token库中获取CsrfToken(支持CsrfToke持久化在cookie、session或自定义库的中)
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);
if (missingToken) {
//第一次请求需要生成一个CsrfToken,并持久化到Token库。
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()
//访问/admin-api,需要以https开头的方式访问
.antMatchers("/admin-api").access("REQUIRES_SECURE_CHANNEL")
//访问/actuator,不受安全通道限制
.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));
}
//LogoutHandler处理登出 *** 作
this.handler.logout(request, response, auth);
//LogoutSuccessHandler处理成功登出后的 *** 作
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())
// sample logout customization
.logout(logout ->
//删除Cookie
logout.deleteCookies("remove")
//使session失效
.invalidateHttpSession(false)
//登出地址
.logoutUrl("/custom-logout")
//登出后跳转地址
.logoutSuccessUrl("/logout-success")
);
}
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
通过用户名密码进行认证。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//开启postOnly开关,默认只支持POST方式请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//获取HttpServletRequest中的username和password,只支持表单模式提交
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//通过用户名和密码,组装成UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
// 支持子类向UsernamePasswordAuthenticationToken注入更多属性
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) {
//从session库中获取SessionInformation
SessionInformation info = sessionRegistry.getSessionInformation(session
.getId());
if (info != null) {
//SessionInformation存在但已过期的情况下
if (info.isExpired()) {
// Expired - abort processing
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " has expired.");
}
//进行登出处理
//主要由SecurityContextLogoutHandler负责处理清空Session、Authentication、SecurityContext
doLogout(request, response);
//通过ExpiredStrategy实现后续处理。比如,重定向跳转or返回提示信息
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
else {
//正常情况下,更新SessionInformation中的最后请求时间
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 {
//从HttpServletRequest中解析出Token
token = this.bearerTokenResolver.resolve(request);
} catch ( OAuth2AuthenticationException invalid ) {
//组装一个bearer token error response返回。详见BearerTokenAuthenticationEntryPoint::commence
this.authenticationEntryPoint.commence(request, response, invalid);
return;
}
//没有解析到Token,直接进入下个过滤器
if (token == null) {
filterChain.doFilter(request, response);
return;
}
//组装好BearerTokenAuthenticationToken,并注入一些额外属性
BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
try {
//从Request解析生成AuthenticationManager,并进行认证
AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
//生成新的SecurityContext,并注入Authentication。并把SecurityContext注入到SecurityContextHolder。
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
filterChain.doFilter(request, response);
} catch (AuthenticationException failed) {
//清空SecurityContext
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
用于获取记录在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) {
//获取RememberMeAuthenticationToken
Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
try {
//对RememberMeAuthenticationToken进行认证
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// 记录认证后的Authentication
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
/**省略部分代码.... */
}
catch (AuthenticationException authenticationException) {
/**省略部分代码.... */
//登录失败处理,主要做的是清空Cookie
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) {
//提取Cookie中的信息
String rememberMeCookie = extractRememberMeCookie(request);
/**省略部分代码.... */
UserDetails user = null;
try {
//解析信息,获取一个UserDetails
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
//通过UserDetails生成一个RememberMeAuthenticationToken
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
/**省略部分代码.... */
}
cancelCookie(request, response);
return null;
}
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter
对未认证的请求,会封装成一个匿名AnonymousAuthenticationToken
。
SessionManagementFilter
主要是对Session相关的管理,例如,防止会话固定攻击Session Fixation Attack
,支持多端登录踢下线等功能。
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 {
/**省略部分代码.... */
//主要是通过AccessDecisionManager::decide 来进行授权工作。
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
//把Token中的SecurityContext去刷新SecurityContextHolder中的SecurityContext
super.finallyInvocation(token);
}
//用AfterInvocationManager::decide方法进行后置授权工作。
super.afterInvocation(token, null);
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)