在介紹Spring Securiry之前,我們試想一下如果我們自己去實現一個安全框架,我們需要包含哪些功能:
- 我們需要對登錄接口或者一些不需要權限的接口放行,同時我們需要對某些接口進行身份認證,例如:在基於jwt的認證體系中,我們需要校驗token是否合法,token合法我們才會放行;
- 我們希望我們寫的安全框架能夠做一些授權的操作,比如:我們可以限制認證后的用戶訪問/user接口需要什么權限,訪問/group接口又需要什么權限;
- 我們希望安全框架能夠提供一個緩存,無論是TreadLocal、還是HttpServletRequest也罷,只要能夠獲取保存當前認證通過的用戶信息即可;
試想一下,如果我們去實現這些功能,我們需要怎么做:
- 我們需要去攔截所有的HTTP請求,我們首先想到的實現方式就是filter、Spring AOP、Intercepter,這三者的實現方式和應用場景都不一樣,這里我們不去細究采用哪種方式,但是我可以告訴你Spring Security是采用了一系列的filter實現的。
- 假設我們也是采用的filter實現,那么我們是不是也要實現一個白名單啊,比如放行/login接口啊,然后剩下的接口,就要走認證流程;
- 認證完之后,我們怎么做授權呢,我們可以這么做,我們先獲取當前登錄用戶所擁有的權限,比如某某用戶對接口資源user具有添加權限,采用這種格式:interface:user:add,我們將若干個這種格式的權限放到一個list,然后放到緩存中
[ "interface:user:add", "nterface:group:delete", ... ]
- 然后我們干一件什么事呢,我們搞個@PreAuthorize注解,然后在搞個注解處理器,注解處理器可以使用Spring AOP去實現。這個注解怎么用呢,我們可以將這個注解加載UserController /user/add接口上:
@PostMapping("/user/add") @PreAuthorize("interface:user:add") public NgspResponseEntity<User> insertUser(...)
- 如果用戶去訪問/user/add接口,我們就去緩存中拉取用戶的權限列表,然后去校驗用戶是否具有訪問這個接口的權限,如果有那么我們就放行。
當然了上面只是一個簡單的實現,Spring Security的實現那是太太太復雜了,他為了滿足各種需求,允許我們自己去配置各種過濾器,功能是強大了,但是學習起來還是比較困難的。
下面我將帶領大家來學習Spring Security框架,Spring Security是一款基於Spring的安全框架,主要包含認證和授權兩大安全模塊,和另外一款流行的安全框架Apache Shiro相比,它擁有更為強大的功能。Spring Security也可以輕松的自定義擴展以滿足各種需求,並且對常見的Web安全攻擊提供了防護支持。如果你的Web框架選擇的是Spring,那么在安全方面Spring Security會是一個不錯的選擇。
一、開啟Spring Security
1、導入依賴
創建一個Spring Boot項目springboot-springsecurity,然后引入spring-boot-starter-security:
<!-- Spring Security的maven依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、創建Controller
新建包com.goldwind.controller,接下來我們創建一個TestController,對外提供一個/hello服務:
package com.goldwind.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: zy * @Description: 測試 * @Date: 2020-2-9 */ @RestController public class TestController { @GetMapping("hello") public String hello() { return "hello spring security"; } }
3、新建App入口
新建入口程序App.java:
package com.goldwind; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @Author: zy * @Description: 啟動程序 * @Date: 2020-2-9 */ @SpringBootApplication public class App { public static void main(String[] args){ SpringApplication.run(App.class,args); } }
這時候我們直接啟動項目,訪問http://localhost:8080/hello,可看到頁面彈出了個formLogin認證框:
這個配置開啟了一個form Login類型的認證,所有服務的訪問都必須先過這個認證,默認的用戶名為user,密碼由Sping Security自動生成,回到IDE的控制台,可以找到密碼信息:
Using generated security password: a77c9456-901e-4848-a221-3822347e52ea
輸入用戶名user,密碼a77c9456-901e-4848-a221-3822347e52ea后,我們便可以成功訪問/hello
接口。
二、基於HTTP basic類型的認證
我們可以通過一些配置將表單認證修改為基於HTTP Basic的認證方式。
1、配置Spring Security
創建包com.goldwind.config,創建一個配置類WebSecurityConfig繼承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter這個抽象類並重寫configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web應用安全配置的適配器:
package com.goldwind.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @Author: zy * @Description: spring security配置類 * @Date: 2020-2-9 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 配置攔截請求資源 * @param http:HTTP請求安全處理 * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() //HTTP Basic認證方式
.and() .authorizeRequests() // 授權配置
.anyRequest() // 所有請求
.authenticated(); // 都需要認證
} }
Spring Security提供了這種鏈式的方法調用。上面配置指定了認證方式為HTTP Basic登錄,並且所有請求都需要進行認證。
這里有一點需要注意,我沒並沒有在Spring Security配置類上使用@EnableWebSecurity注解。這是因為在非Spring Boot的Spring Web MVC應用中,注解@EnableWebSecurity需要開發人員自己引入以啟用Web安全。而在基於Spring Boot的Spring Web MVC應用中,開發人員沒有必要再次引用該注解,Spring Boot的自動配置機制WebSecurityEnablerConfiguration已經引入了該注解,如下所示:
package org.springframework.boot.autoconfigure.security.servlet; // 省略 imports 行
@Configuration // 僅在存在 WebSecurityConfigurerAdapter bean 時該注解才有可能生效 // (最終生效與否要結合其他條件綜合考慮)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class) // 僅在不存在 springSecurityFilterChain 時該注解才有可能生效 // (最終生效與否要結合其他條件綜合考慮)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) // 僅在 Servlet 環境下該注解才有可能生效 // (最終生效與否要結合其他條件綜合考慮)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @EnableWebSecurity // <====== 這里啟用了 Web 安全
public class WebSecurityEnablerConfiguration { }
實際上,一個Spring Web應用中,WebSecurityConfigurerAdapter可能有多個 , @EnableWebSecurity可以不用在任何一個WebSecurityConfigurerAdapter上,可以用在每個WebSecurityConfigurerAdapter上,也可以只用在某一個WebSecurityConfigurerAdapter上。多處使用@EnableWebSecurity注解並不會導致問題,其最終運行時效果跟使用@EnableWebSecurity一次效果是一樣的。
這時候我們重啟項目,再次訪問http://localhost:8080/hello,可以看到認證方式已經是HTTP Basic的方式了:
用戶名依舊是user,密碼由Spring Security自動生成,如果需要換回表單的認證方式,我們只需要簡單修改configure方法中的配置:
@Override protected void configure(HttpSecurity http) throws Exception { // http.formLogin() // 表單方式
http.httpBasic() // HTTP Basic方式
.and() .authorizeRequests() // 授權配置
.anyRequest() // 所有請求
.authenticated(); // 都需要認證
}
三、Spring Security基本原理
上面我們開啟了一個最簡單的Spring Security安全配置,下面我們來了解下Spring Security的基本原理。通過上面的的配置。
1、基本原理
Spring Security所解決的問題就是安全訪問控制,而安全訪問控制功能其實就是對所有進入系統的請求進行攔截, 校驗每個請求是否能夠訪問它所期望的資源。而Spring Security對Web資源的保護是靠Filter實現的。當初始化Spring Security時,WebSecurityConfiguration會創建一個名為 springSecurityFilterChain 的Servlet過濾器,類型為org.springframework.security.web.FilterChainProxy,它實現了javax.servlet.Filter,因此外部的請求會經過此類。
FilterChainProxy是一個代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各個Filter,下圖為實際調試中創建的FilterChainProxy實例。
這些Filter作為Bean被Spring管理,它們是Spring Security核心,各有各的職責,但他們並不直接處理用戶的認 證,也不直接處理用戶的授權,而是把它們交給了認證管理器(AuthenticationManager)和決策管理器 (AccessDecisionManager)進行處理。 Spring Security功能的實現主要是由一系列過濾器鏈相互配合完成,如下圖(只是挑選了一些重要的Filter進行講解):
下面介紹幾個重要的過濾器:
- UsernamePasswordAuthenticationFilter過濾器用於處理基於表單方式的登錄驗證,該過濾器默認只有當請求方法為post、請求頁面為/login時過濾器才生效,如果想修改默認攔截url,只需在剛才介紹的Spring Security配置類WebSecurityConfig中配置該過濾器的攔截url:.loginProcessingUrl("url")即可;
- BasicAuthenticationFilter用於處理基於HTTP Basic方式的登錄驗證,當通過HTTP Basic方式登錄時,默認會發送post請求/login,並且在請求頭攜帶Authorization:Basic dXNlcjoxOWEyYWIzOC1kMjBiLTQ0MTQtOTNlOC03OThkNjc2ZTZlZDM=信息,該信息是登錄用戶名、密碼加密后的信息,然后由BasicAuthenticationFilter過濾器解析后,構建UsernamePasswordAuthenticationFilter過濾器進行認證;如果請求頭沒有Authorization信息,BasicAuthenticationFilter過濾器則直接放行;
- FilterSecurityInterceptor的攔截器,用於判斷當前請求身份認證是否成功,是否有相應的權限,當身份認證失敗或者權限不足的時候便會拋出相應的異常;
- ExceptionTranslateFilter捕獲並處理,所以我們在ExceptionTranslateFilter過濾器用於處理了FilterSecurityInterceptor拋出的異常並進行處理,比如需要身份認證時將請求重定向到相應的認證頁面,當認證失敗或者權限不足時返回相應的提示信息;
2、認證流程
以表單方式登錄驗證為例,認證流程如下:
- 用戶提交用戶名、密碼被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 過濾器獲取到, 封裝為請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。
- 然后過濾器將Authentication提交至認證管理器(AuthenticationManager)進行認證 。
- 認證成功后, AuthenticationManager 身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息, 身份信息,細節信息,但密碼通常會被移除) Authentication 實例。
- SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,通過 SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中。 可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它的實現類為ProviderManager。而Spring Security支持多種認證方式,因此ProviderManager維護着一個 List 列表,存放多種認證方式,最終實際的認證工作是由 AuthenticationProvider完成的。其中web表單的對應的AuthenticationProvider實現類為 DaoAuthenticationProvider,它的內部又維護着一個UserDetailsService負責UserDetails的獲取。最終 AuthenticationProvider將UserDetails填充至Authentication。
3、授權策略
Spring Security可以通過 http.authorizeRequests() 對web請求進行授權保護。Spring Security使用標准Filter建立了對web請求的攔截,最終實現對資源的授權訪問。授權流程如下:
四、Spring Security filter的構造和初始化
我們已經知道Spring Security通過構造層層filter來實現登錄跳轉、權限驗證,角色管理等功能。這里我們將通過剖析Spring Security的核心源碼來說明Spring Security的filter是如何開始構造的,下面的講解比較長,如果你不是特別感興趣,可以直接跳到總結,總結粗略了敘述了Spring Security框架filters的構建過程。我們可以下載Spring Security源碼;
1、@EnableWebSecurity
我們知道要想啟動Spring Security,必須配置注解@EnableWebSecurity,我們就從該注解說起:
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) @Target(value = { java.lang.annotation.ElementType.TYPE }) @Documented @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class }) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { /** * Controls debugging support for Spring Security. Default is false. * @return if true, enables debug support with Spring Security */
boolean debug() default false; }
2、WebSecurityConfiguration類
我們可以看到該注解導入了WebSecurityConfiguration類,進入該類查看:

/* * 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.web.configuration; 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.BeanFactoryPostProcessor; 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.config.crypto.RsaKeyConversionServicePostProcessor; 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; /** * Uses a {@link WebSecurity} to create the {@link FilterChainProxy} that performs the web * based security for Spring Security. It then exports the necessary beans. Customizations * can be made to {@link WebSecurity} by extending {@link WebSecurityConfigurerAdapter} * and exposing it as a {@link Configuration} or implementing * {@link WebSecurityConfigurer} and exposing it as a {@link Configuration}. This * configuration is imported when using {@link EnableWebSecurity}. * * @see EnableWebSecurity * @see WebSecurity * * @author Rob Winch * @author Keesun Baik * @since 3.2 */ @Configuration(proxyBeanMethods = false) public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { private WebSecurity webSecurity; private Boolean debugEnabled; private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers; private ClassLoader beanClassLoader; @Autowired(required = false) private ObjectPostProcessor<Object> objectObjectPostProcessor; @Bean public static DelegatingApplicationListener delegatingApplicationListener() { return new DelegatingApplicationListener(); } @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() { return webSecurity.getExpressionHandler(); } /** * Creates the Spring Security Filter Chain * @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(); } /** * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP * tag support. * @return the {@link WebInvocationPrivilegeEvaluator} */ @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public WebInvocationPrivilegeEvaluator privilegeEvaluator() { return webSecurity.getPrivilegeEvaluator(); } /** * Sets the {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} * instances used to create the web configuration. * * @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); } webSecurityConfigurers.sort(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 public static BeanFactoryPostProcessor conversionServicePostProcessor() { return new RsaKeyConversionServicePostProcessor(); } @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; } } /* * (non-Javadoc) * * @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; } }
如果我們忽略掉細節,只看最重要的步驟,該類主要實現了如下功能:
WebSecurityConfiguration類是作為一個Spring配置源,同時定義了許多bean,這里重點看WebSecurityConfiguration#setFilterChainProxySecurityConfigurer這個方法:
/** * Sets the {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} * instances used to create the web configuration. * * @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); } webSecurityConfigurers.sort(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; }
下面我們對每一個步驟來做相應的源代碼解釋,首先我們來看一下方法得第二個參數:
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}"
可以看一下autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的源代碼:
public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() { List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<SecurityConfigurer<Filter, WebSecurity>>(); Map<String, WebSecurityConfigurer> beansOfType = beanFactory .getBeansOfType(WebSecurityConfigurer.class); for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) { webSecurityConfigurers.add(entry.getValue()); } return webSecurityConfigurers; }
我們如果調試代碼,可以發現beanFactory.getBeansOfType從Spring容器獲取類型為WebSecurityConfigurer的bean,在這里也就是獲取到我們編寫的WebSecurityConfig配置類:
我們可以看一下WebSecurityConfig類的類圖:
webSecurity = objectPostProcessor .postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); }
當有多個配置項時進行排序,進行order重復驗證:
webSecurityConfigurers.sort(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; }
遍歷WebSecurityConfiguration#webSecurityConfigurers集合,調用webSecurity的apply方法,此時也會將我們自定義的WebSecurityConfig應用到 webSecurity.apply方法上:
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); }
最后,初始化WebSecurityConfiguration#webSecurityConfigurers屬性:
this.webSecurityConfigurers = webSecurityConfigurers;
3、WebSecurity類
到這里我們知道了WebSecurityConfiguration類調用上述方法將我們配置的WebSecurityConfig類用WebSecurity類的apply方法關聯起來,那么我們詳細看看WebSecurity類的apply方法:
public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception { add(configurer); return configurer; }
private <C extends SecurityConfigurer<O, B>> void add(C configurer) { Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer .getClass(); synchronized (configurers) { if (buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers .get(clazz) : null; if (configs == null) { configs = new ArrayList<>(1); } configs.add(configurer); this.configurers.put(clazz, configs); if (buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } }
從上述代碼可知,實際上就是將我們定義的WebSecurityConfig配置類放入了WebSecurity類的一個LinkedHashMap中:
該LinkedHashMap在WebSecurity中屬性名為configurers:
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();
可以看到鍵就是我們定義的配置類的Clazz類型,而值是List<SecurityConfigurer<Filter, WebSecurity>>類型,是一個list集合,其中只有一個元素,就是我們編寫的WebSecurityConfig配置類;
我們繼續回到WebSecurityConfiguration類,查看它的另外一個重要的方法:
build的方法來自於WebSecurity的父類AbstractSecurityBuilder,該方法即為Spring Security構建Filter的核心方法,通過webSecurity的build方法構建了Spring Security的Filter:
實際上調用了父類AbstractConfiguredSecurityBuilder的doBuild:
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; } }
這里主要看AbstractConfiguredSecurityBuilder的init方法和WebSecurity實現的performBuild方法,首先看init方法,init方法將會遍歷WebSecurity的LinkHashMap configurers中每個元素configurer,執行以下步驟:
調用configurer的init方法,init該方法來自父類WebSecurityConfigurerAdapter(這里的this指定就是WebSecurity):
init方法中又會調用WebSecurityConfigurerAdapter#getHttp方法:
getHttp方法首先創建一個HttpSecurity對象,用來初始化configurer的http成員:
private HttpSecurity http;
又調用 configure 方法,最終將會執行我們在WebSecurityConfig寫的configure方法:
configurer中的init方法執行完之后,WebSecurity調用addSecurityFilterChainBuilder方法將configurer創建的HttpSecurity放入了WebSecurity的一個list集合里,該list集合屬性名為securityFilterChainBuilders:
public WebSecurity addSecurityFilterChainBuilder(SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) { this.securityFilterChainBuilders.add(securityFilterChainBuilder); return this; }
到目前為止,我們終於知道我們編寫的WebSecurityConfig類的configure方法是如何被調用的了,但是仍有許多疑問沒解開,比如HttpSecurity類的作用,Spring Security是如何通過HttpSecurity類構建一條攔截器鏈等。
這里我們先不分析HttpSecurity類的具體實現,再來看看WebSecurity的init方法執行后所執行的performBuild方法,該方法源碼如下:
@Override protected Filter performBuild() throws Exception { Assert.state( !securityFilterChainBuilders.isEmpty(), () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"); int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); List<SecurityFilterChain> securityFilterChains = new ArrayList<>( chainSize); for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; if (debugEnabled) { logger.warn("\n\n"
+ "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n"); result = new DebugFilter(filterChainProxy); } postBuildAction.run(); return result; }
該方法執行的操作主要如下:
(1)、遍歷WebSecurity的securityFilterChainBuilders列表,一般也就一個元素,也就是我們的WebSecurityConfig配置類創建的HttpSecurity對象,並執行該對象的build方法,初始化成員屬性filters,並通過filters集合構建SecurityFilterChain類;
然后將每個HttpSecurity對象構建的SecurityFilterChain對象添加到securityFilterChains列表中。
(2)、將List<SecurityFilterChain>集合構建成一個FilterChainProxy代理類,返回這個FilterChainProxy代理類;
到這里總的過程就非常明了,WebSecurityConfiguration的springSecurityFilterChain方法最終返回了一個FilterChainProxy代理類,作為Spring Security的頂層filter,而HttpSecurity主要用於注冊和實例化各種filter,HttpSecurity類有個屬性名為filters的List列表專門用於保存過濾器的。
到這里就分成了兩路:
- 一路是HttpSecurity的build方法構建SecurityFilterChain類的原理;
- 一路是FilterChainProxy類的作用;
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
我們先從FilterChainProxy類開始。
4、FilterChainProxy類
當請求到達的時候,FilterChainProxy會調用dofilter方法,會遍歷所有的SecurityFilterChain,對匹配到的url,則調用SecurityFilterChain中的每一個filter做認證授權。FilterChainProxy的dofilter()中調用了doFilterInternal方法,如下:
private List<SecurityFilterChain> filterChains; @Override 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); doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else { doFilterInternal(request, response, chain); } } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 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); }
這里我就不貼VirtualFilterChain的源碼了,實際上就是先去遍歷執行我們filters中的過濾器的doFilter,最后再去執行chiin.doFilter。
我們理清了FilterChainProxy類的作用,那么這些SecurityFilterChain是從哪來的呢?從上節可知SecurityFilterChain是由HttpSecurity的build方法生成的,下面我們分析下HttpSecurity類。
5、HttpSecurity類
HttpSecurity與WebSecurity一樣,都繼承了AbstractConfiguredSecurityBuilder類,而WebSecurity的build和doBuild方法和LinkedHashMap屬性,均來自AbstractConfiguredSecurityBuilder,故HttpSecurity的build方法代碼與WebSecurity的相同,區別在於LinkedHashMap存儲的東西不同:
(1)、WebSecurityConfig類的configure方法
在之前我們已經介紹了WebSecurity的doBuild是如何調用我們自己寫的配置類WebSecurityConfig的configure方法;在該方法中http所調用的方法,最終的結果就是產生url-filter的關系映射:
protected void configure(HttpSecurity http) throws Exception { http.formLogin() //HTTP Basic認證方式
.and() .authorizeRequests() // 授權配置
.anyRequest() // 所有請求
.authenticated(); // 都需要認證
}
比如authorizeRequests(),formLogin()方法分別返回ExpressionUrlAuthorizationConfigurer和FormLoginConfigurer,他們都是SecurityConfigurer接口的實現類,分別代表的是不同類型的安全配置器。而這些安全配置器分別對應一個或多個filter:
- formLogin對應UsernamePasswordAuthenticationFilter,此外我們還可以給安全過濾器FormLoginConfigurer指定其它參數,比如.login("/login"):自定義登錄請求頁面;.loginProcessingUrl("/login")指定UsernamePasswordAuthenticationFilter攔截的Form action;
.and() .formLogin() // 或者httpBasic()
.loginPage("/login") // 指定登錄頁的路徑
.loginProcessingUrl("/login") // 指定自定義form表單提交請求的路徑
- authorizeRequests對應FilterSecurityInterceptor;
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { ApplicationContext context = getContext(); return getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context)) .getRegistry(); }
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }
都調用了getOrApply方法,再來看getOrApply方法,又調用了其中的apply方法:
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply( C configurer) throws Exception { C existingConfig = (C) getConfigurer(configurer.getClass()); if (existingConfig != null) { return existingConfig; } return apply(configurer); }
apply方法又調用了add方法,這里的add方法最終還是將該configurer加入了LinkedHashMap中:
private <C extends SecurityConfigurer<O, B>> void add(C configurer) { Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer .getClass(); synchronized (configurers) { if (buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers .get(clazz) : null; if (configs == null) { configs = new ArrayList<>(1); } configs.add(configurer); this.configurers.put(clazz, configs); if (buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } }
故HttpSecurity在構建filter的過程中,本質還是將形如ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer等類加入了它的LinkedHashMap中,該list集合屬性名為configurers :
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();
這里有一點需要注意,
- 在HttpSecurity的configurers列表中存放的元素都是繼承自SecurityConfigurerAdapter類,Spring Security框架提供的不同類型的安全配置器主要有以下這些;
- 而在WebSecurity的configurers列表中存放的元素都是繼承自WebSecurityConfigurerAdapter類,也就是我們寫的配置類:
啥時候我們會使用到SecurityConfigurerAdapter類,在后面我們介紹手機驗證碼登錄的時候會有一個案例:
如果想使得這個配置生效,我們只需要在WebSecurityConfig配置類的configure方法添加如下代碼:
.apply(smsAuthenticationConfig); // 將短信驗證碼認證配置加到 Spring Security 中 添加一個安全配置其到http的configurers集合
調用http的apply方法,最終將該smsAuthenticationConfig加入了HttpSecurity的configurers列表中。
(2)、HttpSecurity的build方法構建SecurityFilterChain類的原理
那么將這些Configurer類存入LinkedHashMap的作用又是什么?
在前面我們已經說到通過調用HttpSecurity的build方法構建SecurityFilterChain類,而build方法封裝了doBuild方法;
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); }
在我們回憶上面WebSecurity類的doBuild方法,我們知道HttpSecurity類調用的doBuild方法與WebSecurity類一樣,而通過觀察WebSecurity類doBuild方法里init;configure;這些語句的具體實現,實際就是遍歷LinkedHashMap中的元素:
並調用其init方法和configure方法:
@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; } }
我們現在來查看其中一個ExpressionUrlAuthorizationConfigurer類的configure方法的詳細代碼:
@Override public void configure(H http) throws Exception { FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http); if (metadataSource == null) { return; } FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor( http, metadataSource, http.getSharedObject(AuthenticationManager.class)); if (filterSecurityInterceptorOncePerRequest != null) { securityInterceptor .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest); } securityInterceptor = postProcess(securityInterceptor); http.addFilter(securityInterceptor); http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); }
最后來看看HttpSecruity的performBuild()方法:
@Override protected DefaultSecurityFilterChain performBuild() { filters.sort(comparator); return new DefaultSecurityFilterChain(requestMatcher, filters); }
實際上就是通過HttpSecurity的filters集合構建了SecurityFilterChain。
從上面代碼可總結出,HttpSecurity內部維護一個filter列表,而HttpSecurity調用形如authorizeRequests(),formLogin()等方法實際上就是將各種filter添加入它的列表當中,最后通過performBuild()方法構建出SecurityFilterChain,至此HttpSecurity構建filter的總過程就完成了。
6、核心接口SecurityBuilder 與 SecurityConfigurer
上面我們提到的WebSecurity,HttpSecurity都是具體的類。現在,我們從更高的層面來說,從兩個核心接口以及其實現類的類圖來理解下。

public interface SecurityBuilder<O> { /** * Builds the object and returns it or null. * * @return the Object to be built or null if the implementation allows it. * @throws Exception if an error occurred when building the Object */ O build() throws Exception; }

