SpringSecurity认证的执行流程
在Spring Security 中,与认证、授权相关的校验其实都是利用一系列的过滤器来完成的,这些过滤器共同组成了一个过滤器链,如下图所示:

当开启SpringSecurity的debug模式时,可以看到控制台打印出来的各个过滤器执行的顺序:
@EnableWebSecurity(debug=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
1、 WebAsyncManagerIntegrationFilter;
2、 SecurityContextPersistenceFilter;
3、 HeaderWriterFilter;
4、 CsrfFilter;
5、 LogoutFilter;
6、 UsernamePasswordAuthenticationFilter;
7、 DefaultLoginPageGeneratingFilter;
8、 DefaultLogoutPageGeneratingFilter;
9、 RequestCacheAwareFilter;
10、 SecurityContextHolderAwareRequestFilter;
11、 AnonymousAuthenticationFilter:如果之前的认证机制都没有更新SecurityContextHolder拥有的Authentication,那么一个AnonymousAuthenticationToken将会设给SecurityContextHolder;
12、 SessionManagementFilter;
13、 ExceptionTranslationFilter:用于处理在FilterChain范围内抛出的AccessDeniedException和AuthenticationException,并把它们转换为对应的Http错误码返回或者跳转到对应的页面;
14、 FilterSecurityInterceptor:负责保护WebURI,并且在访问被拒绝时抛出异常;
在上面图中所展示的一系列的过滤器中,和认证授权直接相关的过滤器是 AbstractAuthenticationProcessingFilter 和 UsernamePasswordAuthenticationFilter
但是你可能又会问,怎么没看到 AbstractAuthenticationProcessingFilter 这个过滤器呢?这是因为它是一个抽象的父类,其内部定义了认证处理的过程,UsernamePasswordAuthenticationFilter 就继承自 AbstractAuthenticationProcessingFilter
接下来详细介绍下这两个过滤器
AbstractAuthenticationProcessingFilter
在AbstractAuthenticationProcessingFilter 中定义了认证处理过程,具体如下:

由上图可知,在执行过滤器的时候会对请求进行校验requiresAuthentication(),如果校验通过后才会执行具体的认证逻辑。那这里校验的什么?这里先不继续说,后续会再次介绍。
当校验通过后,则会执行具体具体的认证逻辑attemptAuthentication,该方法是个抽象方法,具体的实现逻辑都是在子类中实现的,也就是UsernamePasswordAuthenticationFilter.
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter的一个子类,认证与授权的相关校验均在此类中定义的。具体实现如下:
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";
// ……
// ~ Constructors
// ===================================================================================================
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
// ......其他略......
}
通过上述代码可以分析出如下结论
- 从构造方法中得知 该过滤器只针对
**post请求的/login接口生效**, 在上面曾经说过在执行attemptAuthentication,之前会先调用requiresAuthentication()进行请求校验,校验的规则就是根据该构造器来决定的。 - 过滤器中,再利用obtainUsername和obtainPassword方法,提取出请求里边的用户名/密码,提取方式就是request.getParameter,这也是为什么SpringSecurity中默认的表单登录要通过key/value的形式传递参数,而不能传递JSON参数如果像传递JSON参数,我们可以通过修改这里的代码来进行实现;
- 获取到请求里传递来的用户名/密码之后,接下来会构造一个UsernamePasswordAuthenticationToken对象,传入username和password其中username对应了UsernamePasswordAuthenticationToken中的principal属性,而password则对应了它的credentials属性;
- 接下来再利用setDetails方法给details属性赋值,UsernamePasswordAuthenticationToken本身是没有details属性的,这个属性是在它的父类AbstractAuthenticationToken中定义的details是一个对象,这个对象里边存放的是WebAuthenticationDetails实例,该实例主要描述了请求的remoteAddress以及请求的sessionId这两个信息;
- 最后一步,就是利用AuthenticationManager对象来调用authenticate()方法去做认证校验;
AuthenticationManager与ProviderManager
在上面 UsernamePasswordAuthenticationFilter类的 attemptAuthentication() 方法中得知,该方法的最后一步会进行关于认证的校验,而要进行认证操作首先要获取到一个 AuthenticationManager 对象,这里默认拿到的是AuthenticationManager的子类ProviderManager ,如下图所示:

进入到 ProviderManager 的 authenticate()方法中,来看看认证到底是怎么实现的。比较重要的




