SpringSecurity原理(一)
一、认证的两种方式的介绍
1. 基于Session的认证方式
在之前的单体架构时代,我们认证成功之后都会将信息存入到Session中,然后响应给客户端的是对应的Session中数据的key,客户端会将这个key存储在cookie中,之后都请求都会携带这个cookie中的信息,结构图如下
但是随着技术的更新迭代,我们在项目架构的时候更多的情况下会选择前后端分离或者分布式架构,那么在这种情况下基于session的认证方式就显露了很多的不足,列举几个明显的特点:
- cookie存储的内容有限制4k
- cookie的有效范围是当前域名下,所以在分布式环境下或者前后端分离的项目中都不适用,及时要用也会很麻烦
- 服务端存储了所有认证过的用户信息
2.基于Token的认证方式
相较于Session对需求的兼容,基于Token的方式便是我们在当下项目中处理认证和授权的实现方式的首先了,Token的方式其实就是在用户认证成功后便把用户信息通过加密封装到了Token中,在响应客户端的时候会将Token信息传回给客户端,当下一次请求到来的时候在请求的Http请求的head的Authentication中会携带token
二、SSO和OAuth2.0流程浅析
前面介绍了下认证信息的实现方式,接下来看下我们在分布式环境下会经常碰到的两种解决方案SSO和OAuth2.0
1.SSO
SSO也就是我们经常听到的单点登录,是我们在分布式环境下认证实现的解决方案,具体流程如下:当系统发生第一次访问请求时,系统1肯定是没有提供认证的,这时请求就会跳转到认证中心,如果没有认证就会跳转到登录页面,在登录页面输入正确的帐号密码就可以完成认证过程;认证完成后就会创建一个全局对话及权限令牌;然后再次访问系统1时就会效验令牌信息是否正确,如果正确就可以正常访问业务流程;
2.OAuth2.0
2.1、定义
OAuth是一个开发标准,在这种标准下允许用户让第三方应用访问该用户在某一网站上存储的私密的资源数据,无须将用户名和密码提供给第三方应用。OAuth2.0是OAuth协议的第二个版本,关注客户端开发者的简易性,同时为Web应用、桌面应用和手机等设备提供专门的认证流程。
2.2、基本原理
OAuth在第三方应用与服务提供商之间设置了一个授权层。第三方应用不能直接登录服务提供商,只能登录授权层,以此将用户与客户端区分开来。第三方应用登录授权层所用的令牌,与用户的密码不同。用户可以在登录授权的时候,指定授权层令牌的权限范围和有效期。 第三方应用登录授权层以后,服务提供商根据令牌的权限范围和有效期,向第三方应用开放用户资源。
2.3、作用
让客户端安全可控地获取用户的授权,与服务提供商之间进行交互。可以免去用户同步的麻烦,同时也增加了用户信息的安全。
2.4、常用应用场景
单点登录解决的分布式系统中统一认证的问题,还有一种情况是一个新的系统用户就需要去注册一个账号,用户管理的账号越多越麻烦,为了解决这个问题,当前系统就期望使用你的其他系统的资料来作为认证的信息,比如 微信,QQ,微博等,这时候就该OAuth2.0,实现流程如下
三、SpringSecurity介绍
1.基础环境准备
1.1基于XML文件的方式构建项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>SpringSecurityDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>SpringSecurityDemo Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
</dependencies>
<build>
<finalName>SpringSecurityDemo</finalName>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port> <!-- 访问端口-->
<path>/</path> <!-- 访问路径-->
</configuration>
</plugin>
</plugins>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
WEB.XML
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app version="2.5" id="WebApp_ID" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <display-name>Archetype Created Web Application</display-name> <!-- 初始化spring容器 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- post乱码过滤器 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 前端控制器 --> <servlet> <servlet-name>dispatcherServletb</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServletb</servlet-name> <!-- 拦截所有请求jsp除外 --> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 配置过滤器链 springSecurityFilterChain名称固定--> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %> <html> <head> <title>Title</title> </head> <body> <h1>登录管理</h1> <form action="/login" method="post"> 账号:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"><br> </form> </body> </html>
springsecurity.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd"> <!-- auto-config="true" 表示自动加载SpringSecurity的配置文件 use-expressions="true" 使用Spring的EL表达式 --> <security:http auto-config="true" use-expressions="true"> <security:intercept-url pattern="/login.jsp" access="permitAll()"></security:intercept-url> <!--<security:intercept-url pattern="/login.do" access="permitAll()"></security:intercept-url>--> <!-- 拦截资源 pattern="/**" 拦截所有的资源 access="hasAnyRole('role1')" 表示只有role1这个角色可以访问资源 --> <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"></security:intercept-url> <!-- 配置认证信息 login-page="/login.jsp" 自定义的登录页面 login-processing-url="/login" security中处理登录的请求 default-target-url="/home.jsp" 默认的跳转地址 authentication-failure-url="/failure.jsp" 登录失败的跳转地址 <security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/home.jsp" authentication-failure-url="/failure.jsp" />--> <!-- 配置退出的登录信息 <security:logout logout-url="/logout" logout-success-url="/login.jsp" /> <security:csrf disabled="true"/>--> </security:http> <!-- 设置认证用户来源 noop:SpringSecurity中默认 密码验证是要加密的 noop表示不加密 --> <security:authentication-manager> <security:authentication-provider> <security:user-service> <security:user name="zhang" password="{noop}123" authorities="ROLE_USER"></security:user> <security:user name="lisi" password="{noop}123" authorities="ROLE_ADMIN"></security:user> </security:user-service> </security:authentication-provider> </security:authentication-manager> </beans>
springmvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd"> <!-- 配置扫描路径--> <context:component-scan base-package="com.ghy.security.controller" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> </context:component-scan> </beans>
log4j.properties
log4j.rootCategory=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[ghy] %p [%t] %C.%M(%L) | %m%n
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd"> <import resource="classpath:springsecurity.xml"/> </beans>
启动项目然后访问,发现是可以访问到登录页面的,这个页面是默认的
输入帐号密码可以发现能跳转
然后输入注销网址发现也能注销进入登录页面
这一顿操作下来发现,这登录、注销页面都不是我们定义的,是默认的,这默认页面怎么来的,怎么替换;这是第一个问题,第二,现在的帐号密码是在配置文件中配置的,如果想从数据库中获取怎么获取以及怎么效验
2.原理分析
- 系统启动springSecurity做了啥
- 页面是怎么出来的
- 认证流程是怎么实现的
有了这个基本的认知后我们就可以来具体的分析SpringSecurity的核心原理了,为了便于大家的理解,我们先从客户端发送请求开始到显示登陆界面这条流程开始来分析。
2.1.第一次请求的流程梳理
基于XML的方式分析DelegatingFilterProxy
当我们在浏览器地址栏发送http://localhost:8080/index.html的时候在服务端会被 web.xml 中配置的DelegatingFilterProxy这个过滤器拦截
<!-- 配置过滤器链 springSecurityFilterChain名称固定--> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
所以请求的流程图应该是这样的
public final void init(FilterConfig filterConfig) throws ServletException { Assert.notNull(filterConfig, "FilterConfig must not be null"); this.filterConfig = filterConfig; PropertyValues pvs = new GenericFilterBean.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, (PropertyResolver)env)); this.initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException var6) { String msg = "Failed to set bean properties on filter '" + filterConfig.getFilterName() + "': " + var6.getMessage(); this.logger.error(msg, var6); throw new NestedServletException(msg, var6); } }
this.initFilterBean(); if (this.logger.isDebugEnabled()) { this.logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use"); } }
protected void initFilterBean() throws ServletException { synchronized(this.delegateMonitor) {
//这个delegate是一个过滤器 if (this.delegate == null) { if (this.targetBeanName == null) {
this.targetBeanName = this.getFilterName(); }
WebApplicationContext wac = this.findWebApplicationContext(); if (wac != null) {
this.delegate = this.initDelegate(wac); } } } }
上面的方法首先获取在web.xml中配置的FilterName的值也就是 springSecurityFilterChain,然后再获取Spring的IoC容器对象,如果容器对象不为空,然后执行this.initDelegate(wac);方法
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = this.getTargetBeanName(); Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class); if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig()); } return delegate; }
通过上面这个流程init()初始化过程就结束了,通过上面源码的解析我们能够发现DelegatingFilterProxy这个过滤器在初始的时候从Spring容器中获取了 FilterChainProxy 这个过滤器链的代理对象,并且把这个对象保存在了DelegatingFilterProxy的delegate属性中。那么当请求到来的时候会执行DelegatingFilterProxy的doFilter方法,那么我们就可以来看下这个方法里面又执行了什么
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { Filter delegateToUse = this.delegate;
if (delegateToUse == null) { synchronized(this.delegateMonitor) { delegateToUse = this.delegate; if (delegateToUse == null) { WebApplicationContext wac = this.findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?"); } delegateToUse = this.initDelegate(wac); } this.delegate = delegateToUse; } }
this.invokeDelegate(delegateToUse, request, response, filterChain); }
invokeDelegate是doFilter中的核心代码,字面含义就是调用委托对象。从具体源码来看也确实如此
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain); }
通过上面的代码分析我们发现,当一个请求到来的时候先通过DelegatingFilterProxy来拦截,但是DelegatingFilterProxy不处理具体的逻辑,而是将具体的处理操作交给了delegate过滤器来处理也就是FilterChainProxy来处理。
至于FilterChainProxy怎么来的会在介绍系统初始化的时候会介绍到这块儿的内容的。对于FilterChainProxy怎么处理请求的,根据上面的内容我们知道我们可以直接看doFilter方法即可
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); this.doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else {
//跟进 this.doFilterInternal(request, response, chain); } }
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request); HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest); if (filters != null && filters.size() != 0) {
FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); } else { 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); } }
上面方法中的核心代码第一个是this.getFilters((HttpServletRequest)fwRequest);在这个方法要注意一个概念就是在SpringSecurity中可以存在多个过滤器链,而每个过滤器链又可以包含多个过滤器
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.currentPosition == this.size) { if (FilterChainProxy.logger.isDebugEnabled()) { FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } this.firewalledRequest.reset(); this.originalChain.doFilter(request, response); } else {
++this.currentPosition; Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1); if (FilterChainProxy.logger.isDebugEnabled()) { FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); }
nextFilter.doFilter(request, response, this); } } }
在整个过滤器链中,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); this.logger.debug("Chain processed normally"); } catch (IOException var9) { throw var9; } catch (Exception var10) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10); RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); } if (ase == null) { if (var10 instanceof ServletException) { throw (ServletException)var10; } if (var10 instanceof RuntimeException) { throw (RuntimeException)var10; } throw new RuntimeException(var10); } if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10); } this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase); } }
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { this.logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception); this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) { this.logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception); this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); } else { this.logger.debug("Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception); this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); } } }
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { SecurityContextHolder.getContext().setAuthentication((Authentication)null);
this.requestCache.saveRequest(request, response); this.logger.debug("Calling Authentication entry point."); this.authenticationEntryPoint.commence(request, response, reason); }
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String redirectUrl = null; if (this.useForward) { if (this.forceHttps && "http".equals(request.getScheme())) { redirectUrl = this.buildHttpsRedirectUrlForRequest(request); } if (redirectUrl == null) { String loginForm = this.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 {
redirectUrl = this.buildRedirectUrlToLoginPage(request, response, authException); }
this.redirectStrategy.sendRedirect(request, response, redirectUrl); }
sendRedirect
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException { String redirectUrl = this.calculateRedirectUrl(request.getContextPath(), url); redirectUrl = response.encodeRedirectURL(redirectUrl); if (this.logger.isDebugEnabled()) { this.logger.debug("Redirecting to '" + redirectUrl + "'"); }
response.sendRedirect(redirectUrl); }
此处重定向的地址是 .../login 该请求会被DefaultLoginPageGeneratingFilter过滤器拦截,具体看下面对DefaultLoginPageGeneratingFilter的介绍
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); this.invoke(fi); }
decide方法在做投票选举,第一次的时候回抛出AccessDeniedException异常,而抛出的异常会被ExceptionTranslationFilter中的catch语句块捕获,进而执行handleSpringSecurityException方法。
private void init(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) {
this.loginPageUrl = "/login"; this.logoutSuccessUrl = "/login?logout"; this.failureUrl = "/login?error"; if (authFilter != null) { this.formLoginEnabled = true; this.usernameParameter = authFilter.getUsernameParameter(); this.passwordParameter = authFilter.getPasswordParameter(); if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) { this.rememberMeParameter = ((AbstractRememberMeServices)authFilter.getRememberMeServices()).getParameter(); } } if (openIDFilter != null) { this.openIdEnabled = true; this.openIDusernameParameter = "openid_identifier"; if (openIDFilter.getRememberMeServices() instanceof AbstractRememberMeServices) { this.openIDrememberMeParameter = ((AbstractRememberMeServices)openIDFilter.getRememberMeServices()).getParameter(); } } }
然后在doFilter方法中有判断请求的地址是否是'/login',如果是的话就拦截,否则就放过
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; boolean loginError = this.isErrorPage(request); boolean logoutSuccess = this.isLogoutSuccess(request); if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
chain.doFilter(request, response); } else {
String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length); response.getWriter().write(loginPageHtml); } }
3.基于SpringBoot方式的分析
基于配置文件的方式我们清楚了DelegatingFilterProxy是如何处理的,下面分析下在SpringBoot中也是如何处理的
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
然后再写一个登录后页面
启动项目后会发现控制台生成了一个密码
登录到登录页面帐号是user,密码就是控制台生成的密码
点击登录 可以进入我们定义的登录后页面
接下来就要分析入口,基于SpringBoot的自动装配我们知道整合第三方框架要加载的信息是在spring.factories中,如下
其中有三个我们需要关注的会自动装配的配置类
//初始化的核心配置类
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguratio n,\
//默认的USER帐号配置类
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoCo nfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfig uration,\
这三个中和DelegatingFilterProxy有关系的是第三个 SecurityFilterAutoConfiguration 所以我们就直接先来看这个
@Bean @ConditionalOnBean(name = DEFAULT_FILTER_NAME) public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration( SecurityProperties securityProperties) {
//这段代码和前面讲的拦截器比较类似,我们可以点进去看下做了啥 DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean( DEFAULT_FILTER_NAME); registration.setOrder(securityProperties.getFilter().getOrder()); registration.setDispatcherTypes(getDispatcherTypes(securityProperties)); return registration; }
进去之后看下结构,我们发现有个ServletContextInitializer,这个是上下文初始化的接口,我们进入看下
进入ServletContextInitializer类,进入发现只有一个入口onStartup,先他的RegistraitonBean实现
@FunctionalInterface public interface ServletContextInitializer { void onStartup(ServletContext servletContext) throws ServletException; }
public final void onStartup(ServletContext servletContext) throws ServletException { String description = this.getDescription(); if (!this.isEnabled()) { logger.info(StringUtils.capitalize(description) + " was not registered (disabled)"); } else {
this.register(description, servletContext); } }
protected final void register(String description, ServletContext servletContext) {
D registration = this.addRegistration(description, servletContext); if (registration == null) { logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)"); } else { this.configure(registration); } }
protected Dynamic addRegistration(String description, ServletContext servletContext) { Filter filter = this.getFilter();
return servletContext.addFilter(this.getOrDeduceName(filter), filter); }
然后再看getFilter方法,可以看到真实的创建了一个DelegatingFilterProxy对象并且指定的名声是springSecurityFilterChain也就是DelegatingFilterProxy代理的Spring容器的Filter的名称
public DelegatingFilterProxy getFilter() {
//这个过滤器名称就是外面写死的那个
//这new的玩意其实就和前面XML文件配置的springSecurityFilterChain过滤器一样
return new DelegatingFilterProxy(this.targetBeanName, this.getWebApplicationContext()) {
protected void initFilterBean() throws ServletException { } }; }
找到过滤器后我们向上返回一级,servletContext.addFilter(this.getOrDeduceName(filter), filter);把过滤器加载到上下文中去,然后再向上返一级看下this.configure(registration)方法,注意这个方法我们要进入AbstractFilterRegistrationBean中查看
protected void configure(Dynamic registration) { super.configure(registration); EnumSet<DispatcherType> dispatcherTypes = this.dispatcherTypes; if (dispatcherTypes == null) { T filter = this.getFilter(); if (ClassUtils.isPresent("org.springframework.web.filter.OncePerRequestFilter", filter.getClass().getClassLoader()) && filter instanceof OncePerRequestFilter) { dispatcherTypes = EnumSet.allOf(DispatcherType.class); } else { dispatcherTypes = EnumSet.of(DispatcherType.REQUEST); } } Set<String> servletNames = new LinkedHashSet(); Iterator var4 = this.servletRegistrationBeans.iterator(); while(var4.hasNext()) { ServletRegistrationBean<?> servletRegistrationBean = (ServletRegistrationBean)var4.next(); servletNames.add(servletRegistrationBean.getServletName()); } servletNames.addAll(this.servletNames); if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
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中开始处理
4.SpringSecurity初始化到底经历了什么
通过对 第一次请求的流程梳理 会看到一个FilterChainProxy 至于他是啥时候创建的及默认的过滤器链和过滤器是怎么来的,下面就看下SpringSecurity初始化的时候到底做了哪些事情,基于XML的初始化阶段其实就是各种解析器对标签的解析,过程比较繁琐这里我们就不去分析了,我们直接在SpringBoot项目中来分析,在SpringBoot项目中分析SpringSecurity的初始化过程显然我们需要从 spring.factories 中的SecurityAutoConfiguration开始
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
@ConfigurationProperties( prefix = "spring.security" ) public class SecurityProperties { public static final int BASIC_AUTH_ORDER = 2147483642; public static final int IGNORED_ORDER = -2147483648; public static final int DEFAULT_FILTER_ORDER = -100; private final SecurityProperties.Filter filter = new SecurityProperties.Filter(); private final SecurityProperties.User user = new SecurityProperties.User(); public SecurityProperties() { } public SecurityProperties.User getUser() { return this.user; } public SecurityProperties.Filter getFilter() { return this.filter; } public static class User { private String name = "user"; private String password = UUID.randomUUID().toString(); private List<String> roles = new ArrayList(); private boolean passwordGenerated = true; public User() { }
从上面我们很容易看到,如果我们想要更改他的原有帐号密码只用在配置文件 中如下配置
spring.security.user.name=admin
spring.security.user.password=admin
返回上一层继续,该类引入了 SpringBootWebSecurityConfiguration WebSecurityEnablerConfiguration SecurityDataConfiguration 这三个类,如果还要分析的话入口就只能是这三个类了
4.1、SpringBootWebSecurityConfiguration
@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({WebSecurityConfigurerAdapter.class}) @ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class}) @ConditionalOnWebApplication( type = Type.SERVLET ) public class SpringBootWebSecurityConfiguration { public SpringBootWebSecurityConfiguration() { } @Configuration( proxyBeanMethods = false )
//加载的优先级 @Order(2147483642) static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter { DefaultConfigurerAdapter() { } } }
4.2、WebSecurityEnablerConfiguration
该配置类会在SpringBootWebSecurityConfiguration
注入 Spring IoC 容器后启用 @EnableWebSecurity
注解。也就是说 WebSecurityEnablerConfiguration
目的仅仅就是在某些条件下激活 @EnableWebSecurity
注解。
@Configuration( proxyBeanMethods = false ) @ConditionalOnBean({WebSecurityConfigurerAdapter.class}) @ConditionalOnMissingBean( name = {"springSecurityFilterChain"} ) @ConditionalOnWebApplication( type = Type.SERVLET ) @EnableWebSecurity public class WebSecurityEnablerConfiguration { public WebSecurityEnablerConfiguration() { } }
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class}) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { boolean debug() default false; }
4.3、WebSecurityConfiguration
该配置类WebSecurityConfiguration
使用一个WebSecurity
对象基于用户指定的或者默认的安全配置,你可以通过继承 WebSecurityConfigurerAdapter
或者实现 WebSecurityConfigurer
来定制 WebSecurity
创建一个FilterChainProxy
Bean来对用户请求进行安全过滤。这个FilterChainProxy
的名称就是 WebSecurityEnablerConfiguration
上的 BeanIds.SPRING_SECURITY_FILTER_CHAIN
也就是 springSecurityFilterChain
,它是一个Filter,最终会被作为Servlet过滤器链中的一个Filter应用到Servlet容器中。安全处理的策略主要是过滤器的调用顺序。WebSecurityConfiguration
最终会通过 @EnableWebSecurity
应用到系统。
package org.springframework.security.config.annotation.web.configuration; import java.util.Collections; import java.util.List; import java.util.Map; import javax.servlet.Filter; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.ImportAware; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; /** * Spring Web Security 的配置类 : * 1. 使用一个 WebSecurity 对象基于安全配置创建一个 FilterChainProxy 对象来对用户请求进行安全过滤。 * 2. 也会暴露诸如 安全SpEL表达式处理器 SecurityExpressionHandler 等一些类。 * * @see EnableWebSecurity * @see WebSecurity * * @author Rob Winch * @author Keesun Baik * @since 3.2 */ @Configuration public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { private WebSecurity webSecurity; // 是否启用了调试模式,来自注解 @EnableWebSecurity 的属性 debug,缺省值 false private Boolean debugEnabled; private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers; private ClassLoader beanClassLoader; @Autowired(required = false) private ObjectPostProcessor<Object> objectObjectPostProcessor; /** * * 代理监听器 应该时监听 DefaultAuthenticationEventPublisher 的一些处理策略 */ @Bean public static DelegatingApplicationListener delegatingApplicationListener() { return new DelegatingApplicationListener(); } /** * * 安全SpEL表达式处理器 SecurityExpressionHandler 缺省为一个 DefaultWebSecurityExpressionHandler */ @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() { return webSecurity.getExpressionHandler(); } /** * Spring Security 核心过滤器 Spring Security Filter Chain , Bean ID 为 springSecurityFilterChain * @return the {@link Filter} that represents the security filter chain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); }
return webSecurity.build(); } /** * * 用于模板 如JSP Freemarker 的一些页面标签按钮控制支持 * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP * tag support. * @return the {@link WebInvocationPrivilegeEvaluator} * @throws Exception */ @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public WebInvocationPrivilegeEvaluator privilegeEvaluator() throws Exception { return webSecurity.getPrivilegeEvaluator(); } /** * * 用于创建web configuration的SecurityConfigurer实例, * 注意该参数通过@Value(...)方式注入,对应的bean autowiredWebSecurityConfigurersIgnoreParents * 也在该类中定义 * * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a * {@link WebSecurity} instance * @param webSecurityConfigurers the * {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to * create the web configuration * @throws Exception */ @Autowired(required = false) public void setFilterChainProxySecurityConfigurer( ObjectPostProcessor<Object> objectPostProcessor, @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception { webSecurity = objectPostProcessor .postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); } Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) { Integer order = AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException( "@Order on WebSecurityConfigurers must be unique. Order of " + order + " was already used on " + previousConfig + ", so it cannot be used on " + config + " too."); } previousOrder = order; previousConfig = config; } for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); } this.webSecurityConfigurers = webSecurityConfigurers; } /** * 从当前bean容器中获取所有的WebSecurityConfigurer bean。 * 这些WebSecurityConfigurer通常是由开发人员实现的配置类,并且继承自WebSecurityConfigurerAdapter * */ @Bean public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents( ConfigurableListableBeanFactory beanFactory){ return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory); } /** * A custom verision of the Spring provided AnnotationAwareOrderComparator that uses * {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class * instances for the {@link Order} annotation. * * @author Rob Winch * @since 3.2 */ private static class AnnotationAwareOrderComparator extends OrderComparator { private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator(); @Override protected int getOrder(Object obj) { return lookupOrder(obj); } private static int lookupOrder(Object obj) { if (obj instanceof Ordered) { return ((Ordered) obj).getOrder(); } if (obj != null) { Class<?> clazz = (obj instanceof Class ? (Class<?>) obj : obj.getClass()); Order order = AnnotationUtils.findAnnotation(clazz, Order.class); if (order != null) { return order.value(); } } return Ordered.LOWEST_PRECEDENCE; } } /* * 要是为了获取注解 @EnableWebSecurity 的属性 debugEnabled * * @see org.springframework.context.annotation.ImportAware#setImportMetadata(org. * springframework.core.type.AnnotationMetadata) */ public void setImportMetadata(AnnotationMetadata importMetadata) { Map<String, Object> enableWebSecurityAttrMap = importMetadata .getAnnotationAttributes(EnableWebSecurity.class.getName()); AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes .fromMap(enableWebSecurityAttrMap); debugEnabled = enableWebSecurityAttrs.getBoolean("debug"); if (webSecurity != null) { webSecurity.debug(debugEnabled); } } /* * (non-Javadoc) * * @see * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java. * lang.ClassLoader) */ public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; } }
4.4、SpringWebMvcImportSelector
该类是为了对 Spring Mvc 进行支持的。一旦发现应用使用 Spring Mvc 的核心前置控制器 DispatcherServlet
就会引入 WebMvcSecurityConfiguration
。主要是为了适配 Spring Mvc 。
4.5、OAuth2ImportSelector
该类是为了对 OAuth2.0
开放授权协议进行支持。ClientRegistration
如果被引用,具体点也就是 spring-security-oauth2
模块被启用(引入依赖jar)时。会启用 OAuth2
客户端配置 OAuth2ClientConfiguration
。
4.6、@EnableGlobalAuthentication
这个类主要引入了 AuthenticationConfiguration
目的主要为了构造 认证管理器 AuthenticationManager
;这个注解就做两件事,第一件是:创建一个ObjectPostProcessorConfiguration后置处理器的对象,帮我们把对象注入容器中;第二件事是创建一个认证的管理器
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
//认证的配置类
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}
进入认证配置类 AuthenticationConfiguration类
/* * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.security.config.annotation.authentication.configuration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; /** * Exports the authentication {@link Configuration} * * @author Rob Winch * @since 3.2 * */ @Configuration(proxyBeanMethods = false)
//ObjectPostProcessorConfiguration类是把对象放到容器中去 @Import(ObjectPostProcessorConfiguration.class) public class AuthenticationConfiguration { private AtomicBoolean buildingAuthenticationManager = new AtomicBoolean(); private ApplicationContext applicationContext; private AuthenticationManager authenticationManager; private boolean authenticationManagerInitialized; private List<GlobalAuthenticationConfigurerAdapter> globalAuthConfigurers = Collections .emptyList(); private ObjectPostProcessor<Object> objectPostProcessor; @Bean public AuthenticationManagerBuilder authenticationManagerBuilder( ObjectPostProcessor<Object> objectPostProcessor, ApplicationContext context) { LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context); AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context, AuthenticationEventPublisher.class); DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, defaultPasswordEncoder); if (authenticationEventPublisher != null) { result.authenticationEventPublisher(authenticationEventPublisher); } return result; } @Bean public static GlobalAuthenticationConfigurerAdapter enableGlobalAuthenticationAutowiredConfigurer( ApplicationContext context) { return new EnableGlobalAuthenticationAutowiredConfigurer(context); } @Bean public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(ApplicationContext context) { return new InitializeUserDetailsBeanManagerConfigurer(context); } @Bean public static InitializeAuthenticationProviderBeanManagerConfigurer initializeAuthenticationProviderBeanManagerConfigurer(ApplicationContext context) { return new InitializeAuthenticationProviderBeanManagerConfigurer(context); } public AuthenticationManager getAuthenticationManager() throws Exception { if (this.authenticationManagerInitialized) { return this.authenticationManager; } AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class); if (this.buildingAuthenticationManager.getAndSet(true)) { return new AuthenticationManagerDelegator(authBuilder); } for (GlobalAuthenticationConfigurerAdapter config : globalAuthConfigurers) { authBuilder.apply(config); } //如果想知道这个authenticationManager 的实现是哪个可以断点,会发现是ProviderManager authenticationManager = authBuilder.build(); if (authenticationManager == null) { authenticationManager = getAuthenticationManagerBean(); } this.authenticationManagerInitialized = true; return authenticationManager; } @Autowired(required = false) public void setGlobalAuthenticationConfigurers( List<GlobalAuthenticationConfigurerAdapter> configurers) { configurers.sort(AnnotationAwareOrderComparator.INSTANCE); this.globalAuthConfigurers = configurers; } @Autowired public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Autowired public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) { this.objectPostProcessor = objectPostProcessor; } @SuppressWarnings("unchecked") private <T> T lazyBean(Class<T> interfaceName) { LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource(); String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( applicationContext, interfaceName); if (beanNamesForType.length == 0) { return null; } String beanName; if (beanNamesForType.length > 1) { List<String> primaryBeanNames = getPrimaryBeanNames(beanNamesForType); Assert.isTrue(primaryBeanNames.size() != 0, () -> "Found " + beanNamesForType.length + " beans for type " + interfaceName + ", but none marked as primary"); Assert.isTrue(primaryBeanNames.size() == 1, () -> "Found " + primaryBeanNames.size() + " beans for type " + interfaceName + " marked as primary"); beanName = primaryBeanNames.get(0); } else { beanName = beanNamesForType[0]; } lazyTargetSource.setTargetBeanName(beanName); lazyTargetSource.setBeanFactory(applicationContext); ProxyFactoryBean proxyFactory = new ProxyFactoryBean(); proxyFactory = objectPostProcessor.postProcess(proxyFactory); proxyFactory.setTargetSource(lazyTargetSource); return (T) proxyFactory.getObject(); } private List<String> getPrimaryBeanNames(String[] beanNamesForType) { List<String> list = new ArrayList<>(); if (!(applicationContext instanceof ConfigurableApplicationContext)) { return Collections.emptyList(); } for (String beanName : beanNamesForType) { if (((ConfigurableApplicationContext) applicationContext).getBeanFactory() .getBeanDefinition(beanName).isPrimary()) { list.add(beanName); } } return list; } private AuthenticationManager getAuthenticationManagerBean() { return lazyBean(AuthenticationManager.class); } private static <T> T getBeanOrNull(ApplicationContext applicationContext, Class<T> type) { try { return applicationContext.getBean(type); } catch(NoSuchBeanDefinitionException notFound) { return null; } } private static class EnableGlobalAuthenticationAutowiredConfigurer extends GlobalAuthenticationConfigurerAdapter { private final ApplicationContext context; private static final Log logger = LogFactory .getLog(EnableGlobalAuthenticationAutowiredConfigurer.class); EnableGlobalAuthenticationAutowiredConfigurer(ApplicationContext context) { this.context = context; } @Override public void init(AuthenticationManagerBuilder auth) { Map<String, Object> beansWithAnnotation = context .getBeansWithAnnotation(EnableGlobalAuthentication.class); if (logger.isDebugEnabled()) { logger.debug("Eagerly initializing " + beansWithAnnotation); } } } /** * Prevents infinite recursion in the event that initializing the * AuthenticationManager. * * @author Rob Winch * @since 4.1.1 */ static final class AuthenticationManagerDelegator implements AuthenticationManager { private AuthenticationManagerBuilder delegateBuilder; private AuthenticationManager delegate; private final Object delegateMonitor = new Object(); AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder) { Assert.notNull(delegateBuilder, "delegateBuilder cannot be null"); this.delegateBuilder = delegateBuilder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (this.delegate != null) { return this.delegate.authenticate(authentication); } synchronized (this.delegateMonitor) { if (this.delegate == null) { this.delegate = this.delegateBuilder.getObject(); this.delegateBuilder = null; } } return this.delegate.authenticate(authentication); } @Override public String toString() { return "AuthenticationManagerDelegator [delegate=" + this.delegate + "]"; } } static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder { private PasswordEncoder defaultPasswordEncoder; /** * Creates a new instance * * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use. */ DefaultPasswordEncoderAuthenticationManagerBuilder( ObjectPostProcessor<Object> objectPostProcessor, PasswordEncoder defaultPasswordEncoder) { super(objectPostProcessor); this.defaultPasswordEncoder = defaultPasswordEncoder; } @Override public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication() throws Exception { return super.inMemoryAuthentication() .passwordEncoder(this.defaultPasswordEncoder); } @Override public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception { return super.jdbcAuthentication() .passwordEncoder(this.defaultPasswordEncoder); } @Override public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService( T userDetailsService) throws Exception { return super.userDetailsService(userDetailsService) .passwordEncoder(this.defaultPasswordEncoder); } } static class LazyPasswordEncoder implements PasswordEncoder { private ApplicationContext applicationContext; private PasswordEncoder passwordEncoder; LazyPasswordEncoder(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public String encode(CharSequence rawPassword) { return getPasswordEncoder().encode(rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return getPasswordEncoder().matches(rawPassword, encodedPassword); } @Override public boolean upgradeEncoding(String encodedPassword) { return getPasswordEncoder().upgradeEncoding(encodedPassword); } private PasswordEncoder getPasswordEncoder() { if (this.passwordEncoder != null) { return this.passwordEncoder; } PasswordEncoder passwordEncoder = getBeanOrNull(this.applicationContext, PasswordEncoder.class); if (passwordEncoder == null) { passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); } this.passwordEncoder = passwordEncoder; return passwordEncoder; } @Override public String toString() { return getPasswordEncoder().toString(); } } }
这一段介绍说完了,下面就要看业务了,回退到WebSecurityConfiguration类,里面有个重要方法
/** * Spring Security 核心过滤器 Spring Security Filter Chain , Bean ID 为 springSecurityFilterChain * @return the {@link Filter} that represents the security filter chain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); } // 获取到的就是FilterChainProxy 该对象会被保存到IoC容器中 且name为 springSecurityFilterChain return webSecurity.build(); }
点击build进入AbstractSecurityBuilder类中的build()方法中
public final O build() throws Exception { if (this.building.compareAndSet(false, true)) {
//里面没什么东西,所以继续跟进doBuild()方法 this.object = doBuild(); return this.object; } throw new AlreadyBuiltException("This object has already been built"); }
@Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; beforeInit();
//初始化方法 init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result; } }