UsernamePasswordAutheticationFilter源码解读和实践


如果你对SecurityFilterChain之前的过程存在疑惑,那么可以去研究一下下面两个内容,其实懂不懂下下面两个内容都不会影响你阅读本篇文章。

  • DelegatingFilterProxy
  • FilterProxyChain & SecurityFilterChain

下面会更新上面两个内容,可以关注一下 Spring源码研究导航

一、概述(重点)

第一步,主要对整个UsernamePasswordAutheticationFilter有一个全面的认知,这样读源码才事半功倍。下面会通过小章节对每一个标红的步骤进行解读。

在这里插入图片描述

二、标红小步骤解读

2.1 步骤1(标红1)

AbstractAuthenticationProcessingFilter
«Interface»
ApplicationEventPublisherAware
«Interface»
Aware
«Interface»
BeanNameAware
«Interface»
DisposableBean
«Interface»
EnvironmentAware
«Interface»
EnvironmentCapable
«Interface»
Filter
GenericFilterBean
«Interface»
InitializingBean
«Interface»
MessageSourceAware
«Interface»
ServletContextAware
UsernamePasswordAuthenticationFilter

看一下UsernamePasswordAuthenticationFilter的继承图,还是相当复杂的,不过大多数都跟Spring有关,我们只需要关注AbstractAuthenticationProcessingFilter抽象类和Filter接口就差不多了。

2.1.1 AbstractAuthenticationProcessingFilter

先看AbstractAuthenticationProcessingFilterdoFilter方法,继承了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实现类

«Interface»
AuthenticationManager
«Interface»
Aware
«Interface»
InitializingBean
«Interface»
MessageSourceAware
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 总结

ProviderManagerauthenticate方法中可以总结出下图的结构,虽然有点抽象。主要表达的意思是,ProviderManager内部维护了一堆AuthenticationProvider和父级AuthenticationManagerauthenticate的逻辑是先把认证交给一堆AuthenticationProvider做认证,AuthenticationProvider认证失败了才交给父级。

在这里插入图片描述

2.4 步骤4(标红4)

上面# 三、步骤2 和 步骤3(标红2 和 标红3)AuthenticationProvider做了一个介绍,最终的身份认证会交给它,现在我们对它做详细的介绍。AuthenticationProvider有超级多的实现类,我们只需要关注AbstractUserDetailsAuthenticationProvider抽象类,以及抽象类的实现类DaoAuthenticationProvider

AbstractJaasAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
AnonymousAuthenticationProvider
«Interface»
AuthenticationProvider
DaoAuthenticationProvider
DefaultJaasAuthenticationProvider
JaasAuthenticationProvider
PreAuthenticatedAuthenticationProvider
RememberMeAuthenticationProvider
RemoteAuthenticationProvider
RunAsImplAuthenticationProvider
TestingAuthenticationProvider

2.4.1 AbstractUserDetailsAuthenticationProvider

因为AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,所以我们直接挑authenticate来看。
下面方法主要看两个方法

  1. 查缓存,如果缓存有用户,就直接返回了。可以发现默认实现是NullUserCache,啥也不做。
  2. 做身份验证的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());
    }
}

登录成功结果

在这里插入图片描述

登录失败结果

在这里插入图片描述

四、总结

目前场景比较少,各位有遇到的场景可以留言评论,我给出代码。

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值