認證工作流程:
AbstractAuthenticationProcessingFilter doFilter()(attemptAuthentication()獲取Authentication實體) ->UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子類) attemptAuthentication() (在UsernamePasswordAuthenticationToken()中將username 和 password 生成 UsernamePasswordAuthenticationToken對象,getAuthenticationManager().authenticate進行認證以及返回獲取Authentication實體) ->AuthenticationManager ->ProviderManager()(AuthenticationManager接口實現) authenticate()(AuthenticationProvider.authenticate()進行認證並獲取Authentication實體) ->AbstractUserDetailsAuthenticationProvider(內置緩存機制,如果緩存中沒有用戶信息就調用retrieveUser()獲取用戶) authenticate() (獲取Authentication實體需要userDetails,在緩存中或者retrieveUser()獲取userDetails;驗證additionalAuthenticationChecks(); createSuccessAuthentication()生成Authentication實體) ->DaoAuthenticationProvider retrieveUser() (調用自定義UserDetailsService中loadUserByUsername()加載userDetails) ->UserDetailsService loadUserByUsername()(獲取userDetails)
具體流程請看下面小節。
1.1:請求首先經過過濾器AbstractAuthenticationProcessingFilter以及UsernamePasswordAuthenticationFilter進行處理
當請求來臨時,在默認情況下,請求先經過AbstractAuthenticationProcessingFilter的子類UsernamePasswordAuthenticationFilter過濾器。在UsernamePasswordAuthenticationFilter過濾器調用attemptAuthentication()方法現實主要的兩步過程:
-
創建擁有用戶的詳情信息的Authentication對象,在默認的UsernamePasswordAuthenticationFilter中將創建UsernamePasswordAuthenticationToken的Authentication對象;
-
AuthenticationManager調用authenticate()方法進行認證過程,在默認情況,使用ProviderManager類進行認證。
UsernamePasswordAuthenticationFilter源碼分析:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { .... public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { ..... //1.創建擁有用戶的詳情信息的Authentication對象 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //2.AuthenticationManager進行認證 return this.getAuthenticationManager().authenticate(authRequest); } ... }
1.2請求經過過濾器處理之后,在AuthenticationManager以及ProviderManager認證
在UsernamePasswordAuthenticationFilter中看出,將調用AuthenticationManager接口的authenticate()方法進行詳細認證。默認情況將使用AuthenticationManager子類ProviderManager的authenticate()進行認證,可以分成三個主要過程:
-
AuthenticationProvide.authenticate()進行認證,默認下,將使用AbstractUserDetailsAuthenticationProvider進行認證;
-
認證成功后,從authentication中刪除憑據和其他機密數據,否則拋出異常或者認證失敗;
-
發布認證成功事件,並將Authentication對象保存到security context中。
ProviderManager源碼分析:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... //AuthenticationProvider依次進行認證 for (AuthenticationProvider provider : getProviders()) { ... try { //1.1進行認證,並返回Authentication對象 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } ... catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { //1.2如果1.1認證中沒有一個驗證通過,則使用父類型AuthenticationManager進行驗證 result = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException e) { lastException = e; } } //2.從authentication中刪除憑據和其他機密數據 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } //3.發布認證成功事件,並將Authentication對象保存到security context中 eventPublisher.publishAuthenticationSuccess(result); return result; } }
1.3 認證過程詳細處理:AuthenticationProvider、AbstractUserDetailsAuthenticationProvider以及DaoAuthenticationProvider
在默認認證詳細處理過程中,AuthenticationProvider認證由AbstractUserDetailsAuthenticationProvider抽象類以及AbstractUserDetailsAuthenticationProvider的子類DaoAuthenticationProvider進行方法重寫協助共同工作進行認證的。主要可以分成以下步驟:
-
獲取用戶信息UserDetails,首先從緩存中讀取信息,如果緩存中沒有的化,在UserDetailsService中加載,其最主要可以從我們自定義的UserDetailsService進行讀取用戶信息UserDetails;
-
驗證三步走: 1). preAuthenticationChecks
2). additionalAuthenticationChecks:使用PasswordEncoder.matches()方法進行認證,其驗證方式中驗證數據已經過PasswordEncoder算法加密,可以通過實現PasswordEncoder接口來定義算法加密方式。
3). postAuthenticationChecks
-
將已通過驗證的用戶信息封裝成 UsernamePasswordAuthenticationToken對象並返回;該對象封裝了用戶的身份信息,以及相應的權限信息。
AbstractUserDetailsAuthenticationProvider主要功能提供authenticate()認證方法以及給DaoAuthenticationProvider重寫方法源碼分析:
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... boolean cacheWasUsed = true; //1.1獲取緩存中UserDetails信息 UserDetails user = this.userCache.getUserFromCache(username); //1.2 如果緩存中沒有信息,從UserDetailsService中獲取 if (user == null) { cacheWasUsed = false; try { //使用DaoAuthenticationProvider中重寫的方法去獲取信息 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); }catch{ ... } ... try { //進行檢驗認證 preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); }catch{ ... } ... postAuthenticationChecks.check(user); .... // 將已通過驗證的用戶信息封裝成 UsernamePasswordAuthenticationToken對象並返回 return createSuccessAuthentication(principalToReturn, authentication, user); }
DaoAuthenticationProvider功能主要為認證憑證加密PasswordEncoder,以及重寫AbstractUserDetailsAuthenticationProvider抽象類的retrieveUser、additionalAuthenticationChecks方法,其中retrieveUser主要是獲取UserDetails信息,源碼分析
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //根據UserDetailsService獲取UserDetails信息,從自定義的UserDetailsService獲取 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
additionalAuthenticationChecks主要使用PasswordEncoder進行密碼驗證,源碼分析:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); //進行密碼驗證 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
1.4 認證中所需的認證憑證獲取:UserDetailsService
在認證中必須獲取認證憑證,從UserDetailsService獲取到認證憑證,UserDetailsService接口只有一個方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
通過用戶名 username 調用方法 loadUserByUsername 返回了一個UserDetails接口對象:
public interface UserDetails extends Serializable { //1.權限集合 Collection<? extends GrantedAuthority> getAuthorities(); //2.密碼 String getPassword(); //3.用戶名 String getUsername(); //4.用戶是否過期 boolean isAccountNonExpired(); //5.是否鎖定 boolean isAccountNonLocked(); //6.用戶密碼是否過期 boolean isCredentialsNonExpired(); //7.賬號是否可用(可理解為是否刪除) boolean isEnabled(); }
我們通過實現UserDetailsService自定義獲取UserDetails類,可以從不同數據源中獲取認證憑證。
1.5 總結
總結Spring Security(二)--WebSecurityConfigurer配置以及filter順序和本節Spring security(三)想要實現簡單認證過程:
-
第一步:配置WebSecurityConfig
-
第二步: 實現自定義UserDetailsService,自定義從數據源碼獲取認證憑證。
2 Spring boot與Spring security整合
2.1配置WebSecurityConfig
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub //super.configure(http); http .csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .loginProcessingUrl("/login/form") .failureUrl("/login-error") .permitAll() //表單登錄,permitAll()表示這個不需要驗證 登錄頁面,登錄失敗頁面 .and() .logout().permitAll(); } }
2.2 UserDetailsService實現
@service public class CustomUserService implements UserDetailsService { @Autowired private UserInfoMapper userInfoMapper; @Autowired private PermissionInfoMapper permissionInfoMapper; @Autowired private BCryptPasswordEncoderService bCryptPasswordEncoderService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO Auto-generated method stub //這里可以可以通過username(登錄時輸入的用戶名)然后到數據庫中找到對應的用戶信息,並構建成我們自己的UserInfo來返回。 UserInfoDTO user = userInfoMapper.getUserInfoByUserName(username); if (user != null) { List<PermissionInfoDTO> permissionInfoDTOS = permissionInfoMapper.findByAdminUserId(userInfo.getId()); List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(); for (PermissionInfoDTO permissionInfoDTO : permissionInfoDTOS) { if (permissionInfoDTO != null && permissionInfoDTO.getPermissionName() != null) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority( permissionInfoDTO.getPermissionName()); grantedAuthorityList.add(grantedAuthority); } } return new User(userInfo.getUserName(), bCryptPasswordEncoderService.encode(userInfo.getPasswaord()), grantedAuthorityList); }else { throw new UsernameNotFoundException("admin" + username + "do not exist"); } } }
往期文章:
-
...
各位看官還可以嗎?喜歡的話,動動手指點個贊💗,點個關注唄!!謝謝支持!
也歡迎關注公眾號【Ccww筆記】,原創技術文章第一時間推出
getAuthenticationManager().authenticate進行認證以及返回獲取Authentication實體)