SpringSecurity认证流程UsernamePasswordAuthenticationFilter分析
认证的流程就是完成一系列的过滤器, Sprinig Security 启动源码详细分析文章提过,Security维护一套自己的过滤器链,也可以添加自定义过滤器,并指定order,这次分析默认的认证流程.
当前端请求到后端,经过一系列的servlet过滤器,其中包括Security过滤器DelegatingFilterProxy,而实际调用的为VirtualFilterChain类的成员变量additionalFilters,下图为Security过滤器链如何执行,以及执行结束后,切换到servlet过滤器链


UsernamePasswordAuthenticationFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
三个关键的过滤器
本文只分析UsernamePasswordAuthenticationFilter过滤器
自定义规则的代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf ().disable ().formLogin ()
.loginPage ("/page/login")//请求登入页的路径
.loginProcessingUrl ("/login/code")//输入用户名和密码 验证的路径
// .successHandler (myAuthenticationSuccessHandler)
.successHandler (mySavedRequestAwareAuthenticationSuccessHandler)
// .failureHandler (myAuthenticationFailureHandler)
.failureHandler (mySimpleUrlAuthenticationFailureHandler)
.and ()
.authorizeRequests ()
.antMatchers ("/hello/**").authenticated ()//需要身份认证即可,没有角色要求,下面配置身份验证规则
.antMatchers ("/nihao/**", "/page/login").permitAll ()
.antMatchers ("/nihao/**").hasAnyRole ("role","admin")
//.antMatchers("/imgs/**").permitAll() //如果有静态的资源(存放在 resources/static,resources/templates) 比图 css js 或者文件 ,注意这里不需要写static,我们还可以重写 configure(WevSecurity web)方法
.anyRequest ().authenticated (); //访问需要身份验证,如果验证不通过则返回到登入也
}
- UsernamePasswordAuthenticationFilter在初始化结束后成员变量的情况,方便后续分析源码.
[图片上传中...(image.png-9e01c-1609208956602-0)]
UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter类,调用UsernamePasswordAuthenticationFilter的doFilter方法是调用父类方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//判断request的请求路径是否为需要校验用户名和密码的路径 默认为 /login
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//校验用户名密码
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
- requiresAuthentication(request, response)则是调用了AndRequestMatcher的match()方法,在创建UsernamePasswordAuthenticationFilter对象时创建了AndRequestMatcher类并初始化路径为/login,方法为POST,在调用自定义规则方法中 loginProcessingUrl ("/login/code"),方法时可以修改默认的/login路径,我们这里就修改为/login/code,如果request的请求路径为/login/code并且为POST方法则返回true,取反为false,
- 返回步骤1方法向下执行到方法向下执行
authResult = attemptAuthentication(request, response);执行UsernamePasswordAuthenticationFilter的attemptAuthentication方法
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//再次判断请求方法是不是POST
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//从reqeust中获取username和password
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//初始化UsernamePasswordAuthenticationToken将用户名和密码传入
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
- 初始化UsernamePasswordAuthrnticationToken
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
//将父类AbstractAuthenticationToken的成员变量authenticated赋值为false
super.setAuthenticated(true); // must use super, as we override
}
- 返回步骤3 setDetails(request, authRequest); 追踪源码初始化WebAuthenticationDetails,并赋值给UsernamePasswordAuthenticationToken的details
public WebAuthenticationDetails(HttpServletRequest request) {
this.remoteAddress = request.getRemoteAddr();
HttpSession session = request.getSession(false);
this.sessionId = (session != null) ? session.getId() : null;
}
- 返回步骤3 return this.getAuthenticationManager().authenticate(authRequest);
this.getAuthenticationManager()获取的是ProviderManager对象,执行ProviderManager
- authenticate方法
authenticate会被递归两次调用
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//为UsernamePasswordAuthencationToken
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 第一次getProviders()获取的是ProbiderManager的providers 成员变量是list里面只有一个数据AnonymousAuthenticationPrivoder
//第二次调用getProviders()获取的是ProbiderManager的providers 成员变量是list里面只有一个数据DaoAuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
//第一次AnonymousAuthenticationPrivoder不支持UsernamePasswordAuthencationToken
//第二次DaoAuthenticationProvider支持UsernamePasswordAuthencationToken
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//第二次DaoAuthenticationProvider支持UsernamePasswordAuthencationToken,所以这里调用DaoAuthenticationProvider的authenticate方法
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
//第一次返回null
if (result == null && parent != null) {
try {
//parent为ProviderManager,递归调用
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
- DaoAuthenticationProvider的authenticate方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
//从缓存中获取user对象,已经校验成功的对象会被放到缓存中
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//获取user对象,调用本类(DaoAuthenticationProvider)的retrieveUser方法,
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
try {
//一直到return都是校验用户名和密码和检验用户是否锁定,是否过期等
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 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);
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();
}
//返回UsernamePasswordAuthencationToken
return createSuccessAuthentication(principalToReturn, authentication, user);
}
- user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
//从ioc容器中获取PassWordEncoder,我们在配置类中将 BCryptPasswordEncoder放到容器中
prepareTimingAttackProtection();
try {
// this.getUserDetailsService()是我们自定义的UserDetailServiceImpl类,从数据库中获取的user信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
//返回user
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
-
回到步骤8代码向下执行到
return createSuccessAuthentication(principalToReturn, authentication, user);
下面的图片就是最终得到的UsernamePasswordAuthencationToken
image.png
- 返回到步骤1的authResult = attemptAuthentication(request, response);
authResult就是上图得到的结果代码向下执行
成功则执行successfulAuthentication(request, response, chain, authResult);
successfulAuthentication方法中的这段代码SecurityContextHolder.getContext().setAuthentication(authResult);因为是ThreadLocalSecurityContextHolderStrategy所以供当前线程使用,在调用SecurityContextPersistenceFilter的finally的时候,将session和context绑定起来,再次携带session访问的时候,就会验证是否是已经登入成功的用户在访问.
失败则执行unsuccessfulAuthentication(request, response, failed);
最终执行的是我们自定义的 MySavedRequestAwareAuthenticationSuccessHandler
MySimpleUrlAuthenticationFailureHandler两个类,当然在执行successfulAuthentication会有记住我功能,会在后续分析,过滤器链到此就结束了,直接返回给了前台
- 如果我们不是前后端分离项目,那么登入成功或者失败需要返回页面,错误页面默认情况下就是登入页面,只是在返回的状态和信息提示是登入错误,但是登入成功,我们需要返回被拦截的页面,那Security是如何在登入成功之后返回之前被拦截的页面呢.
在访问路径的时候,判断路径是否需要拦截,拦截之后会存放到DefaultSavedRequest的URLRequest中,当检验成功之中在重定向到URLRequest路径.
下面是自定已成功处理器,当检验成功就会执行下面的onAuthenticationonSuccess
@Component
public class MySavedRequestAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//条件根据项目实际需要,替代true即可,可以在配置文件利用@Value获取变量替代
if(false){
//返回json字符串
response.setContentType ("application/json;charset=UTF-8");
response.getWriter ().write ("登入成功");}
else{
//如果访问/hello被拦截,登入成功后重定向到/hello
super.onAuthenticationSuccess (request, response, authentication);
}
}
}

至于路径的是如何根据自定义规则被拦截,后续分分析.
网友评论