本文已参与「新人创作礼」活动,一起开启掘金创作之路
目录
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
SecurityContextHolderAwareRequestFilter
引言
之前写了几篇有关shiro和spring之间整合的文章。在实际的开发过程中除了用shiro作为安全管理框架之外,用的比较多的就是spring security框架了。相对于shiro而言spring security作为spring全家桶中的一员和spring boot的整合也会更加方便,同时功能也更加的丰富,属于重量级的安全框架。而且随着spring框架体系的日益完善,java 开发中spring框架的比重将会越来越大,所以学习了解spring security这个安全框架还是非常有必要的
框架概述
学习框架最好的办法就是看文档,所以我在学习这个spring security的时候除了去搜索相关的博客文章之外更多的是去spring的官网看官方文档。打开spring security的官方文档你会发现内容真的有点多。那么对于我们实际开发工作中,我想主要会用到的应该只是框架中几个核心组件。
首先和shiro类似的,安全框架的机制建立于Servlet Filter的基础上,经过框架的层层过滤器最终把客户端的请求提交到我们的服务端API上。 而spring提供了一种委托代理过滤器的实现,能够将spring的过滤器注册到原来的过滤器链上。每个SecurityFilterChain通过url进行匹配,注意的是这个链是有序的,如果过请求的url已经成功匹配,那么无论之后的过滤器能否与之匹配,后面的过滤器都不会被执行。命中即跳出。那么框架安全机制的具体实现就会由这些独立的securityFilter去执行实现,而spring也提供了一系列的不同功能的过滤器来扩展spring security的功能。
委托代理过滤器链
过滤器SecurityFilter
从上面的组件图来看这个SecurityFilter是非常重要的一环,spring 通过不同的SecurityFilter对访问进行控制。当我们的服务端接收一个来自客户端的请求的时候,这个请求会依次通过过滤器,经过每一层的过滤器的验证到达我们的业务层。在我们第一次接触spring security框架的时候可以打开debug日志,从中我们可以发现很多关于框架的信息。我们可以在spring boot的默认配置文件application.yml中将security的日志级别降低到debug级别。
这个时候如果我们运行项目可以通过控制台的日志输出看到spring security创建了一个filter chains。也就是所有请求会经过这个过滤器链。分别是
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- LogoutFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
我这边启动后是创建了一个有十一种过滤器的过滤器链。首先通过查看源码来认识一下它们
WebAsyncManagerIntegrationFilter
WebAsyncManagerIntegrationFilter它继承于OncePerRequestFilter。OncePerRequestFilter这个过滤器最简单的理解就是一次请求中只会经过一次。WebAsyncManagerIntegrationFilter主要的业务逻辑在它的doFilterInternal方法中
private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
public WebAsyncManagerIntegrationFilter() {
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor)asyncManager.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
if (securityProcessingInterceptor == null) {
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, new SecurityContextCallableProcessingInterceptor());
}
filterChain.doFilter(request, response);
}
首先通过静态方法获取到WebAsyncManager这个对象,这个对象是属于spring web中的一个组件通过名称来推测,这个是一个管理异步web请求的管理器。这里它会获取一个可回调的拦截器,WebAsyncManager会从它自身的成员变量callableInterceptors中去获取一个SecurityContextCallableProcessingInterceptor对象。内部本身是通过一个map存储这些拦截器的,如果返回的结果是null,那么会新创建一个SecurityContextCallableProcessingInterceptor对象,并且注册到WebAsyncManager。那么到这里可以明白这个过滤器最主要的目标就是注册SecurityContextCallableProcessingInterceptor到管理器中,注册它一定是有作用的。查看源码它主要是在请求的前后做业务的处理
public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
if (this.securityContext == null) {
this.setSecurityContext(SecurityContextHolder.getContext());
}
}
public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
SecurityContextHolder.setContext(this.securityContext);
}
public <T> void postProcess(NativeWebRequest request, Callable<T> task, Object concurrentResult) {
SecurityContextHolder.clearContext();
}
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter是第二个过滤器,它在一个web请求中也只会执行一次 。这个过滤器从名字来推测它的作用是存储securityContext,即存储用户凭证上下文,它会在所有的认证处理机制之前执行,它会处理SecurityContextHolder和SecurityContext做一些准备工作,因为在后续流程中大量的工作将会围绕SecurityContextHolder和SecurityContext展开。它的主要业务功能实现如下
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (request.getAttribute("__spring_security_scpf_applied") != null) {
chain.doFilter(request, response);
} else {
boolean debug = this.logger.isDebugEnabled();
request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
this.logger.debug("Eagerly created session: " + session.getId());
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var13 = false;
try {
var13 = true;
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
var13 = false;
} finally {
if (var13) {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
if (debug) {
this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
if (debug) {
this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
这里主要判断了一个参数__spring_security_scpf_applied。如果参数不为null那么直接放行。如果为null,首先将这个参数设置为true。forceEagerSessionCreation是用来判断是否需要立即创建一个session。通过HttpRequestResponseHolder从session中拉取一个spring security的上下文对象securityContext,之后的认证操作和权限验证都需要从这个securityContext中获取,这个对象存储了当前账号的基本信息。在将securityContext放入securityHolder的操作中通过一个布尔参数做了控制为的是如果在设置securityContext和后续过滤器业务中出现异常,能够保证__spring_security_scpf_applied参数能够被移除,以及securityContextHolder可以被移除。
HeaderWriterFilter
HeaderWriterFilter继承OncePerRequestFilter在一次请求中也只执行一次,它的业务功能主要是配置写入响应头,通过shouldWriteHeaderEagerly判断是在过滤器链之前写入还是之后写入
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (this.shouldWriteHeadersEagerly) {
this.doHeadersBefore(request, response, filterChain);
} else {
this.doHeadersAfter(request, response, filterChain);
}
}
private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
this.writeHeaders(request, response);
filterChain.doFilter(request, response);
}
private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HeaderWriterFilter.HeaderWriterResponse headerWriterResponse = new HeaderWriterFilter.HeaderWriterResponse(request, response);
HeaderWriterFilter.HeaderWriterRequest headerWriterRequest = new HeaderWriterFilter.HeaderWriterRequest(request, headerWriterResponse);
try {
filterChain.doFilter(headerWriterRequest, headerWriterResponse);
} finally {
headerWriterResponse.writeHeaders();
}
}
CorsFilter
这个是主要处理跨域资源共享的过滤器,同样也是继承OncePerRequestFilter,在请求中只执行一次,主要的业务功能如下
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)) {
filterChain.doFilter(request, response);
}
}
首先获取一个跨域相关的配置,将请求和这个跨域配置进行比较去得到一个请求是否有效的结果,
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {
Collection<String> varyHeaders = response.getHeaders("Vary");
if (!varyHeaders.contains("Origin")) {
response.addHeader("Vary", "Origin");
}
if (!varyHeaders.contains("Access-Control-Request-Method")) {
response.addHeader("Vary", "Access-Control-Request-Method");
}
if (!varyHeaders.contains("Access-Control-Request-Headers")) {
response.addHeader("Vary", "Access-Control-Request-Headers");
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
} else if (response.getHeader("Access-Control-Allow-Origin") != null) {
logger.trace("Skip: response already contains "Access-Control-Allow-Origin"");
return true;
} else {
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
this.rejectRequest(new ServletServerHttpResponse(response));
return false;
} else {
return true;
}
} else {
return this.handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
}
}
在方法中,会依次先处理Vary响应头,这个按网上说的,主要是用于客户端的缓存机制。然后判断这个请求是不是一个跨域请求。
这里备注一下跨域的定义,是否跨域看下面三个条件:
- 协议
- 域名
- 端口
三个条件满足任意一个就是跨域请求。如果不是跨域请求则之间返回是有效请求的结果。如果是跨域请求,那么会判断Access-Control-Allow-Origin这个响应头是否为空,如果不为空那么也同样返回是有效请求的结果。如果这个响应头的值也是空,那么判断这个请求是否是预检请求即Options请求。
这里备注一下options请求,指的是在跨域请求时浏览器向另一个域名资源发送的用于来判断实际发送的请求是否安全的操作。
public static boolean isPreFlightRequest(HttpServletRequest request) {
return HttpMethod.OPTIONS.matches(request.getMethod()) && request.getHeader("Origin") != null && request.getHeader("Access-Control-Request-Method") != null;
}
在corsFilter中如果过当前请求的方法是OPTIONS,同时请求头的Origin不是null,同时Access-Control-Request-Method不为null,那么它就是一次预检请求。当当前请求跨域,响应头没有设置允许的源,跨域配置又是null的时候,如果是预检请求就直接拒绝,不会再走向下个过滤器,反之不是预检请求就返回是有效请求的结果。如果跨域的配置不是null那么就会根据跨域的配置,在响应头依次检验和写入允许的origin、method、header、credentials等信息。回到最初的corsFilter中如果返回的是无效请求那么就无法执行到下个过滤器中。否则,就还需要判断是否是预检请求。因为对于预检请求来说,我们的服务器不需要处理其他的业务逻辑,也就不需要再执行到下个过滤器了。
LogoutFilter
这个过滤器顾名思义就是退出登录时所需要用到的过滤器
private RequestMatcher logoutRequestMatcher;
private final LogoutHandler handler;
private final LogoutSuccessHandler logoutSuccessHandler;
过滤器中主要有三个成员变量,第一个是用来判断当前请求路由是不是所配置的退出登录的路由,第二个是退出登录是你需要对账号做的一些业务处理,第三个是退出登录成功(或者说上面这个退出登录logoutHandler的业务逻辑处理完成后)所需要执行的。
这几个组件后面会结合实际代码来说会比较好理解一点。这里先看logoutFilter中的业务逻辑
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (this.requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Logging out user '" + auth + "' and transferring to logout destination");
}
this.handler.logout(request, response, auth);
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
} else {
chain.doFilter(request, response);
}
}
这里首先判断当前的请求路由是不是退出登录的路由,如果不是那么不会执行这个退出登录过滤器的具体逻辑会直接走向下个过滤器。如果是退出登录的路由,那么首先会去执行退出登录的业务逻辑,默认是由SecurityContextLogoutHandler去实现这个logout方法的主要是使当前的session失效,清空当前账号的认证信息,使得当前的securityHolder为null。通俗理解就是把你之前登陆所写入的这个账号认证信息统统移除。当这些逻辑执行完后那么我们的系统就认为我们推出登录的逻辑流程是成功走完了。接下去需要进行退出登录后的执行操作。如果没有自定义配置退出登录成功的处理的话,默认是走跳转,即跳转到退出登录成功的页面。
RequestCacheAwareFilter
这个过滤器主要是请求缓存的处理,如果当前的请求和缓存中的请求匹配了那么会返回缓存中请求去走向下个过滤器,如果没有匹配到,那么会使用原来的请求去走向下个过滤器。
SecurityContextHolderAwareRequestFilter
这个过滤器的作用是对servlet请求进行包装。
AnonymousAuthenticationFilter
这个过滤器是用于对账号设置匿名访问的认证操作的,对于当前请求会生成一个特定的匿名访问的token,然后会对当前请求用户保存一个具有匿名访问权限的账号信息。这样账号在走向后面的过滤器之前就已经具有了匿名访问的权限。
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(this.key, this.principal, this.authorities);
auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
return auth;
}
SessionManagementFilter
这个session管理过滤器主要会涉及账号认证的问题。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (request.getAttribute("__spring_security_session_mgmt_filter_applied") != null) {
chain.doFilter(request, response);
} else {
request.setAttribute("__spring_security_session_mgmt_filter_applied", Boolean.TRUE);
if (!this.securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
try {
this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
} catch (SessionAuthenticationException var8) {
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", var8);
SecurityContextHolder.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, var8);
return;
}
this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
} else if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Requested session ID " + request.getRequestedSessionId() + " is invalid.");
}
if (this.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
chain.doFilter(request, response);
}
}
和之前的过滤器一样有个参数作为了判断的标志__spring_security_session_mgmt_filter_applied。当这个参数不为null的时候直接放行,反之则判断当前请求之前是否保存过当前请求的上下文,如果是true那么直接放行,反之再进行两种判断。一是认证信息是否不为null,二是当前的认证不能是匿名认证,当满足这两个条件之后会判断当前session的认证信息,如果一场则会抛出一个认证类的异常交给认证异常处理器处理同时清除上下文。如果没有异常则会保存当前的上下文。当前请求不满足前面两个条件时就会去校验这次请求的sessionId然后来判断是否放行
ExceptionTranslationFilter
这个过滤器主要是用来捕获和处理异常的。它同样是配置在过滤器链中它主要可以认为是处理两类的异常一种是authentication认证异常,另一种是AccessDeniedException访问异常。这两个异常分别是通过AuthenticationEntryPoint和AccessDeniedHandler去捕获
FilterSecurityInterceptor
这个过滤器主要是用来处理权限访问控制的,它会获取账号信息包括权限信息进行判断,如果权限不足则会将抛出AccessDeniedException异常,并由AccessDeniedHandler来进行捕获。
小结
上面就是spring security框架中主要所使用的一些过滤器,可以发现和shiro对比,spring security额外的为我们封装了了许多的过滤器组件,这样能够大大的提高开发的效率。至此对于spring security应该有了一定的认识,但是我觉得光看这些组件是不够的,只有实际使用操作起来才能使开发者真正的掌握这些技术