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用戶認證流程源碼詳解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM