1. Spring Security流程介绍
1.1 过滤器链
我们知道请求进入服务器端后会经过一连串的过滤器,而Spring Security本质上也是一连串的过滤器,这一串过滤器会以一个独立的Filter插入到FilterChain中,就是FilterChainProxy。
其简化流程图为:
1.2 Spring Security Filter
Spring Security有很多很多的过滤器,处理过程也相当复杂,但我们只需要专注于他最核心的两个功能,认证(Authentication)和授权(Authorization),忽略其他过滤器,可大大简化其流程,便于我们学习和理解。
认证和授权是由两个核心的过滤器实现的:
- 认证处理过滤器:AbstractAuthenticationProcessiongFilter
- 认证管理器:AuthenticationManager
- 请求鉴权过滤器:AbstractSecurityInterceptor
- 决策管理器:AccessDecisionManager
FilterChainProxy是一个代理,真正起作用的是一个个Filter,这些Filter作为Bean被spring管理,是SpringSecurity的核心。每个Filter都有各自的职责,不直接处理认证和授权,而是交由认证管理器和决策管理器实现。
其简化流程图为:
2 Spring Security认证
2.1 认证流程图
2.2 从源码分析认证过程
用户请求携带用户名与密码进入UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter,会先执行AbstractAuthenticationProcessingFilter中的doFilter()方法,doFilter()方法中attemptAuthentication()为认证方法,该方法是抽象方法,在UsernamePasswordAuthenticationFilter中重写
2.2.1 AbstractAuthenticationProcessingFilter
进入过滤器后先执行AbstractAuthenticationProcessingFilter.doFilter()方法
1. doFilter()
流程为:
- 调用attemptAuthentication()方法进行认证,该方法为抽象方法,子类为UsernamePasswordAuthenticationFilter
- 认证失败,执行unsuccessfulAuthentication()方法进入认证失败处理器执行认证失败逻辑
- 认证成功,执行successfulAuthentication()方法进入认证成功处理器执行认证成功逻辑
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//尝试进行认证,该方法为抽象方法,具体实现在子类UsernamePasswordAuthenticationFilter中
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
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);
}
}
}
2.2.2 UsernamePasswordAuthenticationFilter
进入UsernamePasswordAuthenticationFilter的调用attemptAuthentication()方法
1. attemptAuthentication()
流程为:
- 获取提交的用户名和密码
- 获取AuthenticationManager对象,调用AuthenticationManager的authenticate()方法,参数为未经认证的Authentication,返回一个经过认证的Authentication
@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 : "";
//根据传入的用户名和密码生成一个未经认证的Token,是Authentication的子类
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//调用AuthenticationManager中的authenticate()方法,参数为未经认证的Authentication,返回返回一个Authentication对象
return this.getAuthenticationManager().authenticate(authRequest);
}
2.2.3 AuthenticationManager
AuthenticationManager是接口,仅有一个方法authenticate(),其实现类为ProviderManager。
调用进入ProviderManager的Authenticate()方法中 该类中有一系列的AuthenticationProvider,循环判断每个AuthenticationProvier是否支持当前请求 ,支持则调用该AuthentticationProvider中的authenticate()方法,不支持则继续判断下一个AuthenticationProvider 若所有的AuthenticationProvider都不支持,则最后调用全局AuthenticationProvider进行一次认证
1. authenticate()
流程为:
- 循环遍历每个provider,调用其supports()方法,判断该provider是否支持当前请求
- 支持则调用provider中的authenticate()方法进行认证,不支持则遍历判断下一个provider
- 若所有provider都不支持,则调用父Provider(也称为全局Provider)进行最后一次认证
@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();
//循环每个provider
for (AuthenticationProvider provider : getProviders()) {
//判断该provider是否支持当前请求,不支持则继续判断下一个provider
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//支持则调用provider中的authenticate()方法
result = provider.authenticate(authentication);
if (result != null) {
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;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
//若所有的provider都不支持当前请求,则最后调用全局provider进行认证一次
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);
}
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);
}
throw lastException;
}
2.2.4 AuthenticationProvider
AuthenticationProvider同样为接口,提供supports()和authenticate()两个方法,其实现类AbstractUserDetailsAuthenticationProvider为抽象类,其中有两个方法retrieveUser()和additionalAuthenticationChecks()在其子类中实现,子类为DaoUserDetailsAuthenticationProvider
1. AbstractUserDetailsAuthenticationProvider.authenticate()
流程为:
- 从缓存中获取UserDetails对象
- 若缓存中不存在,则调用retrieveUser()从数据库中获取,该方法为抽象方法,具体实现在子类DaoUserDetailsAuthenticationProvider中
- 调用preAuthenticationChecks.check()方法做认证前的检查,主要是账号的可用性,是否锁定等
- 调用additionalAuthenticationChecks()方法进行用户名与密码的验证,该方法为抽象方法,具体实现在子类DaoUserDetailsAuthenticationProvider中
- 是否使用了缓存,使用了则将用户信息存入缓存中
- 认证成功后生成经过认证的Authhentication对象并返回
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
//从缓存中获取用户
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//缓存中无用户则执行retrieveUser()方法从数据库中获取用户
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 {
//认证前的检查,包括用户是否被锁定,是否过期等
this.preAuthenticationChecks.check(user);
//验证用户名与密码的正确性
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);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
//使用了缓存则放入缓存中
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//生成一个通过验证的Authentication对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
2.2.5 DaoUserDetailsAuthenticationProvider
1. DaoUserDetailsAuthenticationProvider.retrieveUser()
流程为:
- 获取UserDetailsService对象,调用loadUserByUsername()方法获取用户信息,返回一个UserDetails用户对象
UserDetailsService是接口,当需要从数据库中获取用户信息时,需要程序员实现UserDetailsService接口,重写loadUserByUsername()方法,完成从数据库中获取用户数据的逻辑,返回对象为UserDetails,重写loadUserByUsername()方法时,可以自定义一个用户类,封装用户信息,继承UserDetails类来返回。
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//调用UserDetailsService中的loadUserByUsername()方法获取用户信息
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);
}
}
2. DaoUserDetailsAuthenticationProvider.additionalAuthenticationChecks()
流程为:
- 将提交的明文进行加密,并和从数据库中获取的密文进行比对
@Override
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"));
}
}
至此,认证流程完成。
3 认证流程总结
由上面源码分析可以看出一共使用了如下几个类,可将其分为四个部分去梳理他们的功能:
后面按照这四个部分再梳理一次,所使用的类如下:
-
UsernamePasswordAuthenticationFilter
-
AbstractAuthenticationProcessingFilter
-
ProviderManager
-
AuthenticationManager
-
AuthenticationProvider
-
AbstractUserDetailsAuthenticationProvider
-
DaoAuthenticationProvider
-
UserDetailsService
-
UserDetails
3.1 过滤器
这一组的职责是将用户提交的用户名与密码数据封装成未经认证的Authentication对象然后交由ProviderManager进行认证。
- AbstractAuthenticationProcessingFilter
- UsernamePasswordAuthenticationFilter
AbstractAuthenticationProcessingFilter为认证过滤器的入口,执行他的doFilter()方法,开始进行表单用户名与密码的验证,在子类AbstractAuthenticationProcessingFilter中将用户名与密码封装成一个未经认证的Authentication对象,然后交由ProviderManger(认证管理器)进行认证。
3.2 认证管理器
这一组的职责是选择一个合适的AuthenticationProvider进行认证
- AuthenticationManager
- ProviderManager
未经认证的Authentication进入ProviderManager之后,会有一系列的AuthenticationProvider,依次进行判断选择,选择一个支持当前请求的AuthenticationProvider进行认证
3.3 AuthenticationProvider
这一组的职责是作为一组固定的模板,提供了完整的验证流程,暴露UserDetailsService接口交由用户自行实现。
- AuthenticationProvider
- AbstractUserDetailsAuthenticationProvider
- DaoAuthenticationProvider
AuthenticationProvider是一组接口,AbstractUserDetailsAuthenticationProvider是实现这组接口的抽象类,在抽象类的authenticate()方法中定义了验证流程,而其中核心的两个方法作为抽象方法,让其子类DaoAuthenticationProvider进行完善,在DaoAuthenticationProvider中使用了UserDetailsService中的方法来获取用户信息,该接口需程序员来实现,自定义用户数据和用户数据的获取逻辑。
3.4 自定义实现
这一组交由程序员进行处理,用户自定义用户数据和用户数据的获取逻辑。
- UserDetailsService
- UserDetails
实现UserDetailsService接口,重写loadUserByUsername()方法,自定义用户数据获取逻辑,自定义用户数据返回类,封装用户数据并继承UserDetails。