Java-Security(四):用户认证流程源码分析

让我们带着以下3个问题来阅读本篇文章:

  • 1)在Spring Security项目中用户认证过程中是如何执行的呢?
  • 2)认证后认证结果如何实现多个请求之间共享?
  • 3)如何获取认证信息? 

在《Java-Security(二):如何初始化springSecurityFilterChain(FilterChainProxy)》中可以发现SpringSecurity的核心就是一系列过滤链,当一个请求进入时,首先会被过滤链拦截到,拦截到之后会首先经过校验,校验之后才可以访问到用户各种信息。

下图是Spring Security过滤链是Spring Security运行的核心,下图对Spring Security过滤链示意图:

从图中我们可以发现Spring Security框架在用户发送一个请求进入系统时,会经过一些列拦截器拦截后才能访问到我们自己定义的Rest API或者自定义Controller API。上图中Spring Security第一个拦截器是SecurityContextPersistenceFilter,它主要存放用户的认证信息。然后进入第二个拦截器UsernamePasswordAuthenticationFilter,它主要用来拦截Spring Security拦截用户密码表单登录认证使用(默认,当发现请求是Post,请求地址是/login,且参数包含了username/password时,就进入了认证环节)。

一、用户认证流程

UsernamePasswordAuthenticationFilter的认证过程流程图如下:

 

下边将会对用户登录认证流程结果源码进行分析:

UsernamePasswordAuthenticationFilter父类AbstractAuthenticationProcessingFilter#doFilter()

当请求是Post且请求地址是/login时,会被UsernamePasswordAuthenticationFilter拦截到,进入该拦截器时会首先进入它的父类`AbstractAuthenticationProcessingFilter#doFilter(ServletRequest req, ServletResponse res, FilterChain chain)`;

AbstractAuthenticationProcessingFilter#doFilter()源码:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    protected ApplicationEventPublisher eventPublisher;
    protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationManager authenticationManager;
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private RequestMatcher requiresAuthenticationRequestMatcher;
    private boolean continueChainBeforeSuccessfulAuthentication = false;
    private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
    private boolean allowSessionCreation = true;
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
        this.setFilterProcessesUrl(defaultFilterProcessesUrl);
    }

    protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
    }

    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) { // 验证是否请求路径是否复核条件:请求方式POST、地址为/login
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response); // 调用子类UsernamePasswordAuthenticationFilter#attemtAuthentication(...)
                if (authResult == null) {
                    return; 
                }

                this.sessionStrategy.onAuthentication(authResult, request, response); // 登录成功后,通过SessionStrategry记录登录信息
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8); // 登录失败,执行AuthenticationFailureHandler
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9); // 登录失败,执行AuthenticationFailureHandler
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response); 
            }

            this.successfulAuthentication(request, response, chain, authResult); // 登录成功后,调用用AuthenticationSuccessHandler
        }
    }

    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return this.requiresAuthenticationRequestMatcher.matches(request);
    }

    public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult); // 登录成功后,将认证用户记录到SecurityContextHolder中,等其他请求进来时,可以从SecurityContextHolder中获取认证信息。
        this.rememberMeServices.loginSuccess(request, response, authResult); // 登录成功后,执行‘记住我’逻辑
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult); // 登录成功后,执行AuthenticationSuccessHandler
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext(); // 登录失败后,清空SecurityContextHolder
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication request failed: " + failed.toString(), failed);
            this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
            this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
        }

        this.rememberMeServices.loginFail(request, response); // 登录失败后,执行‘记住我’逻辑
        this.failureHandler.onAuthenticationFailure(request, response, failed); // 登录失败后,执行AuthenticationFailureHandler
    }

    protected AuthenticationManager getAuthenticationManager() {
        return this.authenticationManager;
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));
    }

    public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
        Assert.notNull(requestMatcher, "requestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requestMatcher;
    }

    public RememberMeServices getRememberMeServices() {
        return this.rememberMeServices;
    }

    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
        this.rememberMeServices = rememberMeServices;
    }

    public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
        this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    protected boolean getAllowSessionCreation() {
        return this.allowSessionCreation;
    }

    public void setAllowSessionCreation(boolean allowSessionCreation) {
        this.allowSessionCreation = allowSessionCreation;
    }

    public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }

    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
        Assert.notNull(successHandler, "successHandler cannot be null");
        this.successHandler = successHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    protected AuthenticationSuccessHandler getSuccessHandler() {
        return this.successHandler;
    }

    protected AuthenticationFailureHandler getFailureHandler() {
        return this.failureHandler;
    }
}

