零、前言
上一篇文章讲解了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();
}
}