public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> { /** * Initialize the {@link SecurityBuilder}. Here only shared state should be created * and modified, but not properties on the {@link SecurityBuilder} used for building * the object. This ensures that the {@link #configure(SecurityBuilder)} method uses * the correct shared objects when building. Configurers should be applied here. * * @param builder * @throws Exception */
void init(B builder) throws Exception; /** * Configure the {@link SecurityBuilder} by setting the necessary properties on the * {@link SecurityBuilder}. * * @param builder * @throws Exception */
void configure(B builder) throws Exception; }
- HttpSecurity是接口SecurityBuilder的實現類,HttpSecuirty內部維護了一個Filter的List集合,我們添加的各種安全配置器對應的Filter最終都會被加入到這個List集合中;
- WebSecurity也是接口securityBuilder的實現類,內部維護着SecurityBuilder的列表,存儲SecurityBuilder,這里主要是存儲HttpSecurity;
很多官方類是XXXConfigurer,這些都是SecurityConfigurer。這些SecurityConfigurer的configure()方法,都會把對應filter添加到HttpSecurity。
7、總結
(1) WebSecurityConfiguration配置類有兩個重要成員屬性,調用setFilterChainProxySecurityConfigurer方法初始化webSecurity、使用我們編寫的配置類WebSecurityConfig初始化webSecurityConfigurers。
private WebSecurity webSecurity; private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
需要注意的是:webSecurityConfigurers的程序都是繼承自WebSecurityConfigurerAdapter類。
(2)、WebSecurity 有兩三個重要成員,然后使用WebSecurityConfiguration#webSecurityConfigurers初始化webSecurity#configurers成員;
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>(); private final List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<>();
(3) 調用WebSecurityConfiguration#springSecurityFilterChain生成FilterChainProxy,主要是通過webSecurity.build()方法實現構建,該方法主要包含兩步:
a、執行父類AbstractConfiguredSecurityBuilder#init方法:
- 遍歷webSecurity#configurers成員,執行configurer的init方法;
- init方法調用getHttp方法,去初始化configurer的http成員(HttpSecurity類型),與此同時執行我們configure(http)方法,就是去調用我們配置類WebSecurityConfig中的configure方法;、
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //HTTP Basic認證方式
.and() .authorizeRequests() // 授權配置
.anyRequest() // 所有請求
.authenticated(); // 都需要認證
} }
- 比如authorizeRequests(),formLogin()方法分別返回ExpressionUrlAuthorizationConfigurer和FormLoginConfigurer,最終會將不同類型的安全配置器添加到http的configurers,這些configure都是繼承自SecurityConfigurerAdapter類。HttpSecurity也是有兩個非常重要的成員:
private List<Filter> filters = new ArrayList<>(); private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();
- 然后調用webSecurity#addSecurityFilterChainBuilder方法將webSecurity#configurers中每個configurer創建的HttpSecurity放入了webSecurity的securityFilterChainBuilders集合里;
b、執行webSecurity#performBuild
- 遍歷webSecurity的securityFilterChainBuilders列表,一般也就一個元素,也就是我們的WebSecurityConfig配置類創建的http對象,並執行http對象的build方法,生成SecurityFilterChain對象,該方法主要包含兩步:
- 執行父類AbstractConfiguredSecurityBuilder#init方法:
- 遍歷http#configurers成員,執行configurer的init方法,只是在init過程中不會創建http對象,而是把http對象傳進去,以ExpressionUrlAuthorizationConfigurer為例,它會向http的filters屬性添加securityInterceptor;
@Override public void configure(H http) throws Exception { FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http); if (metadataSource == null) { return; } FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor( http, metadataSource, http.getSharedObject(AuthenticationManager.class)); if (filterSecurityInterceptorOncePerRequest != null) { securityInterceptor .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest); } securityInterceptor = postProcess(securityInterceptor); http.addFilter(securityInterceptor); http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); }
- 執行http#performBuild方法,實際上就是通過HttpSecurity的filters集合構建了SecurityFilterChain;
- 然后將每個http對象構建的SecurityFilterChain對象添加到List<SecurityFilterChain> securityFilterChains臨時列表中:
- 將securityFilterChains集合構建成一個FilterChainProxy代理類,返回這個FilterChainProxy代理類;
(4) 當請求到達的時候,FilterChainProxy會調用dofilter方法,會遍歷所有的SecurityFilterChain,對匹配到的url,則調用SecurityFilterChain中的每一個filter做認證授權。
最后放一張概括圖,有興趣的朋友可以繪制出具體的時序圖,這里我就不繪制了:
五、登錄、驗證流程分析
我們已經明白了Spring Security的filter的構造。下面我們來介紹一下filter的執行順序(登錄方式改回表單的方式)。
當我們啟動項目訪問http://localhost:8080/hello時:
1、請求/hello接口
由於BasicAuthenticationFilter、UsernamePasswordAuthenticationFilter默認只攔截/login post請求,因此過濾器會直接放行,代碼直接跳轉到FilterSecurityInteceptor上進行權限校驗;
2、權限驗證
此時會調用父類的beforeInvocation進行權限校驗:
protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Security invocation attempted for object "
+ object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass()); } Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { if (rejectPublicInvocations) { throw new IllegalArgumentException( "Secure object invocation "
+ object + " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation
} if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage( "AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = authenticateIfRequired(); // Attempt authorization
try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object); } }
this.obtainSecurityMetadataSource()會調用默認的權限資源管理器,由於我們配置了任何請求都需要經過授權:
.anyRequest() // 所有請求
.authenticated(); // 都需要認證
因此/hello需要的權限為authenticated,此處我們可以通過實現FilterInvocationSecurityMetadataSource接口配置自己權限資源管理器,通過查詢數據庫來實現權限的動態配置,感興趣的可以閱讀:SpringSecurity動態配置權限。
由於用戶沒有登錄,經過AnonymousAuthenticationFilter匿名過濾器處理之后,我們從上下文中可以獲取到用戶的主體信息為:
此時匿名用戶具有的權限為ROLE_ANONYMOUS;然后經過訪問決策管理器判斷用戶有無權限:
this.accessDecisionManager.decide(authenticated, object, attributes);
Spring提供了3個決策管理器:
- AffirmativeBased 一票通過,只要有一個投票器通過就允許訪問;
- ConsensusBased 有一半以上投票器通過才允許訪問資源;
- UnanimousBased 所有投票器都通過才允許訪問;
這里使用默認的決策管理進行判斷有無權限,可以看到決策管理中在引入了投票器(AccessDecisionVoter)的概念,有無權限訪問的最終覺得權是由投票器來決定的,這里權限無法通過:
會拋出權限拒絕的異常,該異常會被ExceptionTranslateFilter捕獲;
3、跳轉到登陸頁面
由於用戶未登錄直接訪問/hello,所以拋出用戶未認證的異常,所以接下來跳轉到Spring Security提供的默認登錄頁 GET:http://localhost:8080/login,最終請求被DefaultLoginPageGeneratingFilter攔截進行處理,返回默認的登錄頁面:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //登錄失敗
boolean loginError = isErrorPage(request); //登出請求
boolean logoutSuccess = isLogoutSuccess(request); //是否是登錄請求
if (isLoginUrlRequest(request) || loginError || logoutSuccess) { String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.length()); response.getWriter().write(loginPageHtml); return; } chain.doFilter(request, response); }
isLoginUrlRequest(request)方法中,如果判斷是/login請求則接下來生成默認的登錄頁面返回:
private boolean isLoginUrlRequest(HttpServletRequest request) { return matches(request, loginPageUrl); } private boolean matches(HttpServletRequest request, String url) { //首先判斷是不是GET請求
if (!"GET".equals(request.getMethod()) || url == null) { return false; } String uri = request.getRequestURI(); int pathParamIndex = uri.indexOf(';'); if (pathParamIndex > 0) { // strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex); } if (request.getQueryString() != null) { uri += "?" + request.getQueryString(); } if ("".equals(request.getContextPath())) { return uri.equals(url); } return uri.equals(request.getContextPath() + url); }
generateLoginPageHtml方法中生成默認登錄頁面:
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { String errorMsg = "none"; if (loginError) { HttpSession session = request.getSession(false); if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); errorMsg = ex != null ? ex.getMessage() : "none"; } } StringBuilder sb = new StringBuilder(); sb.append("<html><head><title>Login Page</title></head>"); if (formLoginEnabled) { sb.append("<body οnlοad='document.f.").append(usernameParameter) .append(".focus();'>\n"); } if (loginError) { sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: "); sb.append(errorMsg); sb.append("</font></p>"); } if (logoutSuccess) { sb.append("<p><font color='green'>You have been logged out</font></p>"); } if (formLoginEnabled) { sb.append("<h3>Login with Username and Password</h3>"); sb.append("<form name='f' action='").append(request.getContextPath()) .append(authenticationUrl).append("' method='POST'>\n"); sb.append("<table>\n"); sb.append(" <tr><td>User:</td><td><input type='text' name='"); sb.append(usernameParameter).append("' value='").append("'></td></tr>\n"); sb.append(" <tr><td>Password:</td><td><input type='password' name='") .append(passwordParameter).append("'/></td></tr>\n"); if (rememberMeParameter != null) { sb.append(" <tr><td><input type='checkbox' name='") .append(rememberMeParameter) .append("'/></td><td>Remember me on this computer.</td></tr>\n"); } sb.append(" <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n"); renderHiddenInputs(sb, request); sb.append("</table>\n"); sb.append("</form>"); } if (openIdEnabled) { sb.append("<h3>Login with OpenID Identity</h3>"); sb.append("<form name='oidf' action='").append(request.getContextPath()) .append(openIDauthenticationUrl).append("' method='POST'>\n"); sb.append("<table>\n"); sb.append(" <tr><td>Identity:</td><td><input type='text' size='30' name='"); sb.append(openIDusernameParameter).append("'/></td></tr>\n"); if (openIDrememberMeParameter != null) { sb.append(" <tr><td><input type='checkbox' name='") .append(openIDrememberMeParameter) .append("'></td><td>Remember me on this computer.</td></tr>\n"); } sb.append(" <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n"); sb.append("</table>\n"); renderHiddenInputs(sb, request); sb.append("</form>"); } sb.append("</body></html>"); return sb.toString(); }
4、開始登錄
當我們輸入完用戶名、密碼點擊登錄時,將會發送post請求:http://localhost:8080/login,該請求頁面是在form標簽action中指定的,POST請求會提交用戶名和密碼登錄信息,此時由UsernamePasswordAuthenticationFilter的父類AbstractAuthenticationProcessingFilter處理,簡單來說就是從請求request中獲取用戶名和密碼進行認證操作(這里有一點需要注意,默認情況下UsernamePasswordAuthenticationFilter該過濾器默認只有當請求方法為post、請求頁面為/login時過濾器才生效,如果想修改其默認攔截頁面,需要在BrowserSecurityConfig中配置該過濾器的攔截url:.loginProcessingUrl("url"),也就是說只有當form標簽action指定的url和.loginProcessingUrl配置的相同時,UsernamePasswordAuthenticationFilter過濾器才生效):
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //當不是登錄請求 POST:/login時則直接跳過
if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } //如果是登錄請求 POST:/login則進行驗證處理
if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { //從請求中獲取用戶名密碼進行校驗
authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication
return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); //校驗失敗則跳轉到登錄頁面
unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed //校驗失敗則跳轉到登錄頁面
unsuccessfulAuthentication(request, response, failed); return; } // Authentication success
if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //校驗成功則跳轉到成功之后頁面
successfulAuthentication(request, response, chain, authResult); }
在doFilter方法中,首先會調用requiresAuthentication方法判斷是不是登錄請求,如果不是則直接跳過這個Filter,如果是則進行身份驗證:
如果請求是登錄操作,則接下來進行身份驗證相關的操作,
UsernamePasswordAuthenticationFilter處理表單方式的用戶認證。在UsernamePasswordAuthenticationFilter的attemptAuthentication方法上打個斷點: 調用authResult = attemptAuthentication(request, response)方法進行驗證,在attemptAuthentication中會從請求中獲取用戶名和密碼構建UsernamePasswordAuthenticationToken :
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //獲取用戶名密碼
String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property
setDetails(request, authRequest); //調用AuthenticationManager的實現類進行校驗
return this.getAuthenticationManager().authenticate(authRequest); }
5、認證管理器(AuthenticationManager)進行認證
在接口AuthenticationManager的實現類ProviderManager調用authenticate方法進行校驗操作。在authenticate方法中提供了一個List<AuthenticationProvider>,開發者可以提供不同的校驗方式(用戶名密碼、手機號密碼、郵箱密碼等)只要其中有一個AutenticationProvider調用authenticate方法校驗通過即可,當校驗不通過時會拋出AuthenticationException ,當所有的AuthenticationProvider校驗不通過時,直接拋出異常由ExceptionTranslationFilter捕捉處理,跳轉到登錄頁面。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); //可以提供多個驗證器,只要其中有一個校驗通過接口
for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using "
+ provider.getClass().getName()); } try { //進行校驗,校驗不通過則直接拋出AuthenticationException
result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status
throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try.
try { result = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request
} catch (AuthenticationException e) { lastException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication
((CredentialsContainer) result).eraseCredentials(); } eventPublisher.publishAuthenticationSuccess(result); return result; } // Parent was null, or didn't authenticate (or throw an exception). //最終校驗不通過則拋出異常,由ExceptionTranslationFilter捕捉處理
if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; }
在接口AuthenticationProvider的實現類AbstractUserDetailsAuthenticationProvider中調用authenticate進行用戶名密碼等的校驗
- 首先從緩存userCahce中獲取,如果獲取不到,則從子類DaoAuthenticationProvider的retirveUser方法中獲取,該方法利用loadUserByUsername函數根據用戶名從數據庫獲得用戶詳情,如果獲取不到則直接拋出異常;
- 將獲取的user和authentication(從瀏覽器輸入的用戶和密碼,用戶名和密碼會傳遞到Spring Security內部,最終封裝成UsernamePasswordAuthenticationToken對象authentication)進行匹配,匹配成功調用createSuccessAuthentication方法創建Authentication返回。;
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { preAuthenticationChecks.check(user); //判斷用戶名和密碼等信息是否完全匹配
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache)
cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
在子類DaoAuthenticationProvider中調用接口的UserDetailsService的loadUserByUsername方法根據用戶名來查找用戶信息(在正式項目中實現此接口來完成從數據庫等中查找),如果查找不到還是直接拋出異常,由上層去處理:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { //調用默認實現InMemoryUserDetailsManager查找用戶名密碼
loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); } throw notFound; } catch (Exception repositoryProblem) { throw new InternalAuthenticationServiceException( repositoryProblem.getMessage(), repositoryProblem); } if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }
當找到用戶信息后,還需要根據用戶信息和password字段進行匹配,在additionalAuthenticationChecks中完成完整的用戶名和密碼認證:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
6、權限校驗
當認證通過時,進行權限校驗,執行FilterSecurityInterceptor代碼的doFilter方法:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); }
在invoke方法中會調用父類AbstractSecurityInterceptor的beforeInvocation對Authentication對象進行權限校驗:
public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking
if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //調用父類的beforeInvocation進行權限校驗
InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
在beforeInvocation中調用接口AccessDecisionManager的實現類,校驗角色:
protected InterceptorStatusToken beforeInvocation(Object object) { //省略部分代碼 //獲取當前線程的Authentication對象
Authentication authenticated = authenticateIfRequired(); // Attempt authorization
try { //權限角色校驗
this.accessDecisionManager.decide(authenticated, object, attributes); } //校驗失敗拋出異常
catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } //省略部分代碼
}
最終調用子類AffirmativeBased的decide方法,在decide方法中會獲取AccessDecisionVoter對權限進行投票處理,獲取投票結果,當投票結果是1時則表示有權限,否則等於-1表示沒有權限,拒絕:
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { //投票獲取結果
int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } //當有權限時直接返回
switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; //否則拒絕
case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } // if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions(); }
在實現類WebExpressionVoter會根據authentication結果進行校驗判斷,根據Authentication對象創建接口SecurityExpressionOperations的實現類SecurityExpressionRoot:
- 當請求url被配置為permitAll,則直接返回true,校驗通過;
- 其他需要登錄請求url會調用isAuthenticated方法,最終調用AuthenticationTrustResolver的AuthenticationTrustResolverImpl的方法進行判斷;
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { protected final Authentication authentication; private AuthenticationTrustResolver trustResolver; private RoleHierarchy roleHierarchy; private Set<String> roles; private String defaultRolePrefix = "ROLE_"; /** Allows "permitAll" expression */
public final boolean permitAll = true; /** Allows "denyAll" expression */
public final boolean denyAll = false; private PermissionEvaluator permissionEvaluator; public final String read = "read"; public final String write = "write"; public final String create = "create"; public final String delete = "delete"; public final String admin = "administration"; /** * Creates a new instance * @param authentication the {@link Authentication} to use. Cannot be null. */
public SecurityExpressionRoot(Authentication authentication) { if (authentication == null) { throw new IllegalArgumentException("Authentication object cannot be null"); } this.authentication = authentication; } public final boolean hasAuthority(String authority) { return hasAnyAuthority(authority); } public final boolean hasAnyAuthority(String... authorities) { return hasAnyAuthorityName(null, authorities); } public final boolean hasRole(String role) { return hasAnyRole(role); } public final boolean hasAnyRole(String... roles) { return hasAnyAuthorityName(defaultRolePrefix, roles); } private boolean hasAnyAuthorityName(String prefix, String... roles) { Set<String> roleSet = getAuthoritySet(); for (String role : roles) { String defaultedRole = getRoleWithDefaultPrefix(prefix, role); if (roleSet.contains(defaultedRole)) { return true; } } return false; } public final Authentication getAuthentication() { return authentication; } public final boolean permitAll() { return true; } public final boolean denyAll() { return false; } public final boolean isAnonymous() { return trustResolver.isAnonymous(authentication); } public final boolean isAuthenticated() { return !isAnonymous(); } public final boolean isRememberMe() { return trustResolver.isRememberMe(authentication); } public final boolean isFullyAuthenticated() { return !trustResolver.isAnonymous(authentication) && !trustResolver.isRememberMe(authentication); } /** * Convenience method to access {@link Authentication#getPrincipal()} from * {@link #getAuthentication()} * @return
*/
public Object getPrincipal() { return authentication.getPrincipal(); } public void setTrustResolver(AuthenticationTrustResolver trustResolver) { this.trustResolver = trustResolver; } public void setRoleHierarchy(RoleHierarchy roleHierarchy) { this.roleHierarchy = roleHierarchy; } /** * <p> * Sets the default prefix to be added to {@link #hasAnyRole(String...)} or * {@link #hasRole(String)}. For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN") * is passed in, then the role ROLE_ADMIN will be used when the defaultRolePrefix is * "ROLE_" (default). * </p> * * <p> * If null or empty, then no default role prefix is used. * </p> * * @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_". */
public void setDefaultRolePrefix(String defaultRolePrefix) { this.defaultRolePrefix = defaultRolePrefix; } private Set<String> getAuthoritySet() { if (roles == null) { roles = new HashSet<String>(); Collection<? extends GrantedAuthority> userAuthorities = authentication .getAuthorities(); if (roleHierarchy != null) { userAuthorities = roleHierarchy .getReachableGrantedAuthorities(userAuthorities); } roles = AuthorityUtils.authorityListToSet(userAuthorities); } return roles; } public boolean hasPermission(Object target, Object permission) { return permissionEvaluator.hasPermission(authentication, target, permission); } public boolean hasPermission(Object targetId, String targetType, Object permission) { return permissionEvaluator.hasPermission(authentication, (Serializable) targetId, targetType, permission); } public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } /** * Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role * does not already start with defaultRolePrefix. * * @param defaultRolePrefix * @param role * @return
*/
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) { if (role == null) { return role; } if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) { return role; } if (role.startsWith(defaultRolePrefix)) { return role; } return defaultRolePrefix + role; } }
在AuthenticationTrustResolverImpl方法isAnonymous中就是判斷傳遞過來的Authentication對象是不是AnonymousAuthenticationToken,如果是AnonymousAuthenticationToken則表示沒有登錄,因為登錄之后生成的對象是UsernamePasswordAuthenticationToken或其他Authentication對象,這也是Spring Security設計的最精華也是最難以理解也是最簡單的方式。
private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class; public boolean isAnonymous(Authentication authentication) { if ((anonymousClass == null) || (authentication == null)) { return false; } return anonymousClass.isAssignableFrom(authentication.getClass()); }
總結:Spring Security對url的權限判斷有兩種方式,一種是請求是permitAll的則直接返回校驗通過,另外一個是判斷Authentication是不是AnonymousAuthenticationToken,因為正常登錄等產生的不是這個對象,如果不是這個類型的對象則表示登錄成功了。
7、權限驗證通過
如果權限通過,代碼最終跳轉到/hello上:
瀏覽器頁面將顯示hello spring security信息;
六、代碼下載
參考文章:
[1] Spring Boot中開啟Spring Security(部分轉載)
[2] Spring Security原理學習--用戶名和密碼認證(三)
[3] Spring Security原理學習--權限校驗(四)
[4] Spring Security Config : 注解 EnableWebSecurity 啟用Web安全
[5] Spring Security實現原理剖析(一):filter的構造和初始化
[6] Spring Security 實現原理的理解記錄(推薦)