1)如果请求地址在`HttpSecurity`中配置的http.formLogin()等信息是否复核条件(默认,验证是否请求方式post、地址为:/login)就会进入认证环节,否则就跳过进入下一个拦截器;

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    ....

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ....
        // 这里就是自定义login页面为login.html,请求地址为/login,设定了自定义failureHandler/successHandler等
        http.formLogin().loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 解决不允许显示在iframe的问题
        ...
    }
    。。。
}

2)认证:调用AbstractAuthenticationProcessingFilter子类UsernamePasswordAuthenticationFilter#attemptAuthentication(request, response)进行认证;

2.1)认证成功后,就会将认证成功信息通过sessionStrategy.onAuthentication(authResult, request, response)保存到内存中;

2.2)认证成功后,将认证信息存储到SecurityContextHolder#context中;

2.3)认证成功后,执行‘记住我’;

2.4)认证成功后,还会执行successHandler : AuthenticationSuccessHandler。

2.5)认证失败后,清空SecurityContextHolder#context中信息;

2.6)认证失败后,执行‘记住我’;

2.5)认证失败后,会执行failureHandler : AuthenticationFailureHandler。

调用AbstractAuthenticationProcessingFilter子类UsernamePasswordAuthenticationFilter#attemptAuthentication(request, response)进行认证

UsernamePasswordAuthenticationFilter源码:

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";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST")); // 指定进入该Filter的请求url规则:POST请求、请求地址为/login。注意:这里也可以在用户自己配置。
    }

    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());
        } else {
            String username = this.obtainUsername(request);// 从请求中获取username参数,该参数也可以用户自定义别名
            String password = this.obtainPassword(request);// 从请求中获取password参数,该参数也可以用户自定义别名
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 将用户、密码包装为UsernamePasswordAuthenticationToken对象
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest); // 调用AuthenticationManager#anthenticate(authRequest)
        }
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

在·attemptAuthentication()·方法内部实现逻辑:

1)验证请求必须是POST,否则抛出异常;

2)包装username/password为UsernamePasswordAuthenticationToken对象;

3)调用AuthenticationManager#authenticate(UsernamePasswordAuthenticationToken authentication)进行认证。

AuthenticationManager#authenticate(UsernamePasswordAuthenticationToken extends Authentication authentication)源码分析:

AuthenticationManager其实是一个接口:

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

AuthenticationManager的唯一实现是ProviderManager类

ProviderManager类源码:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;

    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, (AuthenticationManager)null);
    }

    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
        this.eventPublisher = new ProviderManager.NullEventPublisher();
        this.providers = Collections.emptyList();
        this.messages = SpringSecurityMessageSource.getAccessor();
        this.eraseCredentialsAfterAuthentication = true;
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        this.checkState();
    }

    public void afterPropertiesSet() throws Exception {
        this.checkState();
    }

    private void checkState() {
        if (this.parent == null && this.providers.isEmpty()) {
            throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
        }
    }

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var6 = this.getProviders().iterator(); // 其中就包含了实现类:DaoAuthenticationProvider

        while(var6.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var6.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    result = provider.authenticate(authentication); // 调用DaoAuthenticationProvider#authenticate(UsernamePasswordAuthenticationToken对象)
                    if (result != null) {
                        this.copyDetails(authentication, result);   // 认证成功后将result的信息拷贝给UsernamePasswordAuthenticationToken对象
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            this.eventPublisher.publishAuthenticationSuccess(result);
            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            this.prepareException((AuthenticationException)lastException, authentication);
            throw lastException;
        }
    }
    。。。
}

ProviderManager#List<AuthenticationProvider> providers的AuthenticationProvider实现类包含:

 

ProviderManager#authenticate(Authentication authentication) 内部实现逻辑:

1)遍历providers,其中DaoAuthenticationProvider就是AuthenticationProvider的一个实现;

2)当调用provider#authenticate(authentication);获取登录用户认证,认证成功后会返回用户信息result;

3)调用this.copyDetails(authentication, result);将result的信息赋值给authentication:UsernamePasswordAuthenticationToken。

需要注意:providers的赋值是AuthenticationManagerBuilder去赋值的,具体可以参考其他源码。

 

 

DaoAuthenticationProvider#authentication(authentication)分析:

