零、前言
上一篇文章讲解了SpringSecurity基础配置,这一篇文章试图从源码角度来分析SpringSecurity
的工作流程。
上一篇文章使用的是spirng boot 2.7.0
,这篇文章离上一篇文章比较久了,最新版也更新到spirng boot 3.1.5
,版本跨度比较大,但是上一篇文章中的新版本配置在3.1.5
版本中任然是有效的,只是部分配置有点小变化。所以不用担心版本不一样而不能使用了。文章最后会给出一版基于spirng boot 3.1.5
版本(对于spring security 6.1.5
)的配置,可以相互比较一下。
一、spring security
过滤器
最简单的使用spring security
的方式就是引入相应的spring boot security
依赖,这样访问任何接口都需要认证了。这是为什么呢?
我们都知道,spring mvc
中,一个请求都是经历一系列Filter
,然后到达DispatcherServlet
进行请求处理,而DispatcherServlet
是不涉及请求认证的,所以认证过程一定发生在前面的Filter
中,也就是说一定存在某些操作往我们的系统中添加了过滤器,导致我们的请求需求认证。
我们先来认识下spring security
中定义的过滤器,也可以说是官方提供的Filter
。
1.1 过滤器
spring security
提供的所有过滤器都在FilterOrderRegistration
类中声明,越先定义的Filter
优先级越高,其中任意两个过滤器之间的优先级顺序相差100。里面使用全限定类名添加的Filter
表明该类不在spring-security-config
包及其依赖包中,需要单独引入。
java
FilterOrderRegistration() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(DisableEncodeUrlFilter.class, order.next());
put(ForceEagerSessionCreationFilter.class, order.next());
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextHolderFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
讲到过滤器,在这里就先说下HttpSecurity
中addFilter()
和addFilterBefore()
等方法的区别:
addFilter()
:往过滤器链中添加一个过滤器,添加的过滤器必须位于FilterOrderRegistration
中定义的过滤器中,也就是官方定义的Filter
。addFilterBefore()
、addFilterAfter()
、addFilterAtOffsetOf()
等方法:用于添加自定义过滤器,并指定过滤器优先级(在官方定义的哪个Filter前执行,在哪个Filter后执行)。当然也可以是官方定义的过滤器,如果添加的是官方定义的Filter,这里指定的优先级不会生效,仍然是官方定义的那个优先级。至于原因,可以去看下源码。所有这里推荐这类方法只用来添加自定义过滤器,当然,你要弄明白是怎么回事了,怎么用都行。
1.2 过滤器拦截过程
上面定义了一系列的Filter
,其中ExceptionTranslationFilter
是用来处理异常的Filter
,而ExceptionTranslationFilter
和AuthorizationFilter
是来校验请求是否通过认证的。ExceptionTranslationFilter
在spring security 6.0
中被标记为过时了,在spring security 7.0
中会被删除,故后续都以AuthorizationFilter
进行说明。
这里ExceptionTranslationFilter
先于AuthorizationFilter
执行,这样只需再Filter
执行的最外层加一个try catch
就能捕获AuthorizationFilter
执行的异常,如果请求未认证,则抛出AccessDeniedException
异常,由ExceptionTranslationFilter
进行处理,进而重定向到登录页面。
下面我们具体来看下ExceptionTranslationFilter
实现。
1.2.1 ExceptionTranslationFilter
可以看到,实现很简单,就是在chain.doFilter(request, response)
外面包了一层try catch
,然后对异常进行处理。
java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 直接继续执行后续的Filter,这里就是执行AuthorizationFilter
chain.doFilter(request, response);
}
// AuthorizationFilter 执行抛出IO异常,不处理,原样抛出
catch (IOException ex) {
throw ex;
}
// 不是IO异常,则进行异常处理
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
// 处理异常
handleSpringSecurityException(request, response, chain, securityException);
}
}
handleSpringSecurityException()
该方法进一步区分了异常类型,并根据不同的异常类型做不同的逻辑处理,这里分为两类:
-
AuthenticationException
:表示认证异常,包括用户不存在异常(UsernameNotFoundException
)、密码错误异常(UsernameNotFoundException
)、账号过期异常(UsernameNotFoundException
)异常。所有的AuthenticationException
异常如下所示: -
AccessDeniedException
:表示访问被拒绝异常,也就是无权限。分为两种情况:一种是未认证,需要认证;另一种是认证了,但是不具备对资源的访问权限。
java
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
handleAuthenticationException()
该方法是对AuthenticationException
异常的处理
java
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
this.logger.trace("Sending to authentication entry point since authentication failed", exception);
sendStartAuthentication(request, response, chain, exception);
}
sendStartAuthentication()
该方法用来进行重新认证。包括认证异常AuthenticationException
异常以及AccessDeniedException
异常里的未认证的情况,都会调用该方法,进行重新认证。主要做了一下三件事:
- 设置空的安全上下文
- 保存请求,这样登录后能重定向到登录前访问的地址(使用默认配置。
spring security
可以配置为登录成功后重定向到指定的地址) - 认证端点处理
java
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// 1、设置空的安全上下文
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextHolderStrategy.setContext(context);
// 2、保存请求,这样登录后能重定向到登录前访问的地址
this.requestCache.saveRequest(request, response);
// 3、认证端点处理
this.authenticationEntryPoint.commence(request, response, reason);
}
handleAccessDeniedException()
该方法用来处理AccessDeniedException
异常。分两种情况处理:
- 一种是未认证,需要认证,则调用
sendStartAuthentication()
方法; - 另一种是认证了,但是不具备对资源的访问权限,则调用无权限处理器
AccessDeniedHandler
进行处理
java
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
// 判断是否为匿名用户
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
// 如果是匿名用户或者为rememberMe用户,则要求进行认证
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
// 不是匿名用户,说明已经登录了,这个时候就是无权限了,执行无权限处理器
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
1.2.2 AuthorizationFilter
核心逻辑就是通过授权管理器进行核验,当前用户是否对当前资源有权限,如果没有权限,则抛出AccessDeniedException
异常。
java
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1、对请求只验证一次,并且该属性名称已经进行过验证,则不进行验证。observeOncePerRequest默认为false,表示每次请求都要验证
if (this.observeOncePerRequest && isApplied(request)) {
chain.doFilter(request, response);
return;
}
// 2、部分请求无需验证
if (skipDispatch(request)) {
chain.doFilter(request, response);
return;
}
// 3、缓存进行验证过的属性名称,如果observeOncePerRequest为true,则只进行一次验证,后续再次请求时不会验证
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 4、验证,
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
// 无权限,则抛出AccessDeniedException
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
}
finally {
// 清空缓存的属性名称
request.removeAttribute(alreadyFilteredAttributeName);
}
}
二、spring security
的自动配置
我们先看下spring security
的自动配置类,看他做了哪些事情:
2.1 SecurityAutoConfiguration
java
@AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
核心是注入了一个DefaultAuthenticationEventPublisher
类型的bean,用来发布事件;然后导入了两个类SpringBootWebSecurityConfiguration
、SecurityDataConfiguration
,核心类是SpringBootWebSecurityConfiguration
,我们详细看下。
SpringBootWebSecurityConfiguration
实现很简单,定义了两个静态内部类。
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(withDefaults());
http.httpBasic(withDefaults());
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnClass(EnableWebSecurity.class)
@EnableWebSecurity
static class WebSecurityEnablerConfiguration {
}
}
WebSecurityEnablerConfiguration
是用来判断项目中是否使用了@EnableWebSecurity
注解,如果没有使用,则添加。也就是说我们自定义spring security配置类时,可以不用添加@EnableWebSecurity
注解。spring boot强大吧!虽然但是,还是建议自定义配置类时手动添加@EnableWebSecurity
注解。
再来看下SecurityFilterChainConfiguration
。注入了一个SecurityFilterChain
类型的bean,该类是spring security的核心类,我们所有的配置以及过滤器都是位于SecurityFilterChain
中
该方法做了三件事:
authorizeHttpRequests
:配置认证过滤器AuthorizationFilter
,所有请求都会被此过滤器拦截,如果未登录,则重定向登录页面;否则同行formLogin
:配置用户使用用户名密码的登录认证过滤器UsernamePasswordAuthenticationFilter
,这样我们才能通过用户名密码登录httpBasic
:配置basic认证过滤器BasicAuthenticationFilter
,这样我们就能通过添加请求头来进行认证
虽然说spring boot自动配置只做了这三件事,但是别忘了,这里还有一个入参HttpSecurity http
,初始HttpSecurity
又是怎样的呢?
我们都知道,配置类中@bean
方法的入参来源都是容器的中一个bean对象,所有一定有一个地方注入了一个HttpSecurity
类型的bean。其实就是在HttpSecurityConfiguration
类中,而该类是由EnableWebSecurity
注解引入的,所有呢spring security
项目中需要使用EnableWebSecurity
注解,也就有了前面的WebSecurityEnablerConfiguration
,万一用户忘记了添加EnableWebSecurity
注解,也能保证程序不出错。看看,spring boot
为用户考虑的周全吧。
2.2 HttpSecurityConfiguration
回到HttpSecurityConfiguration.httpSecurity()
来,可以看到他是一个多例bean
,而不是单例,这样我们每次自定义配置时使用的HttpSecurity
的配置信息都是一样的,不会因为一个地方配置了从而影响另一个地方,bean名称为org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity
。
该方法主要做了以下几个配置:
- 设置认证管理器。通过注入的
AuthenticationConfiguration
对象来获取AuthenticationManager
,我们也可以通过此方式来获取AuthenticationManager
。 csrf()
:开启csrf
,withDefaults()
是Customizer
接口中的静态方法,表示不手动设置,使用默认配置,我们也可以使用lambda进行自定义配置,后续所有withDefaults()
方法都一样,就不重复说明了。exceptionHandling()
:启用异常处理器,添加ExceptionTranslationFilter
过滤器,我们访问需要认证的地址,会抛出一个异常,然后被该过滤器拦截,重定向到登陆页面。ExceptionTranslationFilter
由两个核心属性AuthenticationEntryPoint
和AccessDeniedHandler
,AuthenticationEntryPoint
用于设置未登录用户的处理逻辑;AccessDeniedHandler
用于处理用户登录成功了,但是无权限访问的逻辑,实际使用时我们可以设置这两个属性覆盖掉默认的操作。apply(new DefaultLoginPageConfigurer<>())
:添加默认的登录页的配置,该配置添加了两个过滤器:DefaultLoginPageGeneratingFilter
和DefaultLogoutPageGeneratingFilter
。前者用于定义默认的登录页面,后者用于定义默认的注销页面。logout(withDefaults())
:添加注销配置类(LogoutConfigurer
),该配置添加了一个LogoutFilter
类型的Filter,用户设置注销请求地址以及注销处理器。applyDefaultConfigurers()
:加载spring.factories
文件中定义的AbstractHttpConfigurer
类型的对象,对于spring boot
项目再熟悉不过了。
java
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
// 构造认证管理器的构建器
AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
this.objectPostProcessor, passwordEncoder);
// 添加认证管理器
authenticationBuilder.parentAuthenticationManager(authenticationManager());
authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
// 构造HttpSecurity对象
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
// @formatter:off
// 一系列的默认配置
http
.csrf(withDefaults())
.addFilter(webAsyncManagerIntegrationFilter)
.exceptionHandling(withDefaults())
.headers(withDefaults())
.sessionManagement(withDefaults())
.securityContext(withDefaults())
.requestCache(withDefaults())
.anonymous(withDefaults())
.servletApi(withDefaults())
.apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
applyDefaultConfigurers(http);
return http;
}
2.3 源码分析
说了这么多,我们来看下HttpSecurity
中的方法,这里以formLogin()
来进行说明。
2.3.1 HttpSecurity.formLogin()
实现很简单,new了一个FormLoginConfigurer
对象,通过getOrApply()
方法添加到AbstractConfiguredSecurityBuilder.configurers
集合中。入参的formLoginCustomizer
是一个消费者的函数式接口,用于我们自定义FormLoginConfigurer
各个属性,如果无需自定义,则可以使用Customizer.withDefaults()
java
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
return HttpSecurity.this;
}
2.3.2 FormLoginConfigurer
FormLoginConfigurer
继承了AbstractAuthenticationFilterConfigurer
,添加了一个UsernamePasswordAuthenticationFilter
过滤器。
我们直接看init
方法,至于其他属性赋值方法可以查看另一篇文章SpringSecurity基础配置 - 掘金 (juejin.cn)
init()
java
@Override
public void init(H http) throws Exception {
super.init(http);
initDefaultLoginFilter(http);
}
调用了父类的init()
方法,然后再初始化默认的登录过滤器
AbstractAuthenticationFilterConfigurer.init()
该方法主要做了三件事:
- 更新默认的认证信息
- 更新访问权限的默认值
- 注册默认的认证端点
java
@Override
public void init(B http) throws Exception {
// 更新默认的认证信息
updateAuthenticationDefaults();
// 更新访问权限的默认值。
updateAccessDefaults(http);
// 注册默认的认证端点
registerDefaultAuthenticationEntryPoint(http);
}
protected final void updateAuthenticationDefaults() {
// 设置登录请求,默认的登录页面请求为Get请求的/login,故登录请求也为/login,只是请求方法为Post请求
if (this.loginProcessingUrl == null) {
loginProcessingUrl(this.loginPage);
}
// 设置登录失败后的请求地址,如果存在登录失败的Handler,则不会设置,也就是说failureHandler会覆盖掉failureUrl
if (this.failureHandler == null) {
failureUrl(this.loginPage + "?error");
}
// 设置退出登录后的重定向地址,这里默认为欸/login?logout,表示是退出登录后重定向来的,其实就是默认的登录页
LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout");
}
}
protected final void registerDefaultAuthenticationEntryPoint(B http) {
// 设置ExceptionHandlingConfigurer中的默认的认证端点,其实就是设置的ExceptionTranslationFilter中的认证断点,请求为认证时的处理逻辑
registerAuthenticationEntryPoint(http, this.authenticationEntryPoint);
}
protected final void registerAuthenticationEntryPoint(B http, AuthenticationEntryPoint authenticationEntryPoint) {
// 从共享配置中拿到 ExceptionHandlingConfigurer
ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
}
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
getAuthenticationEntryPointMatcher(http));
}
上面提到的ExceptionHandlingConfigurer
认证端点由AbstractAuthenticationFilterConfigurer
构造器赋值:
java
protected AbstractAuthenticationFilterConfigurer() {
setLoginPage("/login");
}
private void setLoginPage(String loginPage) {
this.loginPage = loginPage;
this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}
initDefaultLoginFilter()
其实就是设置DefaultLoginPageGeneratingFilter
的各个属性,属性默认值位于DefaultLoginPageGeneratingFilter
java
private void initDefaultLoginFilter(H http) {
// 从共享对象中获取 DefaultLoginPageGeneratingFilter
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
// 请用表单登录
loginPageGeneratingFilter.setFormLoginEnabled(true);
// 设置用户名参数,默认username
loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
// 设置密码参数,默认password
loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
// 设置登录页面请求,默认/login
loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
// 设置登录失败的请求,默认为/login?error
loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
// 设置登录请求,默认/login
loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
}
}
FormLoginConfigurer
并没有重写configure()
方法,那我们看下父类的configure()
方法。
configure()
主要做了一下几件事:
- 设置端口映射器
- 设置请求缓存器
- 设置认证管理器
- 设置认证成功处理器和认证失败处理器
- 设置session认证策略
- 设置rememberMe服务
- 设置安全上下文仓库
- 设置安全上下文持有者策略
- 添加过滤器
java
@Override
public void configure(B http) throws Exception {
// 1、设置端口映射器
PortMapper portMapper = http.getSharedObject(PortMapper.class);
if (portMapper != null) {
this.authenticationEntryPoint.setPortMapper(portMapper);
}
// 2、设置请求缓存器
RequestCache requestCache = http.getSharedObject(RequestCache.class);
if (requestCache != null) {
this.defaultSuccessHandler.setRequestCache(requestCache);
}
// 3、设置认证管理器
this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 4、设置认证成功处理器和认证失败处理器
this.authFilter.setAuthenticationSuccessHandler(this.successHandler);
this.authFilter.setAuthenticationFailureHandler(this.failureHandler);
if (this.authenticationDetailsSource != null) {
this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
}
// 5、设置session认证策略
SessionAuthenticationStrategy sessionAuthenticationStrategy = http
.getSharedObject(SessionAuthenticationStrategy.class);
if (sessionAuthenticationStrategy != null) {
this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
// 6、设置rememberMe服务
RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
this.authFilter.setRememberMeServices(rememberMeServices);
}
// 7、设置安全上下文仓库
SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
SecurityContextRepository securityContextRepository = securityContextConfigurer
.getSecurityContextRepository();
this.authFilter.setSecurityContextRepository(securityContextRepository);
}
// 8、设置安全上下文持有者策略,认证通过后,会将认证信息放入这里设置的策略中,这样我们就能通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到认证对象了
this.authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
// 过滤器的后置处理
F filter = postProcess(this.authFilter);
// 9、添加过滤器
http.addFilter(filter);
}
FormLoginConfigurer
类的核心内容我们就分析完了,这里总结下
总结
FormLoginConfigurer
是一个SecurityConfigurerAdapter
对象,包括其他方法如authorizeHttpRequests()
添加一个AuthorizeHttpRequestsConfigurer
对象;exceptionHandling()
方法添加的ExceptionHandlingConfigurer
对象,都是SecurityConfigurerAdapter
子类。
对于所有的SecurityConfigurerAdapter
类,核心方法就两个:init()
和configure()
,前者为初始化方法,后者为配置方法,因此我们分析SecurityConfigurerAdapter
子类时从这两个方法入手即可,其他的都是一下属性配置方法。
这里给出了所有SecurityConfigurerAdapter
子类,这些类都会添加一个或多个Filter
,用于完成对应的功能。
而对于Filter,我们则关注doFilter()
方法即可。这里以FormLoginConfigurer
添加的UsernamePasswordAuthenticationFilter
为例进行进一步说明。
2.3.3 UsernamePasswordAuthenticationFilter
我们打开UsernamePasswordAuthenticationFilter
源码,发现其并没有doFilter()
方法,但是有一个父类AbstractAuthenticationProcessingFilter
,不难想到,doFilter()
方法位于其父类中。
AbstractAuthenticationProcessingFilter.doFilter
java
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断请求是否需要认证,不需要直接放行,否则需要认证
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 尝试认证
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 认证成功
// 处理session策略
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功逻辑
successfulAuthentication(request, response, chain, authenticationResult);
}
// 抛出异常,认证失败
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
attemptAuthentication()
该方法是一个抽象方法,由子类实现,这里是UsernamePasswordAuthenticationFilter
java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 获取用户名参数
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
// 获取密码参数
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 初始化认证对象
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// 设置请求详细信息
setDetails(request, authRequest);
// 通过认证管理器认证
return this.getAuthenticationManager().authenticate(authRequest);
}
认证管理器有很多,一般都是由ProviderManager
进行处理。
ProviderManager.authenticate()
java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// 身份验证提供程序逐个进行认证
for (AuthenticationProvider provider : getProviders()) {
// 当前认证器不支持,下一个
if (!provider.supports(toTest)) {
continue;
}
try {
// 认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
...... //异常处理
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
// 使用父认证器
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// 忽略异常
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
......
// 返回认证结果
return result;
}
......
// 抛出异常
throw lastException;
}
对于基于用户名密码的认证方式,使用的是AbstractUserDetailsAuthenticationProvider
认证器,其实就是DaoAuthenticationProvider
认证器
AbstractUserDetailsAuthenticationProvider.authenticate()
java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 获取 UserDetails 对象,由子类DaoAuthenticationProvider实现
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
// 如果隐藏UsernameNotFoundException,则抛出BadCredentialsException,为了安全,不管是用户名还是密码错误,都提示密码错误,则增加破解难度
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// 校验账号是否启用,是否锁定等
this.preAuthenticationChecks.check(user);
// 校验用户名和密码是否正确
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
// 不是缓存数据,则原样抛出异常
if (!cacheWasUsed) {
throw ex;
}
// 使用了缓存,则重新获取最新的,然后再校验
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
// 将用户对象放入缓存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建认证成功的凭证。之前创建的认证凭证,并不代表认证成功,你可以将其理解为是一个VO对象,用来流转属性。
return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider.retrieveUser()
该方法就是调用UserDetailsService
接口的loadUserByUsername()
,获取UserDetails
对象。我们实际使用中不都是会定义一个类,实现UserDetailsService
接口,然后在loadUserByUsername()
方法中自定义处理逻辑嘛,就是这里调用的。
java
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 调用UserDetailsService接口的loadUserByUsername(),获取UserDetails对象
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
successfulAuthentication()
该方法是认证成功后的处理方法,主要做了一下几件事:
- 设置安全上下文,并将其放到安全上下文持有者策略中。这样我们才能通过
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
获取到上下文信息 rememberMe
的登录成功处理逻辑- 登录成功处理器。如果我们自定义了,就是执行自定义的处理逻辑;如果没有自定义,则使用默认的
SimpleUrlAuthenticationSuccessHandler
,将请求重定向到指定的url
,该url
可以通过FormLoginConfigurer
的defaultSuccessUrl()
方法进行设置
java
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 设置安全上下文
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
// 将上下文放到安全上下文持有者策略中,这样我们才能通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取到上下文信息
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// rememberMe 的登录成功处理逻辑
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 登录成功处理器
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
unsuccessfulAuthentication()
该方法用来执行认证失败的处理逻辑。
java
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// 清空上下文信息
this.securityContextHolderStrategy.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
// rememberMe服务的登录失败处理逻辑
this.rememberMeServices.loginFail(request, response);
// 执行定义的失败处理器
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
三、spring security 6.X
配置
这里给出新版本的一个配置,供大家参考。
注意不要忘了
@EnableWebSecurity
注解,前面已经说过,如果忘了spring boot
会自动添加,但还是建议手动添加@EnableWebSecurity
。
spring security 6.0
版本和5.0
的区别不大,主要就是一些方法被标记为过时了,如authorizeRequests()
,同时删除了antMatchers()
和mvcMatchers()
方法,统一使用requestMatchers()
。
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
/**
* Spring Security 白名单url
*/
@Value("${security.whitelist.urls:/login,/register,/captcha}")
private String[] whileUrls;
@Resource
private RuoYiConfig ruoYiConfig;
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;
@Resource
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Resource
private MyLogoutSuccessHandler logoutSuccessHandler;
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 跨域过滤器
*/
@Resource
private CorsFilter corsFilter;
/**
* 定义密码编码方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 通过 AuthenticationConfiguration 获取 AuthenticationManager
* @param configuration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
/**
* 定义spring security配置
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用csrf:基于token认证,故不需要csrf保护
.csrf(AbstractHttpConfigurer::disable)
// 禁用session:基于token认证,不需要session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 请求认证,除了白名单外,所有请求都要认证
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(ruoYiConfig.getWhiteUrls()).permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
// 认证失败处理操作
.exceptionHandling(exception -> exception
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
)
// 登出操作
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
// 添加jwt认证过滤器,由于JwtAuthenticationFilter是自定义过滤器,不在spring security的过滤器链中,
// 故需调用addFilterBefore或addFilterAfter方法将其加入到过滤器链中
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 添加CorsFilter过滤器,由于CorsFilter存在于spring security的过滤器链中,故直接添加即可,
// 当然也可以调用addFilterBefore(),本质上和addFilter()方法的逻辑一致。
.addFilter(corsFilter)
;
return http.build();
}
}