零、前语
上一篇文章讲解了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
包及其依靠包中,需求单独引进。
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
,然后对反常进行处理。
@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
:表明拜访被拒绝反常,也便是无权限。分为两种状况:一种是未认证,需求认证;另一种是认证了,可是不具备对资源的拜访权限。
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
反常的处理
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
反常里的未认证的状况,都会调用该办法,进行从头认证。首要做了一下三件事:
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
进行处理
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
反常。
@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
@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
完结很简单,界说了两个静态内部类。
@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
项目再了解不过了。
@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()
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()
@Override
public void init(H http) throws Exception {
super.init(http);
initDefaultLoginFilter(http);
}
调用了父类的init()
办法,然后再初始化默许的登录过滤器
AbstractAuthenticationFilterConfigurer.init()
该办法首要做了三件事:
- 更新默许的认证信息
- 更新拜访权限的默许值
- 注册默许的认证端点
@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
结构器赋值:
protected AbstractAuthenticationFilterConfigurer() {
setLoginPage("/login");
}
private void setLoginPage(String loginPage) {
this.loginPage = loginPage;
this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}
initDefaultLoginFilter()
其实便是设置DefaultLoginPageGeneratingFilter
的各个特点,特点默许值坐落DefaultLoginPageGeneratingFilter
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服务
- 设置安全上下文仓库
- 设置安全上下文持有者战略
- 增加过滤器
@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
@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
@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()
@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()
@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()
办法中自界说处理逻辑嘛,便是这儿调用的。
@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()
办法进行设置
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()
该办法用来履行认证失利的处理逻辑。
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()
。
@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();
}
}