DaoAuthenticationProvider的authentication()方法实现是定义在它的父类AbstractUserDetailsAuthenticationProvider中。

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserCache userCache = new NullUserCache();
    private boolean forcePrincipalAsString = false;
    protected boolean hideUserNotFoundExceptions = true;
    private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public AbstractUserDetailsAuthenticationProvider() {
    }

    protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        this.doAfterPropertiesSet();
    }

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) { // 如果缓存中不存在用户信息,就调用DaoAuthenticationProvider验证
            cacheWasUsed = false;

            try {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); // 调用DaoAuthenticationProvider#retrieveUser(...)
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);// preAuthenticationChecks.check(user),验证:!user.isAccountNonLocked()、!user.isEnabled()、!user.isAccountNonExpired()就抛出异常;
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user); // postAuthenticationChecks.check(user):验证!user.isCredentialsNonExpired()就抛出异常。
        if (!cacheWasUsed) { // 缓存中没有user信息时,就将user放入缓存
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

    。。。

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        private DefaultPostAuthenticationChecks() {
        }

        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired");
                throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
            }
        }
    }

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        private DefaultPreAuthenticationChecks() {
        }

        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked");
                throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            } else if (!user.isEnabled()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled");
                throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            } else if (!user.isAccountNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired");
                throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }
    }
}

1)内部调用UserDetails user=DaoAuthenticationProvider#retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication)
2)如果1)失败就抛出异常;如果1)成功就执行this.preAuthenticationChecks.check(user)、this.postAuthenticationChecks.check(user)
2.1)this.preAuthenticationChecks.check(user):验证!user.isAccountNonLocked()、!user.isEnabled()、!user.isAccountNonExpired()就抛出异常;
2.2)this.postAuthenticationChecks.check(user):验证!user.isCredentialsNonExpired()就抛出异常。
2.3)缓存中没有user信息时,就将user放入缓存。

DaoAuthenticationProvider#retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication)

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

这的UserDetailsService实现情况:

 

其中我们这里是使用了自定义UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;
    @Autowired
    private PermissionDao permissionDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userService.getUser(username);
        if (sysUser == null) {
            throw new AuthenticationCredentialsNotFoundException("用户名不存在");
        } else if (sysUser.getStatus() == Status.LOCKED) {
            throw new LockedException("用户被锁定,请联系管理员");
        } else if (sysUser.getStatus() == Status.DISABLED) {
            throw new DisabledException("用户已作废");
        }

        LoginUser loginUser = new LoginUser();
        BeanUtils.copyProperties(sysUser, loginUser);

        List<Permission> permissions = permissionDao.listByUserId(sysUser.getId());
        loginUser.setPermissions(permissions);

        return loginUser;
    }

}

在使用自定义UserDetailsService后,需要在项目的config类中指定UserDetailsService实现类。

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenFilter tokenFilter;
    @Autowired
    private TokenService tokenService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        // 基于token,所以不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.authorizeRequests()
                .antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
                        "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
                        "/statics/**")
                .permitAll().anyRequest().authenticated();
        http.formLogin().loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 解决不允许显示在iframe的问题
        http.headers().frameOptions().disable();
        http.headers().cacheControl();

        http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    /**
     * 登陆成功,返回Token
     * 
     * @return
     */
    @Bean
    public AuthenticationSuccessHandler loginSuccessHandler() {
       return new AuthenticationSuccessHandler() {

          @Override
          public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
             LoginUser loginUser = (LoginUser) authentication.getPrincipal();

             Token token = tokenService.saveToken(loginUser);
             ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
          }
       };
    }

    /**
     * 登陆失败
     * 
     * @return
     */
    @Bean
    public AuthenticationFailureHandler loginFailureHandler() {
       return new AuthenticationFailureHandler() {

          @Override
          public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException exception) throws IOException, ServletException {
             String msg = null;
             if (exception instanceof BadCredentialsException) {
                msg = "密码错误";
             } else {
                msg = exception.getMessage();
             }
             ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
             ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
          }
       };

    }

    /**
     * 未登录,返回401
     * 
     * @return
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
       return new AuthenticationEntryPoint() {

          @Override
          public void commence(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException authException) throws IOException, ServletException {
             ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", "请先登录");
             ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
          }
       };
    }

    /**
     * 退出处理
     * 
     * @return
     */
    @Bean
    public LogoutSuccessHandler logoutSussHandler() {
       return new LogoutSuccessHandler() {

          @Override
          public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
             ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功");

             String token = TokenFilter.getToken(request);
             tokenService.deleteToken(token);

             ResponseUtil.responseJson(response, HttpStatus.OK.value(), info);
          }
       };

    }
}

