讓我們帶着以下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
- successfulAuthentication(...)源碼:
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修煉手冊(四)————Security默認表達式的權限控制》