UsernamePasswordAuthenticationFilter的目录
如果你对SecurityFilterChain之前的过程存在疑惑,那么可以去研究一下下面两个内容,其实懂不懂下下面两个内容都不会影响你阅读本篇文章。
- DelegatingFilterProxy
- FilterProxyChain & SecurityFilterChain
下面会更新上面两个内容,可以关注一下 Spring源码研究导航
一、概述(重点)
第一步,主要对整个UsernamePasswordAutheticationFilter有一个全面的认知,这样读源码才事半功倍。下面会通过小章节对每一个标红的步骤进行解读。
二、标红小步骤解读
2.1 步骤1(标红1)
看一下UsernamePasswordAuthenticationFilter
的继承图,还是相当复杂的,不过大多数都跟Spring有关,我们只需要关注AbstractAuthenticationProcessingFilter
抽象类和Filter
接口就差不多了。
2.1.1 AbstractAuthenticationProcessingFilter
先看AbstractAuthenticationProcessingFilter
的doFilter
方法,继承了Filter
方法之后Servlet
会执行doFilter
,所以直接看doFiter
方法就可以了。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
// doFilter逻辑主要有三个
// 1. 通过attemptAuthentication对用户身份进行验证
// 2. 身份验证成功之后执行successfulAuthentication
// 3. 身份验证失败执行unsuccessfulAuthentication
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断请求是否符合过滤器的要求。
// 使用RequestMatcher判断,判断的条件跟URL和请求方法有关。
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// attemptAuthentication是子类UsernamePasswordAuthenticationFilter实现的。
// 这个方法跟用户身份验证有关。
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
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);
}
}
...
}
身份验证成功之后的回调。
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
// 负责重定向回原来的页面,下面有详细的解释。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 保存认证成功之后的用户信息
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// 这个母鸡
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 重定向到原始的访问地址,就比如:
// 你访问localhost:8080/user,由于你没有认证,所以后端会让浏览器重定向到身份认证的页面http://localhost:8080/login
// 你在http://localhost:8080/login网址通过了认证之后,后端会让浏览器重定向回localhost:8080/user
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
身份验证失败之后的回调。
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
...
// 失败处理也分三个部分
// 1. 如果没有指定失败的重定向链接,那么直接调用response.sendError
// 2. 如果指定了defaultFailureUrl,forwardToDestination=true,那么就把请求转发给defaultFailureUrl,这个是内部转发,共享request。
// 3. 最后就是重定向了,重定向到defaultFailureUrl。
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
} else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
}
else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
...
}
2.1.2 UsernamePasswordAuthenticationFilter
这个类主要看attemptAuthentication
方法。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
setDetails(request, authRequest);
// 这里就是要进入到我们红色圆圈2了。
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
2.3 步骤2 和 步骤3(标红2 和 标红3)
2.3.1 解读
通过上面的分析,可以从UsernamePasswordAuthenticationFilter的return this.getAuthenticationManager().authenticate(authRequest)代码得知,UsernamePasswordAuthenticationFilter会把身份认证的任务抛给AuthenticationManager。接下来我们对AuthenticationManager进行分析,从下图得知ProviderManager
实现了AuthenticationManager
接口,这里主要是理解ProviderManager
实现类。
下面主要看一下ProviderManager
的核心属性(providers、parent)和核心方法authenticate
。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 两个核心属性
private List<AuthenticationProvider> providers = Collections.emptyList();
private AuthenticationManager parent;
// 方法主要有两个核心的方法
// 1. 首先从认证提供器(AuthenticationProvider)里边做身份验证
// 2. 如果认证提供器认证失败,那就继续给上一级的认证管理器(AuthenticationManager)做认证
@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();
// 先遍历所有的AuthenticationProvider,通过他们去做身份验证。
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
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;
}
}
// 如果所有的AuthenticationProvider的身份验证都失败了,那么去找ProviderManager父级。
if (result == null && this.parent != null) {
try {
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)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}
2.3.2 总结
从ProviderManager
的authenticate
方法中可以总结出下图的结构,虽然有点抽象。主要表达的意思是,ProviderManager
内部维护了一堆AuthenticationProvider
和父级AuthenticationManager
,authenticate
的逻辑是先把认证交给一堆AuthenticationProvider
做认证,AuthenticationProvider
认证失败了才交给父级。
2.4 步骤4(标红4)
上面# 三、步骤2 和 步骤3(标红2 和 标红3) 对AuthenticationProvider
做了一个介绍,最终的身份认证会交给它,现在我们对它做详细的介绍。AuthenticationProvider
有超级多的实现类,我们只需要关注AbstractUserDetailsAuthenticationProvider
抽象类,以及抽象类的实现类DaoAuthenticationProvider
。
2.4.1 AbstractUserDetailsAuthenticationProvider
因为AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,所以我们直接挑authenticate
来看。
下面方法主要看两个方法
- 查缓存,如果缓存有用户,就直接返回了。可以发现默认实现是NullUserCache,啥也不做。
- 做身份验证的retrieveUser方法,这是一个抽象方法,所有我们要看子类DaoAuthentiationProvider。
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
private UserCache userCache = new NullUserCache();
@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;
// 查缓存,这里也可以进行扩展,我们可以把缓存实现成Redis
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 调用抽象方法
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();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
}
2.4.2 DaoAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@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.4.3 总结 UserDetailsService & UserCache
上面的AbstractUserDetailsAuthenticationProvider的authenticate方法逻辑是先查缓存(UserCache),再交给UserDetailsService。那如何自定义自己的UserDetailsService和UserCache呢?在下个大章节说。
三、说完源码,开始做实践
3.1 身份验证后置处理
主要是failureHandler.setUseForward(true),如果设置为false就重定向,如果设置为true就是内部转发。
@EnableWebSecurity
public class SpringSecurityConfig {
@Autowired
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("123")).roles("admin"));
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.formLogin()
.failureHandler(buildAuthenticationFailureHandler())
.successHandler(buildSimpleUrlAuthenticationSuccessHandler());
return http.build();
}
// 失败后置处理
SimpleUrlAuthenticationFailureHandler buildAuthenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/other/login/fail");
return failureHandler;
}
}
@RestController
@RequestMapping("/other")
public class UserController {
@PostMapping("/login/fail")
public AjaxResult fail() {
return AjaxResult.failure("账号或密码错误!");
}
@PostMapping("/login/success")
public AjaxResult success() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return AjaxResult.success("登录成功!", authentication.getPrincipal());
}
}
登录失败的效果
登录成功的效果
3.2 前后端分离项目的登录配置
这个配置加上了登录的后置处理,重点在移除了自带的登录页面,设置了登录url,有的同学很懵逼,我解释一下,设置了登录url之后UsernamePasswordAuthenticationFilter会拦截/user/login做登录处理,然后就是走我们# 一、概述(重点)那个图的整个过程了
@EnableWebSecurity
public class SpringSecurityConfig {
@Autowired
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("123")).roles("admin"));
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 关闭自带的登录页面
http.getConfigurer(DefaultLoginPageConfigurer.class).disable();
http.formLogin()
.failureHandler(buildAuthenticationFailureHandler())
.successHandler(buildSimpleUrlAuthenticationSuccessHandler())
// 设置登录url
.loginProcessingUrl("/user/login")
.and()
.csrf().disable();
return http.build();
}
// 登录失败处理器
SimpleUrlAuthenticationFailureHandler buildAuthenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/other/login/fail");
return failureHandler;
}
// 登录成功处理器,内部转发
AuthenticationSuccessHandler buildSimpleUrlAuthenticationSuccessHandler() {
return ((request, response, authentication) -> request.getRequestDispatcher("/other/login/success").forward(request, response));
}
}
@RestController
@RequestMapping("/other")
public class UserController {
@PostMapping("/login/fail")
public AjaxResult fail() {
return AjaxResult.failure("账号或密码错误!");
}
@PostMapping("/login/success")
public AjaxResult success() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return AjaxResult.success("登录成功!", authentication.getPrincipal());
}
}
登录成功结果
登录失败结果
四、总结
目前场景比较少,各位有遇到的场景可以留言评论,我给出代码。