上边定义的AuthenticationSuccessHandler中做了特殊处理:

Token token = tokenService.saveToken(loginUser);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);

当登录成功后,返回了Token给前端,因此前端与后端交互时采用的token进行验证,因此才需要配置一个TokenFilter来做特殊处理:

这里定义一个拦截类TokenFilter.java(目的是在进入UsernamePasswordAuthenticationFilter拦截器之前提前将token换得authentication对象存入SecurityContextHolder#context中,SpringSecurity内部是采用的authentication去验证的。)

@Component
public class TokenFilter extends OncePerRequestFilter {

    public static final String TOKEN_KEY = "token";

    @Autowired
    private TokenService tokenService;
    @Autowired
    private UserDetailsService userDetailsService;
    private static final Long MINUTES_10 = 10 * 60 * 1000L;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = getToken(request);
        if (StringUtils.isNotBlank(token)) {
            LoginUser loginUser = tokenService.getLoginUser(token);
            if (loginUser != null) {
                loginUser = checkLoginTime(loginUser);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser,
                        null, loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 校验时间<br>
     * 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
     * 
     * @param loginUser
     * @return
     */
    private LoginUser checkLoginTime(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MINUTES_10) {
            String token = loginUser.getToken();
            loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
            loginUser.setToken(token);
            tokenService.refresh(loginUser);
        }
        return loginUser;
    }

    /**
     * 根据参数或者header获取token
     * 
     * @param request
     * @return
     */
    public static String getToken(HttpServletRequest request) {
        String token = request.getParameter(TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            token = request.getHeader(TOKEN_KEY);
        }

        return token;
    }

}

二、认证后认证结果如何实现多个请求之间共享?

下面我们来分析下Spring Security认证后如何实现多个请求之间共享登录信息。

UsernamePasswordXXXFilter完整认证流程

其实UsernamePasswordAuthenticationFilter#doFilter()[实际上doFilter()定义在它的父类AbstractAuthenticationProcessingFilter类中]中包含了比较完整的认证流程。下图是对认证流程的一个完整解析:包含了认证失败、认证成功后的处理逻辑。

 结合AbstractAuthenticationProcessingFilter#doFilter(...)代码进行分析:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response);       // 调用UsernamePasswordAuthenticationFilter#attempAuthentication(...)
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response); // 认证成功后动作1:执行session策略
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);             // 认证失败后动作1:执行this.unsuccessfulAuthentication(...)
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9);             // 认证失败后动作1:执行this.unsuccessfulAuthentication(...)
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            this.successfulAuthentication(request, response, chain, authResult);      // 认证成功后动作2:执行this.successfulAuthentication(...)
        }
    }

我们这里重点关系是认证成功后处理动作:

  • 认证成功后动作1:执行session策略;
  • 认证成功后动作2:执行this.successfulAuthentication(...):
    • successfulAuthentication(...)源码:
          protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
              if (this.logger.isDebugEnabled()) {
                  this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
              }
      
              SecurityContextHolder.getContext().setAuthentication(authResult); // 登录成功后,将认证用户记录到SecurityContextHolder中,等其他请求进来时,可以从SecurityContextHolder中获取认证信息。
              this.rememberMeServices.loginSuccess(request, response, authResult); // 登录成功后,执行‘记住我’逻辑
              if (this.eventPublisher != null) {
                  this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
              }
      
              this.successHandler.onAuthenticationSuccess(request, response, authResult); // 登录成功后,执行AuthenticationSuccessHandler
          }
    • 代码逻辑:
      • 1)登录成功后,将认证用户记录到SecurityContextHolder中,等其他请求进来时,可以从SecurityContextHolder中获取认证信息;
      • 2)登录成功后,执行‘记住我’逻辑;
      • 3)登录成功后,执行AuthenticationSuccessHandler

SecurityContextHolder存储通过认证的用户信息

从上边代码分析可以得知当认证成功后,会将用户登录信息存储到SecurityContextHolder#context中,但要了解SecurityContextHolder如何存储用户信息,还需要查阅该类的实现源码:

public class SecurityContextHolder {
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty("spring.security.strategy");
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

    public SecurityContextHolder() {
    }

    public static void clearContext() {
        strategy.clearContext();
    }

    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    public static int getInitializeCount() {
        return initializeCount;
    }

    private static void initialize() {
        if (!StringUtils.hasText(strategyName)) {
            strategyName = "MODE_THREADLOCAL"; // 默认策略实现
        }

        if (strategyName.equals("MODE_THREADLOCAL")) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();              // 1)本地线程存储策略(内存)     private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
        } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();   // 2)可继承本地线程存储策略(内存)private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal();
        } else if (strategyName.equals("MODE_GLOBAL")) {
            strategy = new GlobalSecurityContextHolderStrategy();                   // 3)全局策略(静态变量,内存)    private static SecurityContext contextHolder = new SecurityContextImpl();
        } else {                                                                    // 4)自定义策略
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
            } catch (Exception var2) {
                ReflectionUtils.handleReflectionException(var2);
            }
        }

        ++initializeCount;
    }

    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

    public static void setStrategyName(String strategyName) {
        SecurityContextHolder.strategyName = strategyName;
        initialize();
    }

    public static SecurityContextHolderStrategy getContextHolderStrategy() {
        return strategy;
    }

    public static SecurityContext createEmptyContext() {
        return strategy.createEmptyContext();
    }

    public String toString() {
        return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]";
    }

    static {
        initialize();
    }
}

从查阅代码可以知道SecurityContextHolder#context就是SecurityContextHolderStrategy#context。

默认SecurityContextHolderStrategy提供了三种实现,另外也支持用户自定义:

1)本地线程存储策略(内存) private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
2)可继承本地线程存储策略(内存)private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal();
3)全局策略(静态变量,内存) private static SecurityContext contextHolder = new SecurityContextImpl();
4)自定义策略。

默认SecurityContextHolderStrategy实现为:ThreadLocalSecurityContextHolderStrategy。

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();

    ThreadLocalSecurityContextHolderStrategy() {
    }

    public void clearContext() {
        contextHolder.remove();
    }

    public SecurityContext getContext() {
        SecurityContext ctx = (SecurityContext)contextHolder.get();
        if (ctx == null) {
            ctx = this.createEmptyContext();
            contextHolder.set(ctx);
        }

        return ctx;
    }

    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}

将认证后的信息存储到ThreadLocal<SecurityContext>变量中,那么就可以实现其他线程就可以共享该变量。

  但是具体另外一个请求进来时,会先经过SecurityContextPersistenceFilter,它主要具有以下功能:使用SecurityContextRepository在session中保存或更新一个SecurityContext域对象(相当于一个容器),并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 其他的过滤器都需要依赖于它。在 Spring Security 中,虽然安全上下文信息被存储于 Session 中,但我们在实际使用中不应该直接操作 Session,而应当使用 SecurityContextHolder。

三、获取认证用户信息

上边我们知道最终认证通过后Spring Security是把信息存储到了Sesssion中,但是如果要获取认证信息可以通过SecurityContextHolder去拉取:

    @GetMapping("/me")
    public LoginUser getMeDetail() {
        return UserUtil.getLoginUser();
    }

   public class UserUtil {
     public static LoginUser getLoginUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            if (authentication instanceof AnonymousAuthenticationToken) {
                return null;
            }

            if (authentication instanceof UsernamePasswordAuthenticationToken) {
                return (LoginUser) authentication.getPrincipal();
            }
        }

        return null;
     }
  }

上边这种方式只获取到我们想要的特定认证信息,另外也可以通过:

@GetMapping("/me1")
public Object getMeDetail(Authentication authentication){
    return authentication;
}

这种方式会获取用户的全部信息,包括地址等信息。如果我们只想获取用户名和密码以及它的权限,不需要ip地址等太多的信息可以使用下面的方式来获取信息。

@GetMapping("/me2")
public UserDetails getMeDetail(@AuthenticationPrincipal UserDetails userDetails){
     return userDetails;
}

至此,本文深入源码了解到了Spring Seucrity的认证流程,以及认证结果如何在多个请求之间共享的问题。也许上面的内容看的不是很清楚,你可以结合源码来解读,自己看一看源码Spring Security的认证流程会更加清晰。

后续我们将讲解如何自定账户、权限信息:第一篇文章中我们在applicationContext-shiro.xml中配置了账户、密码、用户权限,我们知道这么配置是写死的,在真实项目中需要将账户、密码、权限保存到数据库或者其他系统中,如何实现呢?

参考:

Spring Security验证流程剖析及自定义验证方法

Spring Security: 认证架构(流程)

Spring Security修炼手册(四)————Security默认表达式的权限控制

Spring Security源码分析一:Spring Security认证过程

Spring Security用户认证流程源码详解

posted @ 2020-01-18 23:49  cctext  阅读(1313)  评论(0编辑  收藏  举报