以上四张截图就是ProviderManager认证相关的核心逻辑
Spring Security中关于认证的重要逻辑几乎都是在这里完成的
- 首先利用反射,获取到要认证的authentication对象的Class字节码
- 判断当前provider是否支持该authentication对象
- 如果当前provider不支持该 authentication 对象,则退出当前判断,进行下一次判断。
- 如果支持,则调用provider的authenticate方法开始做校验,校验完成后,会返回一个新的Authentication
- 这里如果 provider 的 authenticate 方法没能返回一个 Authentication 认证对象,则会调用 provider 的 parent 对象中的 authenticate 方法继续校验。
- 而如果通过了校验,返回了一个Authentication认证对象,则调用copyDetails()方法把旧Token的details属性拷贝到新的Token中,如下图;
- 接下来会调用eraseCredentials()方法来擦除凭证信息,也就是我们的密码,这个擦除方法比较简单,就是将Token中的credentials属性置空;
- 最后通过publishAuthenticationSuccess()方法将认证成功的事件广播出去;
- SpringSecurity会监听这个事件,接收到这个Authentication对象,进而调用SecurityContextHolder.getContext().setAuthentication(…)方法,将AuthenticationManager返回的Authentication对象,存储在当前的SecurityContext对象中;
AuthenticationProvider
AuthenticationManager是负责管理协调认证工作的,但并不负责认证功能的真正实现,认证功能的真正实现是由 AuthenticationManager 中引用的 AuthenticationProvider的子类来完成的,通过源码我们可以看到AuthenticationManager 中引用了一个providers列表,
private List<AuthenticationProvider> providers = Collections.emptyList();
providers集合的泛型是*AuthenticationProvider接口,*AuthenticationProvider接口有多个实现子类,如下图:

DaoAuthenticationProvider类关系

AuthenticationProvider接口的一个直接子类是AbstractUserDetailsAuthenticationProvider,该类又有一个直接子类DaoAuthenticationProvider。平常我们使用用户名密码登录时,默认就是使用该Provider。
Spring Security中默认就是使用DaoAuthenticationProvider来实现基于数据库模型认证授权工作的!
DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证方式,可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。
DaoAuthenticationProvider类中并没有重写 authenticate() 方法,authenticate() 方法是在父类AbstractUserDetailsAuthenticationProvider中实现的。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
//获取authentication中存储的用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//判断是否使用了缓存
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//retrieveUser()是一个抽象方法,由子类DaoAuthenticationProvider来实现,用于根据用户名查询用户。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
......
}
try {
//进行必要的认证前和额外认证的检查
preAuthenticationChecks.check(user);
//这是抽象方法,由子类DaoAuthenticationProvider来实现,用于进行密码对比
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
//在发生异常时,尝试着从缓存中进行对象的加载
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
//认证后的检查操作
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//认证成功后,封装认证对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
查看源码我们可以知道大概的逻辑
-
首先会从Authentication提取出登录用户名
-
然后利用得到的username,先去缓存中查询是否有该用户
-
如果缓存中没有该用户,则去执行 retrieveUser() 方法获取当前用户对象。而这个retrieveUser()方法是个抽象方法,在AbstractUserDetailsAuthenticationProvider类中并没有实现,是由子类DaoAuthenticationProvider来实现的。

-
在DaoAuthenticationProvider类的retrieveUser()方法中,会调用getUserDetailsService()方法,得到UserDetailsService对象,执行我们自己在登录时候编写的loadUserByUsername()方法,然后返回一个UserDetails对象,也就是我们的登录对象如下图所示;

-
接下来会继续往下执行preAuthenticationChecks.check()方法,检验user中各账户属性是否正常,例如账户是否被禁用、是否被锁定、是否过期等
-
接着会继续往下执行additionalAuthenticationChecks()方法,进行密码比对。而该方法也是抽象方法,也是由子类DaoAuthenticationProvider进行实现。我们在注册用户时对密码加密之后,Spring Security就是在这里进行密码比对的******。如下所示。

- 然后在postAuthenticationChecks.check()方法中检查密码是否过期,如下所示;
- 然后判断是否进行了缓存,如果未进行缓存,则执行缓存操作,这个缓存是由SpringCacheBasedUserCache类来实现的;
- 我们这里如果没有对缓存做配置,则会执行默认的缓存配置操作。如果我们对缓存进行了自定义的配置,比如配置了RedisCache,就可以把对象缓存到redis中。
- 最后通过createSuccessAuthentication()方法构建出一个新的 UsernamePasswordAuthenticationToken对象。

6986

被折叠的 条评论
为什么被折叠?



