1. 带着疑问去学习
在使用spring-security时,我们应该去思考一些问题:
- 系统在启动spring-security时做了哪些事
- 默认的认证界面如何出现的
- 默认的认证流程如何实现
2. 流程分析
客户端发起请求到最终的servlet可能会经过如下过程:
我们可以思考一下,权限认证是不是也是其中某一个或者某几个filter呢?
基于XML分析
<filter>
<filter-name>springSecurityFilterChain</filter-name> <!-- 这里的名称是固定的 -->
<filter-class>org.springframeword.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
DelegatingFilterProxy并不是SpringSecurity提供,而是Spring框架本身就存在的。它继承自GenericFilterBean,Servlet容器在启动的时候就会执行GenericFilterBean的init方法。
public final void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
this.filterConfig = filterConfig;
// Set bean properties from init parameters.
PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
Environment env = this.environment;
if (env == null) {
env = new StandardServletEnvironment();
}
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
String msg = "Failed to set bean properties on filter '" +
filterConfig.getFilterName() + "': " + ex.getMessage();
logger.error(msg, ex);
throw new NestedServletException(msg, ex);
}
}
// 子类扩展,初始化自己的filter.
initFilterBean();
if (logger.isDebugEnabled()) {
logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
}
}
我们看看子类DelegatingFilterProxy的initFilterBean()方法里面做了什么事
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
if (this.targetBeanName == null) {
//这里获取的就是web.xml中配置的filterName=springSecurityFilterChain
this.targetBeanName = getFilterName();
}
// 获取IOC容器对象.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
//获取委托处理请求的过滤器,这里的实际过滤是通过FilterChainProxy处理的
this.delegate = initDelegate(wac);
}
}
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
// 获取的值是springSecurityFilterChain
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
// SpringIOC容器中是根据springSecurityFilterChain这个名称获取bean对象的,
// 所以web.xml中必须使用springSecurityFilterChain命名filter, 否则获取不到
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
在debug模式下我们可以看到真实的具体信息
通过上面源码的解析我们能够发现DelegatingFilterProxy这个过滤器在初始的时候从Spring容器中获取了 FilterChainProxy 这个过滤器链的代理对象,并且把这个对象保存在了DelegatingFilterProxy 的delegate属性中。那么当请求到来的时候会执行DelegatingFilterProxy的doFilter方法,那么我们就可以来看下这个方法里面又执行了什么
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Filter delegateToUse = this.delegate;
//if中的逻辑在初始化阶段已经完成
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// 核心代码,调用委托对象处理
invokeDelegate(delegateToUse, request, response, filterChain);
}
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//这里的delegate属性在之前的初始化阶段被赋值成FilterChainProxy
delegate.doFilter(request, response, filterChain);
}
实际流程变为:
FilterChainProxy请求处理分析
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 对request对象做防火墙检查,校验提交方式是否合法【post, get等】
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
// 核心方法,这里是获取这个请求过滤链中的所有过滤器
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
//存在过滤器的情况下,构建一个虚拟的过滤器链路并执行
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
我们需要了解的概念,SpringSecurity中可以存在多个过滤器链,而每个过滤器链又可以包含多个过滤器
spring-security中15个核心过滤器
SpringSecurity中的主要过滤器
ChannelProcessingFilter
处理https,没配置就没有?WebAsyncManagerIntegrationFilter
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成SecurityContextPersistenceFilter
new HttpRequestResponseHolder(request,response)
HttpSessionSecurityContextRepository#loadContext
从request中获取session,从Session中取出已认证用户的信息保存在SecurityContext中,提高效率, 避免
每一次请求都要解析用户认证信息,方便接下来的filter直接获取当前的用户信息
如果是第一次请求,session没有相关信息,那么会创建一个新的SecurityContext
包装request、response
SecurityContextHolder.setContext(contextBeforeChainExecution);
finally
将上下文保存到HttpSessionSecurityContextRepository
清除Holder中的上下文HeaderWriterFilter
往该请求的Header中添加相应的信息,在http标签内部使用security:headers来控制CorsFilter
未配置就没有CsrfFilter
对需要验证的请求验证是否包含csrf的token信息,如果不包含,则报错。 这样攻击网站无法获取到
token信息,则跨域提交的信息都无法通过过滤器的校验LogoutFilter
根据request的请求方法和路径匹配判断当前是否为注销URL
默认匹配 POST /logout
this.handler.logout(request, response, auth);
CsrfLogoutHandler
SecurityContextLogoutHandler
使session失效
清除remember me
清除SecurityContextHolder的SecurityContext
LogoutSuccessEventPublishingLogoutHandler
通知注销事件
SimpleUrlLogoutSuccessHandler#onLogoutSuccess(request, response, auth);
重定向,默认为/login?logout
...-
UsernamePasswordAuthenticationFilter
遍历本Manager和父Manager的所有AuthenticationProvider 如果有AuthenticationProvider支持处理当前类型的Authenticationif (!provider.supports(toTest)) { continue; } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } /** 穿插一点使用的知识点 1. 可自定义Authenticaton类,实现Authentication接口完成 2. 自定义provider进行用户身份鉴权,支持类型是自己自定义的Authenticaton即可 /
try { user = retrieveUser(username,authentication); }catch (UsernameNotFoundException notFound){ throw new BadCredentialsException(xx) } protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //核心处理,这里可以自定义实现 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } ... } // 上述自定义实现,可以通过实现UserDetailService接口,实现loadUserByUsername方法,加载自己业务的用户信息,从数据库中读取啥的
用户相关前置判断
preAuthenticationChecks.check(user);
AbstractUserDetailAuthenticationProvider.DefaultPreAuthenticationChecks
!user.isAccountNonLocked() 账号是否被锁定
!user.isEnabled() 账号是否可用
!user.isAccountNonExpired() 账号是否过期
验证密码是否正确
additionalAuthenticationChecks(user,authentication)
如果验证成功
擦除所有地方的密码信息
发布AuthenticationSuccessEvent事件
如果验证失败
抛出AuthenticationException
清除SecurityContext
... -
DefaultLoginPageGeneratingFilter
if (isLoginUrlRequest(request) || loginError || logoutSuccess) { //这里是根据身份验证结果,硬编码生成html页面返回 String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length); response.getWriter().write(loginPageHtml); return; }
-
DefaultLogoutPageGeneratingFilter
if (this.matcher.matches(request)) {//匹配url "/logout" renderLogout(request, response);//硬编码返回html页面 } else { filterChain.doFilter(request, response); }
ConcurrentSessionFilter
取出session
若过期,进入注销逻辑,return
没过期,更新session
没session就跳过
...BasicAuthenticationFilter
没配置就没有
处理HTTP请求中的BASIC authorization头部,把认证结果写入SecurityContextHolderRequestCacheAwareFilter
从缓存中寻找是否已经有解析过的请求,若有,替换掉原生请求
否则,继续
SecurityContextHolderAwareRequestFilter
封装Request,丰富API
...RememberMeAuthenticationFilter
当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统
AnonymousAuthenticationFilter
检测 SecurityContextHolder 中的SecurityContext是否存在 Authentication
如果不存在为其提供一个匿名 Authentication key随机生成,username为anonymous,权限只有ROLE_Anonymous
authenticated为true
否则跳过
...SessionManagementFilter
防止会话固定?;すセ?br> 限制已认证用户可以同时打开多少个会话ExceptionTranslationFilter
直接chain.doFilter(request, response)
通过catch处理下一个Filter或应用逻辑产生的异常
AuthenticationException
AccessDeniedException
当前是匿名认证或者认证信息不全
sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(xx))
其他
accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);FilterSecurityInterceptor
主要用于鉴权逻辑的处理,之前的UsernamePasswordAuthenticationFilter等都是用于登录逻辑处理。
过滤器如何执行
先前我们讲到,会构造出一个虚拟链路执行对应的filter。VirtualFilterChain
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
// size=15 currentPosition初始化为0 会从0-14一个一个取出过滤器执行
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
// 调用取出的过滤器执行对应的过滤方法,此处用到责任链模式
nextFilter.doFilter(request, response, this);
}
}
详细分析ExceptionTranslationFilter、FilterSecurityInterceptor
- 在整个过滤器链中,ExceptionTranslationFilter是倒数第二个执行的过滤器,它的作用是通过catch处理下一个Filter【也就是FilterSecurityInterceptor】或应用逻辑产生的异常
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
}catch (Exception ex) {
//....省略相关代码
if (ase != null) {
//...省略相关非核心代码
//异常核心处理方法
handleSpringSecurityException(request, response, chain, ase);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
//开始登录认证异常处理
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);//这里实际上是调用了AuthenticationEntryPoint.commence()方法来处理认证异常
}
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 {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
// 获取重定向的地址, 默认是http://localhost:8080/login
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
//重定向到指定路径页面
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
/**
使用说明:
1. 这里的AuthenticationEntryPoint可以自定义逻辑处理,实现对应的commence方法即可
2. 可根据相关逻辑重定向或者直接返回认证失败的信息,依据自身业务来
/
- FilterSecurityInterceptor是SpringSecurity过滤器链中的最后一个过滤器,作用是先判断是否身份
验证,然后在做权限的验证。第一次访问的时候处理的在doFilter中的方法的关键代码如下:
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}catch (AccessDeniedException accessDeniedException){
//发布AuthorizationFailureEvent事件
throw accessDeniedException; }
decide方法在做投票选举,第一次的时候回抛出AccessDeniedException异常,而抛出的异常会
被ExceptionTranslationFilter中的catch语句块捕获,进而执行handleSpringSecurityException方法。
基于SpringBoot方式分析
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
基于SpringBoot的自动装配,第三方框架要加载的信息在spring.factories中
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
这三个类中与DelegatingFilterProxy有关系的是SecurityFilterAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {
private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)//name = "springSecurityFilterChain"
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
//将filter添加到spring容器中
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
}
我们来看一下这里面的构建逻辑是如何处理的
从ServletContextInitializer.onStartU()开始分析
- RegistrationBean的onStartUp方法
- DynamicRegistrationBean的register方法
protected final void register(String description, ServletContext servletContext) { //1. 添加过滤器,DelegatingFilterProxy D registration = addRegistration(description, servletContext); if (registration == null) { logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)"); return; } //2. 设置拦截url 默认是 /* 拦截所有 configure(registration); }
AbstractFilterRegistrationBeanprotected Dynamic addRegistration(String description, ServletContext servletContext) { Filter filter = getFilter(); return servletContext.addFilter(getOrDeduceName(filter), filter); } //这里就很清晰了,显示的声明了一个DelegatingFilterProxy,并且指明beanName=springSecurityFilterChain public DelegatingFilterProxy getFilter() { return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) { @Override protected void initFilterBean() throws ServletException { // Don't initialize filter bean on init() } }; }
protected void configure(FilterRegistration.Dynamic registration) { ..... Set<String> servletNames = new LinkedHashSet<>(); for (ServletRegistrationBean<?> servletRegistrationBean : this.servletRegistrationBeans) { servletNames.add(servletRegistrationBean.getServletName()); } servletNames.addAll(this.servletNames); // DelegatingFilterProxyRegistrationBean 创建时没有指定ServletRegistrationBeans, urlPatterns。为空 if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) { // DEFAULT_URL_MAPPINGS="/*", 默认拦截所有 registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS); } else { if (!servletNames.isEmpty()) { registration.addMappingForServletNames(dispatcherTypes, this.matchAfter, StringUtils.toStringArray(servletNames)); } if (!this.urlPatterns.isEmpty()) { registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, StringUtils.toStringArray(this.urlPatterns)); } } }
至此,我们可以看到在springboot中,通过DelegatingFilterProxyRegistrationBean创建了一个,DelegatingFilterProxy过滤器并且执行了拦截地址是 /*。后续的流程就和xml流程一致,通过FilterChainProxy处理。