前言
- 在阅读本文前,希望你具备如下知识
- 了解Spring Boot和Spring Security的基本使用
- 具备Java Web的基本知识
- 具备认证和授权的基本知识
- 本文基于Spring Boot2.6.6讲解,内置Spring Security版本为5.6.2,所采用的配置方式依然为继承WebSecurityConfigurerAdapter的方式,虽然新版Spring Security已经不再采用这种配置方式,但本文对于你了解Spring Security的工作原理仍然具有重要意义。
- 看源码的时候最好盯紧流程图,抓住主线
1.Spring Boot如何集成Spring Security
- IDEA中新建一个Spring Boot工程
- 引入依赖
<dependencies> <!-- Spring Web相关依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security相关依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
- 编写一个简单的
Controller
@RestController @RequestMapping("/hello") public class HelloController { @GetMapping("/helloWorld") public String helloWorld() { return "Hello World!"; } }
- 启动项目,在浏览器的地址栏输入
http://localhost:8080/hello/helloWorld
,此时会出现如何如下登录页
- 输入默认的用户名
user
和密码(idea
控制台会打印出默认密码,如下图所示),登录成功,浏览器页面成功展示出"Hello World!"
以上便是Spring Boot集成Spring Security的基本流程,那么我们访问HelloController
时,浏览器为何会跳转到登录页?默认用户名为什么是user
?控制台的初始密码是如何打印出来的?
2.Spring Security的默认认证和授权流程
首先说一下的浏览器为何会跳转到登录页,打开浏览器的开发者工具可以看到我们的请求被重定向到了http://localhost:8080/login
。
为什么我们的请求会被重定向呢?肯定是我们没有通过认证呀,一定是在没有通过Spring Security
的认证的情况下,Spring Security
拦截了我们请求之后,将请求重定向到了默认登录页,当然这只是猜测,具体为什么会跳转到默认登录页,还需要我们了解Spring Security
的认证和授权流程。既然前面都已经提到拦截请求了,那什么能拦截我们的请求呢,那肯定是Filter
呀!对,你没猜错,Spring Security
的认证和授权就是通过一系列Filter
实现的,即FilterChain
,当我们集成Spring Security
时,Spring security
就为我们配置了默认的FilterChain
,其中包含15个Filter
,可以可以通过下面的代码查看15个Filter
,具体这15个Filter
从何而来,我们会在后面进行讨论。
@SpringBootApplication
public class SpringSecurityApplication {
public static void main(String[] args) throws ClassNotFoundException {
ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringSecurityApplication.class, args);
DefaultSecurityFilterChain defaultSecurityFilterChain = applicationContext.getBean(DefaultSecurityFilterChain.class);
List<Filter> filters = defaultSecurityFilterChain.getFilters();
for (int i = 0; i < filters.size(); i++) {
System.out.println((i + 1) + ": " + filters.get(i));
}
}
}
默认情况下,当我们访问HelloController
,我们请求会经过上图所示的15个Filter
,啊!怎么会有这么多Filter
?Spring Security
怎么如此复杂,既然Spring Secuity
是认证和授权框架,那么我们只要着重研究与认证和授权有关的Filter就行,那么,是哪个Filter
呢?对,你没看错,就是那个UsernamePasswordAuthenticationFilter
,见名知意,一看这个Filter
就是用来进行认证的。既然是UsernamePasswordAuthenticationFilter
,那肯定是将用户输入的用户名和密码与服务器存储的用户名和密码进行比对,一致就认证通过,不一致就认证失败,至于这个密码怎么存储,存储在什么地方,我们后面再进行讨论。认证通过之后就进行授权,那么授权又是哪一个Filter
完成的呢,可能看名字看不出来,其实就是最后一个FilterSecurityInterceptor
,咦?怎么就这个不叫Filter
,叫Interceptor
,那是因为请求到达接口之前要进行拦截,判断本次请求是否有权限访问接口。
2.1 认证
UsernamePasswordAuthenticationFilter
的大致认证流程如下图所示。
AbstractAuthenticationProcessingFilter
:基于浏览器的http认证请求的抽象处理器。该过滤器有一个authenticationManager
属性。具体的认证流程交给AuthenticationManager
进行处理。该过滤器还有一个requiresAuthenticationRequestMatcher
,如果请求与matcher
匹配,该过滤器将拦截请求并尝试从该请求执行身份验证,身份验证由attemptAuthentication
方法执行,该方法必须由子类实现。UsernamePasswordAuthenticationFilter
:对表单提交的用户名和密码进行认证。Authentication
:封装了当前待认证的用户信息,和认证通过后的用户信息,最常见的实现类是UsernamePasswordAuthenticationToken
。AuthenticationManager
:只有一个抽象方法Authentication authenticate(Authentication authentication)
,其实现类需要重写该方法,所有的认证流程都在该方法中,ProviderManager
是其最常见的实现类。AbstractUserDetailsAuthenticationProvider
:AuthenticationProvider
的实现类,而AbstractUserDetailsAuthenticationProvider
是使用UserDetail
对象进行认证的,认证时需要一个Authentication
对象,认证成功后返回一个Authentication
对象。该类主要用于响应UsernamePasswordAuthenticationToken
身份验证请求。不同的AuthenticationProvider
在认证时所需要的Authentication
对象是不同的。AuthenticationProvider
:具有Authentication authenticate(Authentication authentication)
和boolean supports(Class<?> authentication)
两个抽象方法,具体的认证逻辑由不同的实现类提供,不同的实现类认证不同Authentication
对象,方法supports
用于判定该实现类是否支持对应的Authentication
。UserDetailsService
:用于获取服务器存储的用户信息的核心接口,只有一个抽象方法UserDetails loadUserByUsername(String username)
。UserDetails
:封装服务器存储的用户信息。
2.1.1 UsernamePasswordAuthenticationFilter
可能直接看认证流程图比较抽象,下面就结合具体代码来理解上面的认证流程,代码里面还有很多细节,首先是UsernamePasswordAuthenticationFilter
的父类AbstractAuthenticationProcessingFilter
如下所示。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
protected ApplicationEventPublisher eventPublisher;
/**
* 用于请求认证
*/
private AuthenticationManager authenticationManager;
private RememberMeServices rememberMeServices = new NullRememberMeServices();
/**
* 用于判断当前请求是否需要认证
*/
private RequestMatcher requiresAuthenticationRequestMatcher;
/**
* 认证成功后是否要继续执行过滤器链
*/
private boolean continueChainBeforeSuccessfulAuthentication = false;
/**
* 认证成功后的session处理策略
*/
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
/**
* 用于处理认证成功后相关事宜
*/
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
/**
* 用于处理认证失败后相关事宜
*/
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
/**
* @param defaultFilterProcessesUrl 当前Filter要进行认证的url,实际就是为属性requiresAuthenticationRequestMatcher赋值
*/
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
setFilterProcessesUrl(defaultFilterProcessesUrl);
}
/**
* @param requiresAuthenticationRequestMatcher 用于判断当前请求是否需要认证
*/
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
/**
* @param defaultFilterProcessesUrl 当前Filter要进行认证的url
* @param authenticationManager 用于请求认证
*/
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl,
AuthenticationManager authenticationManager) {
setFilterProcessesUrl(defaultFilterProcessesUrl);
setAuthenticationManager(authenticationManager);
}
/**
* @param requiresAuthenticationRequestMatcher 用于判断当前请求是否需要认证
* @param authenticationManager 用于请求认证
*/
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,
AuthenticationManager authenticationManager) {
setRequiresAuthenticationRequestMatcher(requiresAuthenticationRequestMatcher);
setAuthenticationManager(authenticationManager);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
/**
* 调用requiresAuthentication方法来确定请求是否用于身份验证,是否应该由此过滤器处理。
* 如果是身份验证请求,则调用attemptAuthentication来执行身份验证。有三种可能的结果:
* 1.返回一个Authentication对象。配置的SessionAuthenticationStrategy将被调用,
* 然后调用successfulAuthentication方法
* 2.认证过程中出现异常。将调用unsuccessfulAuthentication方法
* 3.返回Null,表示认证过程未完成
*/
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) {
// 认证失败后的处理策略
unsuccessfulAuthentication(request, response, ex);
}
}
/**
* 判断当前的请求是否需要进行认证
* @return 需要认证就返回ture,不需要认证就返回false
*/
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
/**
* 用于执行具体的认证流程,由子类进行实现
* @return 经过身份验证的用户令牌,如果身份验证不完整,则为空
* @throws AuthenticationException 认证失败将抛出异常
*/
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
/**
* 认证成功后的处理策略,子类可以重写此方法,以便在身份验证成功后继续FilterChain。
* 1.将认证成功后的用户信息存入SecurityContextHolder
* 2.登录成功后通知已配置的RememberMeServices
* 3.通过配置的ApplicationEventPublisher触发一个InteractiveAuthenticationSuccessEvent
* 4.将其他行为委托给AuthenticationSuccessHandler
* @param authResult 方法attemptAuthentication返回的经过身份验证的用户令牌
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
// 1.将认证成功后的用户信息存入SecurityContextHolder,
SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// 2.登录成功后通知已配置的RememberMeServices
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
// 3.通过配置的ApplicationEventPublisher触发一个InteractiveAuthenticationSuccessEvent
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 4.将其他行为委托给AuthenticationSuccessHandler
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
/**
* 认证失败后的处理策略
* 1.清除SecurityContextHolder
* 2.将登录失败通知已配置的RememberMeServices
* 3.将其他行为委托给AuthenticationFailureHandler
*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
从上面的源码可以看出AbstractAuthenticationProcessingFilter
的主要作用就以下三点:
- 判断当前请求是否需要认证
- 如果当前请求需要认证,就调用
attemptAuthentication
来执行身份验证 - 处理认证成功或失败相关事宜
从上面的分析可以看出,具体的认证逻辑都在其子类当中,接下来就来看看其子类UsernamePasswordAuthenticationFilter
是如何进行认证的。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 登录表单提交用户名使用的默认参数名
*/
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
/**
* 登录表单提交密码用的默认参数名
*/
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
/**
* 默认情况下,只有当前请求的url为 “/login”,且http请求为post请求时进行认证
*/
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
// 初始化父类的属性requiresAuthenticationRequestMatcher
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
// 初始化父类的属性requiresAuthenticationRequestMatcher和authenticationManager
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
// 1.判断请求是不是POST请求,只有POST请求才进行认证
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2.从请求中获取用户名
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
// 3.从请求中获取密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 4.将用户名和密码封装为UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 5.为待认证的UsernamePasswordAuthenticationToken添加配置信息,扩展UsernamePasswordAuthenticationToken
setDetails(request, authRequest);
// 6.调用AuthenticationManager的authenticate进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 从请求中获取密码
*/
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
/**
* 从请求中获取用户名
*/
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
/**
* 为待认证的UsernamePasswordAuthenticationToken添加配置信息
*/
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
从上面的源码可以看出,UsernamePasswordAuthenticationFilter
做的事情其实非常简单,就是获取到请求参数后,将请求参数封装为对应的Authentication
,并将Authentication
交给AuthenticationManager
进行认证。
2.1.2 ProviderManager
从前面的分析可以看出,UsernamePasswordAuthenticationFilter
只是简单的做了一些认证前的准备工作和认证后的善后工作,真正的认证流程其实是在AuthenticationManager
当中,下面就来看一下其实现类ProviderManager
是如何实现认证的。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(org.springframework.security.authentication.ProviderManager.class);
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
/**
* 一系列AuthenticationProvider,不同的AuthenticationProvider认证不同的Authentication
*/
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
/**
* 父AuthenticationManager,通常情况下为ProviderManager
*/
private AuthenticationManager parent;
/**
* 认证完成后是否要清楚凭据信息
*/
private boolean eraseCredentialsAfterAuthentication = true;
/**
* 初始化providers
*/
public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), null);
}
/**
* 初始化providers
*/
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
/**
* 初始化providers和父AuthenticationManager
*/
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
@Override
public void afterPropertiesSet() {
checkState();
}
private void checkState() {
Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
"A parent AuthenticationManager or a list of AuthenticationProviders is required");
Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
"providers list cannot contain null values");
}
/**
* 对传入的Authentication对象进行身份验证。
* @param authentication 待认证的Authentication对象
* @return 认证完成后Authentication对象,包含用户权限信息
* @throws AuthenticationException 认证失败将抛出异常
*/
@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()) {
// 1.遍历providers,判断当前的AuthenticationProvider是否支持传入的Authentication对象
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
// 2.调用AuthenticationProvider的authenticate方法进行认证
result = provider.authenticate(authentication);
if (result != null) {
// 3.将身份验证请求的其他详细信息(例如IP地址,证书序列号等)拷贝到认证完成后Authentication对象中
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
// 4.认证结果结果为空,则交给父AuthenticationManager进行认证
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
// 5.父AuthenticationManager开始认证,再次进入ProviderManager的authenticate方法
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
// 6.返回结果
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
// 7.认证失败,抛出异常
throw lastException;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
/**
* 将身份验证请求的其他详细信息(例如IP地址,证书序列号等)拷贝到认证完成后Authentication对象中
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
this.eraseCredentialsAfterAuthentication = eraseSecretData;
}
public boolean isEraseCredentialsAfterAuthentication() {
return this.eraseCredentialsAfterAuthentication;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
@Override
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
@Override
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}
从上面的源码可以看出AuthenticationManager
做的事情也非常简单,就是找到合适的AuthenticationProvider
进行认证,如果自己搞不定,就交给父AuthenticationManager
来搞定,那么为什么要给每一个AuthenticationManager
设置一个父AuthenticationManager
,具体可以参阅官方文档。
2.1.3 DaoAuthenticationProvider
从前面的分析可以看出,AuthenticationManager
实际上就相当于一个管理者,真正干活的其实还是AuthenticationProvider
,事实上,UsernamePasswordAuthenticationFilter
中AuthenticationManager
管理的AuthenticationProvider
,其实并不是DaoAuthenticationProvider
,而是AnonymousAuthenticationProvider
,真正的DaoAuthenticationProvider
其实是在其父AuthenticationManager
中的。前面讲到过不同的AuthenticationProvider
对不同的Authentication
进行认证,而前面UsernamePasswordAuthenticationFilter
封装的Authentication
实现类是UsernamePasswordAuthenticationToken
,只有DaoAuthenticationProvider
支持这种类型的Authentication
,AnonymousAuthenticationProvider
是不支持的,所以最终完成认证的是UsernamePasswordAuthenticationFilter
中AuthenticationManager
的父AuthenticationManager
,下面就来看看DaoAuthenticationProvider
是如何进行认证的,首先看到它的父类AbstractUserDetailsAuthenticationProvider
,因为其父类重写了authenticate
方法。
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
/**
* 此方法由子类进行实现,用于对用户信息进行一些额外的检查,例如密码检查
* @param userDetails 服务器事先存储的用户信息
* @param authentication 当前需要被认证的Authentication
* @throws AuthenticationException 认证失败抛出异常
*/
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
/**
* 此方法由子类实现,用于加载服务器事先存储的用户信息
* @param username 用户名
* @param authentication 当前需要被认证的Authentication
* @return 服务器事先存储的用户信息
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
@Override
public final void afterPropertiesSet() throws Exception {
Assert.notNull(this.userCache, "A user cache must be set");
Assert.notNull(this.messages, "A message source must be set");
doAfterPropertiesSet();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1.断言,当前AuthenticationProvider仅支持UsernamePasswordAuthenticationToken类型的Authentication
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 2.从Authentication中获取获取用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// 3.根据用户名从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 4.从缓存中未获取到用户信息,从其它地方获取用户信息,此方法由子类实现
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
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 {
// 5.对用户信息进行一些预检查
this.preAuthenticationChecks.check(user);
// 6.对用户信息进行一些额外的检查(例如检查密码是否正确),此方法由子类进行实现
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 7.对用户信息进行一些后置检查
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
// 8.缓存中不存在用户信息,将用户信息存入缓存
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 8.创建验证通过后的Authentication,将其返回给调用者
return createSuccessAuthentication(principalToReturn, authentication, user);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
/**
* 创建验证通过后的Authentication(此Authentication包含用户的权限信息)
*/
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
protected void doAfterPropertiesSet() throws Exception {
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* 判断当前的AuthenticationProvider是否支持传入的Authentication
*/
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
}
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
}
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
}
}
从上面的源码可以看出,父类做的工作其实非常简单,主要就就两点:一是判断当前的AuthenticationProvider
是否支持传入的Authentication
,而是获取服务器存储的用户信息,对用户信息进行一些简单的检查工作。
既然父类把具体的认证工作交给了子类的additionalAuthenticationChecks
方法,那么接下来就来看看子类DaoAuthenticationProvider
是如何完成认证的。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
// 校验上送的密码是否正确
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 调用UserDetailsService的loadUserByUsername方法获取服务器存储的用户信息,默认情况下实现类为InMemoryUserDetailsManager
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);
}
}
}
从上面的源码当中,我们可以看出子类DaoAuthenticationProvider
仅仅是利用PasswordEncoder
做了密码的校验,并未做其它复杂的校验。
2.1.4 总结
啊!怎么认证流程还是怎么复杂?其实总结起来,就是UsernamePasswordAuthenticationFilter
将我们提交的用户信息封装为Authentication
对象,此时的Authentication
是没有权限信息的。然后,将Authentication
对象交给内部的AuthenticationManager
处理,AuthenticationManager
一看,这我也不会处理呀,于是就去找AuthenticationProvider
,AuthenticationProvider
那么多,找哪一个呢,肯定是根据不同的Authentication
找不同的AuthenticationProvider
,即调用每个AuthenticationProvider
的supports
方法,找到对应的AuthenticationProvider
后就交给对应AuthenticationProvider
。AuthenticationProvider
拿到对应的Authentication
对象后,就进行具体的认证流程:找UserDetailsService
拿到事先存储的用户信息和提交的Authentication
中封装的用户信息进行比对,认证成功后,就重新生成一个认证通过后的Authentication
对象,此时的Authentication
对象中具有用户的权限信息,所以,整个认证流程的核心就是AuthenticationManager
和AuthenticationProvider
,两者之间的关系可参阅官方文档。
2.2 授权
上面讲完了认证流程,接下来讲一下授权流程,FilterSecurityInterceptor
的大致授权流程如下图所示。
AbstractSecurityInterceptor
:对用户的请求进行授权的抽象类,负责从SecurityContextHolder
中获取Authentication
,以及获取ConfigAttribute
。FilterSecurityInterceptor
:AbstractSecurityInterceptor
的实现类,负责拦截请求,将ServletRequest
,ServletResponse
,FilterChain
封装为FilterInvocation
。FilterInvocation
:保证请求和响应是HttpServletRequest
和HttpServletResponse
的实例,供后面的授权使用。ConfigAttribute
:封装了用户要访问接口所需的权限信息。AccessDecisionManager
:根据AccessDecisionVoter
的投票结果判断是否允许用户访问指定的接口。AffirmativeBased
:AccessDecisionManager
的简单实现,通过轮询其管理的所有AccessDecisionVoter
,只要有一个AccessDecisionVoter
投赞成票或反对票,本次就允许或禁止用户访问接口。AccessDecisionVoter
:对用户的本次访问进行投票,最终的投票结果由AccessDecisionVoter
收集,并做出最终的访问决策。WebExpressionVoter
:AccessDecisionVoter
的实现类,基于SpEL
(Spring 表达式语言)的AccessDecisionVoter
。
2.2.1 FilterSecurityInterceptor
FilterSecurityInterceptor
作为过滤器链中最后一个Filter
,起着至关重要的作用,决定着本次请求能否访问目标资源,接下来就来看看FilterSecurityInterceptor
是如何授权的。
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
// 用于标识每个请求进行过授权
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
// 用于确定请求的安全属性,包括可访问的URL、所需的权限、角色等,用于后续的访问决策和权限验证
private FilterInvocationSecurityMetadataSource securityMetadataSource;
// 标识每个请求是否只进行一次授权
private boolean observeOncePerRequest = true;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将request、response、chain封装为FilterInvocation,保证当前请求是HTTP请求
invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// 判断当前请求是否已经授权过,防止重复检查
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 开始授权
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
private boolean isApplied(FilterInvocation filterInvocation) {
return (filterInvocation.getRequest() != null)
&& (filterInvocation.getRequest().getAttribute(FILTER_APPLIED) != null);
}
public boolean isObserveOncePerRequest() {
return this.observeOncePerRequest;
}
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return this.securityMetadataSource;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
}
从上面可以看出,FilterSecurityInterceptor
只是做了一些准备工作和善后工作,真正的授权流程在其父类的beforeInvocation
方法中,下面就来看看父类是如何进行授权的。
public abstract class AbstractSecurityInterceptor
implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
protected final Log logger = LogFactory.getLog(getClass());
/**
* 决定是否授权用户访问目标资源
*/
private AccessDecisionManager accessDecisionManager;
/**
* 是否拒绝公共请求
*/
private boolean rejectPublicInvocations = false;
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
// 1.判断当前传入对象的类型是否为FilterInvocation
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
// 2.根据传入的对象获取要访问资源所需的权限或角色信息
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (CollectionUtils.isEmpty(attributes)) {
// 3.若未获取要访问资源所需的权限或角色信息,认为当前的请求为公共请求,若拒绝策略为true,则抛出异常,即当前请求需要配置权限信息
Assert.isTrue(!this.rejectPublicInvocations,
() -> "Secure object invocation " + object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized public object %s", object));
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"), object, attributes);
}
// 4.判断当前用户是否已经经过身份认证
Authentication authenticated = authenticateIfRequired();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
}
// 5、开始授权
attemptAuthorization(object, attributes, authenticated);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
}
if (this.publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs != null) {
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
newCtx.setAuthentication(runAs);
SecurityContextHolder.setContext(newCtx);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
}
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
/**
* 授权方法,委托AccessDecisionManager进行授权
* @param object FilterInvocation
* @param attributes 要访问资源所需的权限或角色信息
* @param authenticated 经过身份认证的Authentication对象,里面包含用户具有的权限信息
*/
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
attributes, this.accessDecisionManager));
}
else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
}
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
throw ex;
}
}
/**
* 判断当前用户是否已经经过身份认证,若未经过身份认证,再用AuthenticationManager认证一遍
* @return 经过身份认证的Authentication对象
*/
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
}
return authentication;
}
authentication = this.authenticationManager.authenticate(authentication);
// Don't authenticated.setAuthentication(true) because each provider does that
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
}
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
return authentication;
}
}
从上面的源码可以看出,FilterSecurityInterceptor
和前面的UsernamePasswordAuthenticationFilter
一样,都是先做一些准备工作,最后再把具体的认证逻辑或授权逻辑交给具体的AuthenticationManager
或AccessDecisionManager
,最后再做一些善后处理工作。
2.2.2 AffirmativeBased
在默认情况下中,AccessDecisionManager
的默认实现类是AffirmativeBased
,接下来就来看一下AffirmativeBased
是如何进行授权的。
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}
/**
* 简单地轮询所有配置的AccessDecisionVoter,并在任何AccessDecisionVoter投了肯定票时授予访问权。拒绝访问只有当有拒绝投票和没有肯定的投票。
* 如果每个AccessDecisionVoter都放弃投票,则决定将基于isAllowIfAllAbstainDecisions()属性(默认为false)
* @param authentication 经过身份认证的Authentication对象,里面包含用户具有的权限信息
* @param object FilterInvocation
* @param configAttributes 要访问资源所需的权限或角色信息
* @throws AccessDeniedException 拒绝访问抛出异常
*/
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}
从上面的源码可以看出,AffirmativeBased
只是简单的轮询每个AccessDecisionVoter
,根据它们的投票结果做出访问决策,只要其中有一个AccessDecisionVoter
投了赞成票时就允许访问,反之,只要有一个投反对票就不允许访问。
2.2.3 WebExpressionVoter
在Spring Security
中,AccessDecisionVoter
最常见的实现类是WebExpressionVoter
,这是一种根据SpEL表达式来做出投票结果的AccessDecisionVoter
,关于SpEL表达式,大家感兴趣的话可以参阅官方文档,当然,还有其它AccessDecisionVoter
,大家可以自行研究。
2.2.4 总结
从前面的流程图和分析不难看出,整个授权流程的核心其实是AccessDecisionManager
和AccessDecisionVoter
,AccessDecisionManager
管理着一系列AccessDecisionVoter
,并根据每一个AccessDecisionVoter
的投票结果做出最终决策。每一个AccessDecisionVoter
通过用户所具有的权限信息(前面的认证流程已经将用户的权限信息存入Authentication
)和待访问接口所需的权限信息(ConfigAttribute
)进行比较,做出自己的投票结果。不难看出,其实AccessDecisionManager
和AccessDecisionVoter
的关系有点类似与前面认证流程中AuthenticationManager
和AuthenticationProvider
的关系。
2.3 异常
当然,前面所讲述的只是正常情况下认证和授权流程,那么当认证和授权过程中出现异常,Spring Security
又是如何处理的呢?下面就来看一下Spring Security
中的异常处理机制。前面已经讲到过,UsernamePasswordAuthenticationFilter
在认证过程中如果出现异常会调用unsuccessfulAuthentication
方法,将异常交给AuthenticationFailureHandler
进行处理,那么,如果当前请求不需要认证,在整个过滤器链中执行的过程中如果出现异常,Spring Security
是如何进行处理的呢?接下来请看另一个Filter
:ExceptionTranslationFilter
,这个过滤器被用来处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。下面就来看看这个过滤器是如何处理异常的。
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
/**
* 用于处理AccessDeniedException
*/
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
/**
* 用于处理AuthenticationException
*/
private AuthenticationEntryPoint authenticationEntryPoint;
/**
* 用户判断当前用户是不是匿名用户或已经记住的用户
*/
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
/***
* 分析当前的异常中是否包含AccessDeniedException或AuthenticationException
*/
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private RequestCache requestCache = new HttpSessionRequestCache();
@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 {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
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) {
// 当前异常信息中既不包括AuthenticationException,也不包括AccessDeniedException
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);
}
}
private void rethrow(Exception ex) throws ServletException {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
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);
}
}
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);
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
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);
}
// 交给对应的AccessDeniedHandler处理AccessDeniedException
this.accessDeniedHandler.handle(request, response, exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
// 交给对应的AuthenticationEntryPoint处理AuthenticationException
this.authenticationEntryPoint.commence(request, response, reason);
}
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
@Override
protected void initExtractorMap() {
super.initExtractorMap();
registerExtractor(ServletException.class, (throwable) -> {
ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
return ((ServletException) throwable).getRootCause();
});
}
}
}
通过上面的源码分析可以看出,ExceptionTranslationFilter
把过滤器链中出现的AuthenticationException
交给了AuthenticationEntryPoint
处理,AccessDeniedException
交给了AccessDeniedHandler
处理。无论是UsernamePasswordAuthenticationFilter
中的AuthenticationFailureHandler
,还是这里的AuthenticationEntryPoint
和AccessDeniedHandler
,都是一个接口,我们都可以自定义其实现类,自定义异常处理逻辑。
2.4 SecurityContextHolder
从前面的分析可以看出,无论是前面的认证和授权,还是异常处理,都或多或少用到了SecurityContextHolder
,前面说过SecurityContextHolder
存储着SecurityContext
,而SecurityContext
中存储着Authentication
,Authentication
中又存储着用户的相关信息,那么接下来就来看一下SecurityContextHolder
是如何存储SecurityContext
的。这里就不贴源码了,直接给出结论,SecurityContextHolder
中默认存储策略是ThreadLocalSecurityContextHolderStrategy
,就是将SecurityContext
存储在ThreadLocal
,方便当前线程在任何地方取出Authentication
使用。
前面在讲解UsernamePasswordAuthenticationFilter
时说过,认证成功后的Authentication
会被存入SecurityContextHolder
,那么本次请求后面的所有Filter
都可以获取到认证成功后的用户信息。认证成功后,后面的请求都不需要认证了,那么UsernamePasswordAuthenticationFilter
就不会将Authentication
存入SecurityContextHolder
,此时,后面的Filter
是如何从SecurityContextHolder
获取Authentication
的呢?Authentication
又是何时被存入SecurityContextHolder
的呢?接下来就得说一下另一个非常重要的Filter
:SecurityContextPersistenceFilter
。
public class SecurityContextPersistenceFilter extends GenericFilterBean {
// 标识当前Filter已经处理过该请求
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
// 请求之间持久化SecurityContext的策略
private SecurityContextRepository repo;
// 是否强制创建session
private boolean forceEagerSessionCreation = false;
public SecurityContextPersistenceFilter() {
// 初始化repo,默认情况下为基于HttpSession的存储策略
this(new HttpSessionSecurityContextRepository());
}
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
@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 {
// ensure that filter is only applied once per request
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 从session中获取SecurityContext
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
// 将SecurityContext存入SecurityContextHolder
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
// 在整个过滤器链执行完成后,重新将SecurityContext存入session,此时的SecurityContext可能存储的是认证成功后的用户信息
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
this.forceEagerSessionCreation = forceEagerSessionCreation;
}
}
从上面的源码可以看出,每次请求SecurityContextPersistenceFilter
都会从session
中取出SecurityContext
存入SecurityContextHolder
,在整个过滤器执行完成后又从SecurityContextHolder
中取出SecurityContext
,再将SecurityContext
存入session
,那样,认证完成后的Authentication
都会被存入session
中,后面每次访问再重新取出SecurityContext
存入SecurityContextHolder
,后面授权的时候就可以取出用户信息。至于SecurityContextRepository
是如何将SecurityContext
存入session
的,又是如何从session
中取出SecurityContext
的,大家可以自行去了解。
3. 过滤器链SecurityFilterChain
前面讲完了Spring Security
的默认认证和授权流程,现在就来填一下前面的坑,讨论一下过滤器链是如何产生的,以及过滤器链中一些重要过滤器的作用。我们都知道Spring Boot
的厉害之处,就在于它的自动装配,那么这过滤器链不可能凭空产生,肯定是由Spring Boot
自动装配产生的。首先,我们找到自动装配的配置文件spring.factories
,如下图所示。
在spring.factories
中找到名为SecurityAutoConfiguration
的配置类,这就是Spring Security
的自动配置类。
我们可以看到这个自动配置类生效的条件是类加载路径下存在名为DefaultAuthenticationEventPublisher
的类,这个毋庸置疑,肯定是成立的,当我们引入依赖的那一刻,这个类就存在了。下面我们重点关注@Import
导入的两个配置类,SpringBootWebSecurityConfiguration
和WebSecurityEnablerConfiguration
,这两个类至关重要。
3.1 SpringBootWebSecurityConfiguration
首先,我们看到这个名为SpringBootWebSecurityConfiguration
的配置类,如下图所示。
我们看到这个配置类的注释,其大概意思是说这个配置类是web安全的默认配置,如果用户配置了自己的WebSecurityConfigurerAdapter
或SecurityFilterChain
bean,这个配置类就不会生效,那么这解释了为什么我们集成Spring Security
时要自己写一个类继承WebSecurityConfigurerAdapter
作为Spring Security
的配置类。我们可以看到在这个配置类中只有一个名为defaultSecurityFilterChain
的bean,这个bean的类型就是SecurityFilterChain
,是不是很熟悉?没错,这就是Spring Security
为我们配置的默认FilterChain
,这也是我们前面为什么会用那段代码获取所有的Filter
。这时可能有人就会问了,前面的代码不是DefaultSecurityFilterChain
吗?事实上,只要我们进入http.build()
一探究竟,就会发现DefaultSecurityFilterChain
就是SecurityFilterChain
的实现类,而这个方法所需要的参数是一个HttpSecurity
对象,而这个参数会由其它配置类产生自动注入到这里。
我们可以看到这个配置类生效的条件是使用默认的WebSecurity
配置,且当前的应用程序是一个web应用程序,我们进入@ConditionalOnDefaultWebSecurity
,如下图所示。
然后我们进入DefaultWebSecurityCondition
,如下图所示。
我们可以看到SpringBootWebSecurityConfiguration
生效的条件就在这个类里面,即类加载路径下有SecurityFilterChain
和HttpSecurity
这两个类,且容器中不存在类型为WebSecurityConfigurerAdapter
和SecurityFilterChain
的bean,我们在集成Spring Security
时,什么都没配置,容器中肯定是不会存在这两个bean的。
3.2 WebSecurityEnablerConfiguration
接下来,看到名为WebSecurityEnablerConfiguration
的配置类,如下图所示。
首先看到这个配置类的注释,其大概意思是说,这个配置类的目的是确保引入注解@EnableWebSecurity
,如果用户自己添加了该注解或者容器中已经存在名为springSecurityFilterChain
的bean,那么这个配置类就不会生效。一句话,这个配置类的目的就是引入注解@EnableWebSecurity
,所以,这个注解才是关键。
接下来,我们进入@EnableWebSecurity
,如下图所示。
我们重点关注@Import
导入的两个配置类,WebSecurityConfiguration
和HttpSecurityConfiguration
,这两个类至关重要。
3.2.1 WebSecurityConfiguration
WebSecurityConfiguration
,顾名思义,就是WebSecurity
的配置类,至于这个WebSecurity
是什么?有什么作用?我们后面再做详细讨论。首先我们看到WebSecurityConfiguration
的注释,如下图所示。
这个注释的大概意思是说这个配置类的作用是使用WebSecurity
创建一个FilterChainProxy
,而FilterChainProxy
与Web安全至关重要,至于这个FilterChainProxy
是什么?有什么作用?这个我们后面再做详细讨论,现在只需知道前面的SecurityFilterChain
是由它管理的。注释后面还说我们可以通过继承WebSecurityConfigurerAdapter
或是实现WebSecurityConfigurer
的方式对WebSecurity
进行个性化配置,欸!这里又提到了WebSecurityConfigurerAdapter
,说明WebSecurityConfigurerAdapter
在Spring Security
中如此重要,至于它和WebSecurity
是什么关系,我们后面再做详细讨论。
我们看到创建FilterChainProxy
的方法,如下图所示。
首先看到方法的注释,方法的注释说这个方法返回的是一个代表SecurityFilterChain
的Filter
,哦!原来FilterChainProxy
也是一个Filter
呀!而且它还管理着SecurityFilterChain
。再看这个方法产生的Bean
的名字是springSecurityFilterChain
,也就是到时可以通过这个名字获取FilterChainProxy
。再看这个方法的前面三句,前面有个断言,大概意思是说,WebSecurityConfigurerAdapter
和SecurityFilterChain
只能存在其一,而这个SecurityFilterChain
可由前面的SpringBootWebSecurityConfiguration
产生,在这个方法下面就有一个set
方法将容器中的所有SecurityFilterChain
注入到这个配置类中,为什么这两个只能存在一个呢?这两者之间又是什么关系呢?这个我们后文再做讨论。继续往后看,如果这两个都没有的话,Spring Security
还贴心的为我们添加了一个WebSecurityConfigurerAdapter
,将WebSecurityConfigurerAdapter
添加到WebSecurity
,如果存在SecurityFilterChain
的话,还将为WebSecurity
添加一个SecurityFilterChainBuilder
,然后遍历所有的WebSecurityCustomizer
对WebSecurity
进行个性化配置,最后调用WebSecurity
的build
方法来创建FilterChainProxy
。看来WebSecurity
创建FilterChainProxy
需要一个WebSecurityConfigurerAdapter
或SecurityFilterChain
,那么这个WebSecurity
从何而来,接下来看到这个配置类的另一个方法setFilterChainProxySecurityConfigurer
,如下图所示。
我们可以看到这个方法需要两个参数:ObjectPostProcessor
和List<SecurityConfigurer<Filter, WebSecurity>>
。首先说一下这两个参数从何而来,还记得前面的注解@EnableWebSecurity
吗?ObjectPostProcessor
就是由@EnableWebSecurity
上面另一个注解@EnableGlobalAuthentication
产生的,至于这个ObjectPostProcessor
的具体作用,现在我们不去过多深究,只需要知道它会对创建的对象做一些增强的逻辑;再看到另一个参数,这个参数需要容器中所有SecurityConfigurer<Filter, WebSecurity>
的实现类(实际上前面的注解限制了为SecurityConfigurer
的子接口WebSecurityConfigurer
的实现类),而WebSecurityConfigurerAdapter
就是其中的一个实现类。这个方法的主要作用就是创建WebSecurity
,然后获取容器中的所有WebSecurityConfigurer
,为其configurers
赋值(即调用WebSecurity
的apply
方法)。
3.2.2 HttpSecurityConfiguration
接下来看到HttpSecurityConfiguration
,首先看到这个配置类的注释,如下图所示。
从注释可以看出,这个配置类的主要作用就是创建HttpSecurity
,这个HttpSecurity
是什么,有什么作用,相信配置过WebSecurityConfigurerAdapter
的并不陌生,这个HttpSecurity
有什么作用?与WebSecurity
又有什么关系?我们后面再做详细讨论,下面看到创建HttpSecurity
的方法。
注意看,这个Bean是多例的,并不是单例的,也就是说这个Bean并不会提前产生,只会在需要用到的时候产生。还记得前面的SpringBootWebSecurityConfiguration
吗?那里就需要一个HttpSecurity
,这里就是Spring Security
做的一系列默认配置,可以看到已经添加了两个Filter
,其它的都使用了默认的配置。
3.3 FilterChainProxy
说到FilterChainProxy
,就不得不提到Spring Security
的整体架构,FilterChainProxy
在Spring Security
中扮演者极其重要的角色,具体可以参阅Spring Security
的官方文档。
首先来看下面这张图。
官网对这张图的解释是:
Spring provides a Filter
implementation named DelegatingFilterProxy
that allows bridging between the Servlet container’s lifecycle and Spring’s ApplicationContext
. The Servlet container allows registering Filter
s using its own standards, but it is not aware of Spring defined Beans. DelegatingFilterProxy
can be registered via standard Servlet container mechanisms, but delegate all the work to a Spring Bean that implements Filter
.
我们都知道服务器在启动后会在Servlet
容器中注册一系列Filter
在请求到达服务器时,会经过这些Filter
,而Servlet
容器并不知道Spring Security
中的Filter
,因为这些Filter
都在Spring
的ApplicationContext
中,为了能让Spring Security
中的Filter
处理请求,就设计了一个DelegatingFilterProxy
,将其注册到Servlet
容器中,负责拦截请求,并将所有的工作委派给Spring Security
中的Filter
,但是Spring Security
中的Filter
数量众多,这时就需要一个FilterChain
,即前面SpringBootWebSecurityConfiguration
创建的SecurityFilterChain
,而SecurityFilterChain
就是由HttpSecurity
创建的。这是就需要引出我们的主角了:FilterChainProxy
,咦!怎么是FilterChainProxy
?不是SecurityFilterChain
吗?别急,看下面这两张图.。
从图中可以看出DelegatingFilterProxy
中的delegate
就是FilterChainProxy
,而FilterChainProxy
中管理着一系列SecurityFilterChain
,SecurityFilterChain
中又管理着一系列Filter
,这些Filter
正是Spring
的ApplicationContext
中的Filter
,为什么FilterChainProxy
要管理着众多SecurityFilterChain
呢?看右边的图就知道了,不同SecurityFilterChain
针对不同的路径,也就是说我们可以在项目中根据不同的路径配置不同的SecurityFilterChain
,不同的路径具有不同的校验规则,而FilterChainProxy
会根据不同的路径选取不同的SecurityFilterChain
。
3.4 总结
相信前面讲了那么多,大家都已经绕晕了吧,别急,我们先来捋捋这个过程。
首先,既然我们要实现认证和授权,那就需要Filter
,而这些一个Filter
又无法搞定,需要很多的Filter
,不同的Filter
各司其职,共同完成认证和授权,保护我们的Web应用程序。那么,如何让这些Filter
协调起来,有效工作呢?没错,此时就需要FilterChain
(即SecurityFilterChain)。但是,我们如果想要更加精细化的配置,让不同的路径具有的校验规则,怎么办呢?这时就需要多个SecurityFilterChain
,不同的SecurityFilterChain
针对不同的路径。
其次,当请求到达服务器时,怎么多的SecurityFilterChain
,该用哪一个呢,那肯定的做出决策呀!这时,就需要FilterChainProxy,让它来管理众多的SecurityFilterChain
,根据不同的请求路径使用不同的SecurityFilterChain
。
最后,我们有这么多Filter
,有用吗?没用啊!为什么没用?因为Servlet
容器不知道啊,那么怎样才能让Servlet
容器知道呢?那就弄个DelegatingFilterProxy,将它注册到Servlet
容器,让它将请求传递给我们的FilterChainProxy
,这样我们创建的那些Filter
就可以正常工作了。
那么,,Spring Security
整这么麻烦干什么呢?直接将那些Filter
注册到Servlet
容器不更香吗?这个问题就留给各位自己思考了。
4. HttpSecurity和WebSecurity
前面已经介绍完了Spring Security
的整体架构,想必大家都已经知道了Spring Security
的两大核心:FilterChainProxy
和SecurityFilterChain
。下面就来介绍一下这两大核心的缔造者:WebSecurity
和HttpSecurity
。
首先来回顾一下WebSecurity
和HttpSecurity
是如何创建FilterChainProxy
和SecurityFilterChain
。前面已经讲到到过WebSecurity
和HttpSecurity
都是经过一系列配置后,最后调用build
方法创建FilterChainProxy
和SecurityFilterChain
,那么调用build
方法都发生了什么?FilterChainProxy
和SecurityFilterChain
是如何被创建出来的?下面我们就来一探究竟。
先来看一下这两个类的类继承关系图,如下图所示。
我们可以看到这两个类都有一个相同的类继承关系:SecurityBuilder
–>AbstractSecurityBuilder
–>AbstractConfiguredSecurityBuilder
。无论是WebSecurity
,还是HttpSecurity
,其实都是一个SecurityBuilder
,这个接口中只有一个build
方法,通过调用build
方法构建我们想要的对象,这其实就是设计模式中建造者模式在Spring Security
中的具体体现。当我们调用build
方法,其调用流程如下图所示。
首先看到AbstractConfiguredSecurityBuilder
的doBuild
方法,如下图所示。
我们可以看到beforeInit
、beforeConfigure
、performBuild
方法,AbstractConfiguredSecurityBuilder
都未对其进行实现,而是由子类自行实现,那么子类就可以在init
和configure
之前,进行自己的个性化配置。在一切准备工作完成后,最后,再调用子类的performBuild
方法,构建具体的对象。我们再看到另外两个方法:init
和configure
。
可以看到这两个方法都是调用SecurityConfigurer
的init
和configure
,对当前的SecurityBuilder
进行一系列配置操作,那么SecurityBuilder
在构建对象的过程中,重点研究对象便是这些SecurityConfigurer
(init
方法和configure
方法都干了什么)和留给子类实现的三个方法。我们看到接口SecurityConfigurer
,如下图所示。
我们可以看见这个这个SecurityConfigurer
是一个泛型接口,这个接口中定义了两个方法来对SecurityBuilder
做一些必要的配置,至于这些重要的配置是什么,我们后文再做详细讨论。这个接口的泛型参数O
代表SecurityBuilder
要构建的目标对象,而泛型参数B
代表构建目标对象的SecurityBuilder
,我们可以通过这两个参数清晰的知道当前SecurityConfigurer
是配置的哪一个SecurityBuilder
,以及这个SecurityBuilder
构建的目标对象是什么。
4.1 HttpSecurity
首先我们看到HttpSecurity
,如下图所示。
我们看到HttpSecurity
的定义,继承了AbstractConfiguredSecurityBuilder
(其中的泛型参数和SecurityConfigurer
一样),而其中的泛型参数O
为DefaultSecurityFilterChain
,泛型参数B
为HttpSecurity
,也就是说HttpSecurity
是一个构建DefaultSecurityFilterChain
的SecurityBuilder
。
4.1.1 默认配置
在HttpSecurity
的源码当中,HttpSecurity
并未实现beforeInit
,只是实现了beforeConfigure
、performBuild
,下面我们就来看看这两个方法里面到底发生了什么,以及在默认情况下HttpSecurity
中有哪些SecurityConfigurer
。
要研究HttpSecurity
中的SecurityConfigurer
,就得知道这些SecurityConfigurer
从何而来,还记得前面讲解HttpSecurityConfiguration
中那段默认配置的代码和SpringBootWebSecurityConfiguration
中创建DefaultSecurityFilterChain
那段代码吗?Spring Security
在这两处为HttpSecurity
设置了默认的SecurityConfigurer
,下面就来具体看这两处代码。
// HttpSecurityConfiguration
http
.csrf(withDefaults()) // 1.添加CsrfConfigurer,并采用默认配置
.addFilter(new WebAsyncManagerIntegrationFilter()) // 直接添加filter
.exceptionHandling(withDefaults()) // 2.添加ExceptionHandlingConfigurer,并采用默认配置
.headers(withDefaults()) // 3.添加HeadersConfigurer,并采用默认配置
.sessionManagement(withDefaults()) // 4.添加SessionManagementConfigurer,并采用默认配置
.securityContext(withDefaults()) // 5.添加SecurityContextConfigurer,并采用默认配置
.requestCache(withDefaults()) // 6.添加RequestCacheConfigurer,并采用默认配置
.anonymous(withDefaults()) // 7.添加AnonymousConfigurer,并采用默认配置
.servletApi(withDefaults()) // 8.添加ServletApiConfigurer,并采用默认配置
.apply(new DefaultLoginPageConfigurer<>()); // 9.添加DefaultLoginPageConfigurer,并采用默认配置
http.logout(withDefaults()); // 10.添加LogoutConfigurer,并采用默认配置
// SpringBootWebSecurityConfiguration
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 11.添加ExpressionUrlAuthorizationConfigurer
.anyRequest() // ExpressionUrlAuthorizationConfigurer的配置信息
.authenticated() // ExpressionUrlAuthorizationConfigurer的配置信息
.and() // 返回当前的SecurityBuilder,即HttpSecurity
.formLogin() // 12.添加FormLoginConfigurer,并采用默认配置
.and() // 返回当前的SecurityBuilder,即HttpSecurity
.httpBasic(); // 13.添加HttpBasicConfigurer,并采用默认配置
// 构建DefaultSecurityFilterChain
return http.build();
}
从上面的代码可以看出,在默认情况下,Spring Security
为我们添加了13个SecurityConfigurer
和一个Filter
,那么这些SecurityConfigurer
是如何添加到HttpSecurity
当中的呢?我们可以发现无论是HttpSecurityConfiguration
中的配置方式,还是SpringBootWebSecurityConfiguration
中的配置方式,最终都是经过了getOrApply
和apply
这两个方法。
public HttpSecurity csrf(Customizer<CsrfConfigurer<HttpSecurity>> csrfCustomizer) throws Exception {
ApplicationContext context = getContext();
// 使用传入的Customizer个性化配置当前的SecurityConfigurer
csrfCustomizer.customize(getOrApply(new CsrfConfigurer<>(context)));
return HttpSecurity.this;
}
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
ApplicationContext context = getContext();
return getOrApply(new CsrfConfigurer<>(context));
}
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer)
throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
configurer.addObjectPostProcessor(this.objectPostProcessor);
// 告知当前SecurityConfigurer要配置的SecurityBuilder是谁
configurer.setBuilder((B) this);
add(configurer);
return configurer;
}
从上面可以看出,我们有两种配置方式可以个性化配置当前的SecurityConfigurer
:一是像HttpSecurityConfiguration
中那样传入一个Customizer
,二是像SpringBootWebSecurityConfiguration
中那样先添加对应的SecurityConfigurer
,然后再调用SecurityConfigurer
对应的方法进行个性化配置,若要想配置另外的SecurityConfigurer
,可以调用当前配置SecurityConfigurer
的and
方法获得要配置的SecurityBuilder
,再配置另外的SecurityConfigurer
。
前面说完了这些SecurityConfigurer
,那么这些SecurityConfigurer
是拿来干什么的呢,其实大家可能已经猜出来了,这些SecurityConfigurer
的数量和前面Spring Security
为我们默认配置的Filter
数量非常接近,没错,这些SecurityConfigurer
就是用来配置HttpSecurity
中的每一个Filter
的,我们挑选一个SecurityConfigurer
,例如DefaultLoginPageConfigurer
,查看其源码,如下所示。
public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<DefaultLoginPageConfigurer<H>, H> {
private DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = new DefaultLoginPageGeneratingFilter();
private DefaultLogoutPageGeneratingFilter logoutPageGeneratingFilter = new DefaultLogoutPageGeneratingFilter();
@Override
public void init(H http) {
// 对DefaultLoginPageGeneratingFilter进行配置
this.loginPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
// 对DefaultLogoutPageGeneratingFilter进行配置
this.logoutPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
http.setSharedObject(DefaultLoginPageGeneratingFilter.class, this.loginPageGeneratingFilter);
}
private Map<String, String> hiddenInputs(HttpServletRequest request) {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
return (token != null) ? Collections.singletonMap(token.getParameterName(), token.getToken())
: Collections.emptyMap();
}
@Override
@SuppressWarnings("unchecked")
public void configure(H http) {
AuthenticationEntryPoint authenticationEntryPoint = null;
ExceptionHandlingConfigurer<?> exceptionConf = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionConf != null) {
authenticationEntryPoint = exceptionConf.getAuthenticationEntryPoint();
}
if (this.loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) {
this.loginPageGeneratingFilter = postProcess(this.loginPageGeneratingFilter);
// 添加DefaultLoginPageGeneratingFilter
http.addFilter(this.loginPageGeneratingFilter);
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null) {
// 添加DefaultLogoutPageGeneratingFilter
http.addFilter(this.logoutPageGeneratingFilter);
}
}
}
}
从上面的源码可以看出,DefaultLoginPageConfigurer
在init
方法中对其中的两个Filter
进行了配置,最后在configure
方法中将这两个Filter
添加到了HttpSecurity
(在添加时会将Filter
封装为HttpSecurity
内部的OrderedFilter
,方便排序)中,为后面创建DefaultSecurityFilterChain
方法做准备。至于这些SecurityConfigurer
都配置了哪些Filter
,是如何配置的,大家可以自行去研究。
介绍完了HttpSecurity
中的SecurityConfigurer
,下面就来看看HttpSecurity
中performBuild
到底是如何创建DefaultSecurityFilterChain
的。
@Override
protected DefaultSecurityFilterChain performBuild() {
this.filters.sort(OrderComparator.INSTANCE);
List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
for (Filter filter : this.filters) {
sortedFilters.add(((OrderedFilter) filter).filter);
}
return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}
其实这个方法很简单,就是对之前SecurityConfigurer
添加的所有Filter
进行排序后,最后再创建DefaultSecurityFilterChain
。
4.1.2 继承WebSecurityConfigurerAdapter后
当我们继承WebSecurityConfigurerAdapter
后,前面的配置类SpringBootWebSecurityConfiguration
将会失效,因此Spring Security
将不会为我们产生默认的SecurityFilterChain
,而是采用我们在WebSecurityConfigurerAdapter
中configure
方法的配置信息创建SecurityFilterChain
,因此前面的代码将无法获取Spring Security
配置的所有Filter
,但是可以通过如下代码获取。
@SpringBootApplication
public class SpringSecurityApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringSecurityApplication.class, args);
FilterChainProxy filterChainProxy = applicationContext.getBean(FilterChainProxy.class);
List<SecurityFilterChain> filterChains = filterChainProxy.getFilterChains();
for (SecurityFilterChain filterChain : filterChains) {
List<Filter> filters = filterChain.getFilters();
for (int i = 0; i < filters.size(); i++) {
System.out.println((i + 1) + ": " + filters.get(i));
}
}
}
}
既然Spring Security
不会为我们产生默认的SecurityFilterChain
,那么Spring Security
是如何创建SecurityFilterChain
的?具体详见4.2.2。
4.2 WebSecurity
首先我们看到WebSecurity
,如下图所示。
首先看到注释,其大概意思是说WebSecurityConfiguration
将创建WebSecurity
,然后利用WebSecurity
创建FilterChainProxy
。后面还说了如果要对WebSecurity
进行个性化配置,我们可以创建WebSecurityConfigurer
或WebSecurityCustomizer
,或者继承WebSecurityConfigurerAdapter
,当然更多的情况是继承WebSecurityConfigurerAdapter
(毕竟继承抽象类肯定比自己实现接口要方便得多)。当然,这些内容在前面讲解WebSecurityConfiguration
时都已经提及,在Spring Boot
启动过程中,WebSecurityConfiguration
会获取容器中所有的WebSecurityConfigurer
(WebSecurityConfigurerAdapter
是其实现类)、WebSecurityCustomizer
对创建的WebSecurity
进行个性化配置,然后利用WebSecurity
创建FilterChainProxy
。
我们再看到WebSecurity
的定义,继承了AbstractConfiguredSecurityBuilder
(其中的泛型参数和SecurityConfigurer
一样),而其中的泛型参数O
为Filter
,泛型参数B
为WebSecurity
,也就是说当前的SecurityBuilder
是WebSecurity
,其构建的目标对象是Filter
,而FilterChainProxy
就是一个Filter
(前面讲解WebSecurityConfiguration
时已经提及)。
4.2.1 默认配置
在WebSecurity
的源码当中,WebSecurity
并未实现beforeInit
、beforeConfigure
,只是实现了performBuild
。在前面已经说过,SecurityBuilder
在构建对象的过程中,重点研究对象是这些SecurityConfigurer
和最后构建目标对象的performBuild
方法,默认情况下,容器中并没有WebSecurityConfigurer
(SecurityConfigurer
的子接口)的实现类,因此WebSecurity
中并没有SecurityConfigurer
,所以最后就剩下了performBuild
方法,如下所示。
protected Filter performBuild() throws Exception {
/*
断言:securityFilterChainBuilders不能为空
*/
Assert.state(!this.securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this is done by exposing a SecurityFilterChain bean "
+ "or by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke " + WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
for (RequestMatcher ignoredRequest : this.ignoredRequests) {
WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
+ ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
// 遍历所有的securityFilterChainBuilders,构建SecurityFilterChain
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}
if (this.privilegeEvaluator == null) {
this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
requestMatcherPrivilegeEvaluatorsEntries);
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (this.httpFirewall != null) {
filterChainProxy.setFirewall(this.httpFirewall);
}
if (this.requestRejectedHandler != null) {
filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
if (this.debugEnabled) {
this.logger.warn("\n\n" + "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
this.postBuildAction.run();
return result;
}
从上面的代码可以看出,WebSecurity
就是遍历所有的securityFilterChainBuilder
(即构建SecurityFilterChain
的SecurityBuilder
,这些securityFilterChainBuilder
在WebSecurityConfiguration
已经添加),调用每一个securityFilterChainBuilder
的build
方法创建SecurityFilterChain
,将这些SecurityFilterChain
放入集合中,最后创建FilterChainProxy
。
4.2.2 继承WebSecurityConfigurerAdapter后
当我们继承WebSecurityConfigurerAdapter
之后,容器中就存在了WebSecurityConfigurer
(SecurityConfigurer
的子接口)的实现类,WebSecurityConfiguration
将不会向WebSecurity
中添加securityFilterChainBuilder
,那么securityFilterChainBuilder
又是从何处添加的呢?我们继续往下看。
既然WebSecurity
中有了WebSecurityConfigurer
,那么重点就在WebSecurityConfigurerAdapter
的init
方法和configure
方法中,如下所示。
@Override
public void init(WebSecurity web) throws Exception {
HttpSecurity http = getHttp();
/**
向WebSecurity中添加securityFilterChainBuilder(能构建SecurityFilterChain的SecurityBuilder),即HttpSecurity
这段代码和WebSecurityConfiguration中添加securityFilterChainBuilder的那段代码类似
*/
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
// 我们可以再子类中重写此方法,配置WebSecurity,更多的情况下是重写另一个configure方法,配置HttpSecurity
@Override
public void configure(WebSecurity web) throws Exception {
}
protected final HttpSecurity getHttp() throws Exception {
if (this.http != null) {
return this.http;
}
AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = authenticationManager();
this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
// 创建HttpSecurity
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
if (!this.disableDefaults) {
// 如果没有禁用默认配置,将采用默认配置
applyDefaultConfiguration(this.http);
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader
.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
this.http.apply(configurer);
}
}
// 将创建好的HttpSecurity传递给我们重写的configure方法,让我们对HttpSecurity进行个性化配置
configure(this.http);
return this.http;
}
/**
子类一般需要重写此方法,对HttpSecurity进行个性化配置
这段代码里面的配置与配置类SpringBootWebSecurityConfiguration中的配置一模一样
*/
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). "
+ "If subclassed this will potentially override subclass configure(HttpSecurity).");
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin();
http.httpBasic();
}
/**
默认配置:与配置类HttpSecurityConfiguration中的配置一模一样,都是向HttpSecurity中添加SecurityConfigurer
*/
private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
http.csrf();
http.addFilter(new WebAsyncManagerIntegrationFilter());
http.exceptionHandling();
http.headers();
http.sessionManagement();
http.securityContext();
http.requestCache();
http.anonymous();
http.servletApi();
http.apply(new DefaultLoginPageConfigurer<>());
http.logout();
}
从上面的源码可以看出,当WebSecurityConfiguration
不向WebSecurity
中添加securityFilterChainBuilder
后,WebSecurity
利用我们继承的WebSecurityConfigurerAdapter
添加了securityFilterChainBuilder
,最后再调用performBuild
方法完成了SecurityFilterChain
(调用每个securityFilterChainBuilder
的build
方法)和FilterChainProxy
的创建。
4.3 总结
不知不觉已经讲了那么多,想必大家已经被绕的云里雾里了吧,别急,先来回顾一下。
首先,WebSecurity
和HttpSecurity
都是通过调用自己的build
方法创建FilterChainProxy
和SecurityFilterChain
的,而Spring Security
又将这个过程拆分为了五步:beforeInit
、init
、beforeConfigure
、configure
、performBuild
。
其次,WebSecurity
创建FilterChainProxy
需要SecurityFilterChain
,在默认情况下就采用SpringBootWebSecurityConfiguration
事先创建好的SecurityFilterChain
,当我们继承了WebSecurityConfigurerAdapter
后,就利用WebSecurityConfigurerAdapter
创建securityFilterChainBuilder
(即HttpSecurity
),最后再调用其build
方法创建SecurityFilterChain
;HttpSecurity
创建SecurityFilterChain
需要一系列Filter
,而Spring Security
并不是采用直接创建Filter
的方式,而是用一系列SecurityConfigurer
去对Filter
进行配置后,再创建最终的Filter
,将其添加到HttpSecurity
中。
最后,想必大家都已经明白了我们为什么要继承WebSecurityConfigurerAdapter
,并重写里面的configure(HttpSecurity http)
方法,实际上就是将HttpSecurity
对象传给我们,然后将HttpSecurity
中的SecurityConfigurer
暴露给我们,让我们对其进行配置,进而影响到每一个Filter
的配置,最后影响整个SecurityFilterChain
的最终配置。WebSecurityConfigurerAdapter
本身就是一个SecurityConfigurer
,影响着最终FilterChainProxy
的产生。
事实上,在我们继承WebSecurityConfigurerAdapter
的那一刻,WebSecurity
和HttpSecurity
的创建先后顺序就已经发生了改变,默认情况下是HttpSecurity
在前,WebSecurity
在后,继承WebSecurityConfigurerAdapter
之后,则是WebSecurity
在前,HttpSecurity
(由WebSecurity
帮我们创建HttpSecurity
)在后。
通过前面的讲述我们可以发现,无论是SecurityFilterChain
,还是FilterChainProxy
,都是利用SecurityBuilder
一步一步构建出来的,在构建的过程中,通过一系列的SecurityConfigurer
进行个性化配置,最终构建出目标对象。前面所讲述的AuthenticationManager
也是通过这种方式构建出来的,大家如果感兴趣的话可以自行去了解,毕竟授之以鱼不如授人以渔嘛。
5. 控制台的初始密码是如何产生的
初始密码是如何产生的,和前面的过滤器链一样,肯定不能凭空产生,也是由Spring Boot
自动装配产生的。我们在spring.factories
中找到名为UserDetailsServiceAutoConfiguration
,如下所示。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class },
type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {
// 明文密码前缀,数据库中直接存储明文
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
// 密码加密算法正则表达式
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
@Bean
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
// 从SecurityProperties获取用户信息,这意味着我们可以直接在application.yml中配置用户信息
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
// 将生成的初始密码打印在控制台
if (user.isPasswordGenerated()) {
logger.warn(String.format(
"%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "
+ "Your security configuration must be updated before running your application in "
+ "production.%n",
user.getPassword()));
}
// 如果我们指定了PasswordEncoder,存储的密码就不会指定前缀,注意:指定了PasswordEncoder后,密码一定也要以这种方式存储
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
// 存储明文密码,加上指定前缀
return NOOP_PASSWORD_PREFIX + password;
}
}
6. PasswordEncoder
前面在讲解UserDetailsServiceAutoConfiguration
和DaoAuthenticationProvider
是都提到了PasswordEncoder
,而且在讲解UserDetailsServiceAutoConfiguration
的时候讲到了Spring Security
在存储密码时,会加上特定的前缀,为什么要给密码加前缀呢?为什么指定了PasswordEncoder
后,就不会加前缀呢?下面就来探究以下。
在默认情况下,DaoAuthenticationProvider
中的PasswordEncoder
会在无参构造方法中被赋值为DelegatingPasswordEncoder
,而DelegatingPasswordEncoder
,则是由PasswordEncoderFactories
产生的。
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
我们可以看到,在DelegatingPasswordEncoder
中存储着一系列PasswordEncoder
,每一个PasswordEncoder
都指定了特定的前缀,那么这些前缀有什么作用呢?接下来我们看到DelegatingPasswordEncoder
的matches
方法。
public class DelegatingPasswordEncoder implements PasswordEncoder {
private static final String PREFIX = "{";
private static final String SUFFIX = "}";
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
// 从带有前缀的密码中提取前缀,获得当前密码的加密方式
String id = extractId(prefixEncodedPassword);
// 根据前缀找特定的PasswordEncoder
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
}
// 从带有前缀的密码中提取密码
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
// 进行密码比对
return delegate.matches(rawPassword, encodedPassword);
}
private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
}
int start = prefixEncodedPassword.indexOf(PREFIX);
if (start != 0) {
return null;
}
int end = prefixEncodedPassword.indexOf(SUFFIX, start);
if (end < 0) {
return null;
}
return prefixEncodedPassword.substring(start + 1, end);
}
private String extractEncodedPassword(String prefixEncodedPassword) {
int start = prefixEncodedPassword.indexOf(SUFFIX);
return prefixEncodedPassword.substring(start + 1);
}
}
我们可以看到,前缀就是用来指定密码的加密方式,最后校验密码的时候方便找到特定的PasswordEncoder
进行密码校验,而前缀为{noop}
就直接存储的是明文密码。那么,Spring Security
为什么要这么设计呢?指定了PasswordEncoder
后为什么就不会采用DelegatingPasswordEncoder
呢?这些问题大家可以自行去思考。
7. 代码示例
https://github.com/POILKJHMNBV/SpringSecurityDemo