【權限管理系統】Spring security(三)---認證過程(原理解析,demo)


在前面兩節Spring security (一)架構框架-Component、Service、Filter分析Spring Security(二)--WebSecurityConfigurer配置以及filter順序為Spring Security認證作好了准備,可以讓我們更好的理解認證過程以及項目代碼編寫。

1.認證過程工作流程

認證工作流程:  

 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()方法現實主要的兩步過程:

  1. 創建擁有用戶的詳情信息的Authentication對象,在默認的UsernamePasswordAuthenticationFilter中將創建UsernamePasswordAuthenticationToken的Authentication對象;

  2. 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()進行認證,可以分成三個主要過程:

  1. AuthenticationProvide.authenticate()進行認證,默認下,將使用AbstractUserDetailsAuthenticationProvider進行認證;

  2. 認證成功后,從authentication中刪除憑據和其他機密數據,否則拋出異常或者認證失敗;

  3. 發布認證成功事件,並將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進行方法重寫協助共同工作進行認證的。主要可以分成以下步驟:

  1. 獲取用戶信息UserDetails,首先從緩存中讀取信息,如果緩存中沒有的化,在UserDetailsService中加載,其最主要可以從我們自定義的UserDetailsService進行讀取用戶信息UserDetails;

  2. 驗證三步走: 1). preAuthenticationChecks

    2). additionalAuthenticationChecks:使用PasswordEncoder.matches()方法進行認證,其驗證方式中驗證數據已經過PasswordEncoder算法加密,可以通過實現PasswordEncoder接口來定義算法加密方式。

    3). postAuthenticationChecks

  3. 將已通過驗證的用戶信息封裝成 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(三)想要實現簡單認證過程:

  1. 第一步:配置WebSecurityConfig

  2. 第二步: 實現自定義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實體)


免責聲明!

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



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