天天看點

SpringSecurity中常用到的最要實作

SpringSecurity中最要的三個實作

  • ​​寫在前面​​
  • ​​一、核心配置類 WebSecurityConfigurerAdapter​​
  • ​​二、使用者/權限加載接口 UserDetailsService​​
  • ​​三、接入控制管理(AccessDecisionManager)​​
  • ​​四、請求攔截(OncePerRequestFilter)​​
  • ​​五、核心驗證器(AuthenticationManager)​​
  • ​​5.1、AuthenticationManager​​
  • ​​5.2、ProviderManager​​
  • ​​5.3、AuthenticationProvider​​
  • ​​六、驗證流程圖,分析​​

寫在前面

一、核心配置類 WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .csrf()
        .disable()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        //.and()
            /*.formLogin()
            .successHandler(loginSuccessHandler())
            .failureHandler(authenticationFailureHandler())*/
        .and()
            .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
        .and()
            .logout()
            .logoutSuccessHandler(logoutSuccessHandler())
        .and()
            .userDetailsService(userDetailsService)
            //OPTIONS請求全部放行
            .authorizeRequests()
            .antMatchers(/*HttpMethod.OPTIONS,*/ "/**").permitAll()
            .antMatchers("/auth/login").permitAll()
            .antMatchers("/auth/logout").permitAll()
            .accessDecisionManager(accessDecisionManager());
      //使用自定義的 Token過濾器 驗證請求的Token是否合法
        http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
        super.configure(http);
    }

    @Bean
    public JwtTokenFilter authenticationTokenFilterBean() {
        return new JwtTokenFilter();
    }
    @Bean
    public JwtTokenProvider jwtTokenProvider() {
        return new JwtTokenProvider();
    }

}      

二、使用者/權限加載接口 UserDetailsService

@Component("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Override
    public UserDetails loadUserByUsername(String userCode) throws UsernameNotFoundException {
        UserInfo userInfo = userService.findByUserCode(userCode);
        if (userInfo == null) {
            throw new UsernameNotFoundException("-------UserDetailsService---------->" + userCode);
        }
        List<String> resources = userService.findAllResourcesByUserCode(userCode);

        Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        resources.forEach(code -> {
            GrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code);
            grantedAuthorities.add(simpleGrantedAuthority);
        });
        return new User(userCode, userInfo.getPassword(), grantedAuthorities);
    }
}      

三、接入控制管理(AccessDecisionManager)

@Component("accessDecisionManager")
public class AuthAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        // 鑒權驗證
        if (!configAttributes.isEmpty()) {
            String authorize = null;
            for (ConfigAttribute configAttribute : configAttributes) {
                System.out.println("-------AccessDecisionManager--------------" + configAttribute.toString());
                authorize = configAttribute.toString();
                break;
            }
            if (Constants.NO_AUTHORIZE.equals(authorize)) {
                return;
            }
        }
        // 登陸驗證
SecurityUtils.getCurrentUserLogin());
        if (Constants.ANONYMOUS_USER.equals(SecurityUtils.getCurrentUserLogin())) {
            throw new AccessDeniedException(" 沒有權限通路! ");
        }
        //驗證權限
        if (object instanceof FilterInvocation) {
            FilterInvocation web = (FilterInvocation) object;
            String uri = web.getRequestUrl();
            System.out.println("-------AccessDecisionManager--------------" + uri);

            String urlCode = this.getUrlCode(uri);
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

            List<? extends GrantedAuthority> collect = authorities.stream().collect(Collectors.toList());
            for (GrantedAuthority grantedAuthority : collect) {
                String authority = grantedAuthority.getAuthority();
                if (authority.equals(urlCode)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException(" 沒有權限通路! ");
    }


}      

四、請求攔截(OncePerRequestFilter)

是封裝在 org.springframework.web.filter 這個包下的,可用于攔截請求,驗證請求頭中的資訊,或者初始基本資訊

springSecurity中可用這個配置。如下的環境

SpringSecurity中常用到的最要實作

五、核心驗證器(AuthenticationManager)

5.1、AuthenticationManager

該對象提供了認證方法的入口,接收一個Authentiaton對象作為參數;

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

5.2、ProviderManager

它是 AuthenticationManager 的一個實作類,提供了基本的認證邏輯和方法;它包含了一個 List 對象,通過 AuthenticationProvider 接口來擴充出不同的認證提供者(當Spring Security預設提供的實作類不能滿足需求的時候可以擴充AuthenticationProvider 覆寫supports(Class<?> authentication) 方法);

驗證邏輯

AuthenticationManager 接收 Authentication 對象作為參數,并通過 authenticate(Authentication) 方法對其進行驗證;AuthenticationProvider實作類用來支撐對 Authentication 對象的驗證動作;UsernamePasswordAuthenticationToken實作了 Authentication主要是将使用者輸入的使用者名和密碼進行封裝,并供給 AuthenticationManager 進行驗證;驗證完成以後将傳回一個認證成功的 Authentication 對象;

Authentication

Authentication對象中的主要方法

public interface Authentication extends Principal, Serializable {
  //#1.權限結合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")傳回字元串權限集合
  Collection<? extends GrantedAuthority> getAuthorities();
  
  //#2.使用者名密碼認證時可以了解為密碼
  Object getCredentials();
  
  //#3.認證時包含的一些資訊。
  Object getDetails();
  
  //#4.使用者名密碼認證時可了解時使用者名
  Object getPrincipal();
  
  #5.是否被認證,認證為true  
  boolean isAuthenticated();
  
  #6.設定是否能被認證
  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
  ...
  ...
  }      

ProviderManager是AuthenticationManager的實作類,提供了基本認證實作邏輯和流程;

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
    //#1.擷取目前的Authentication的認證類型
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    boolean debug = logger.isDebugEnabled();
    //#2.周遊所有的providers使用supports方法判斷該provider是否支援目前的認證類型,不支援的話繼續周遊
    for (AuthenticationProvider provider : getProviders()) {
      if (!provider.supports(toTest)) {
        continue;
      }

      if (debug) {
        logger.debug("Authentication attempt using "
            + provider.getClass().getName());
      }

      try {
        #3.支援的話調用provider的authenticat方法認證
        result = provider.authenticate(authentication);

        if (result != null) {
          #4.認證通過的話重新生成Authentication對應的Token
          copyDetails(authentication, result);
          break;
        }
      }
      catch (AccountStatusException e) {
        prepareException(e, authentication);
        // SEC-546: Avoid polling additional providers if auth failure is due to
        // invalid account status
        throw e;
      }
      catch (InternalAuthenticationServiceException e) {
        prepareException(e, authentication);
        throw e;
      }
      catch (AuthenticationException e) {
        lastException = e;
      }
    }

    if (result == null && parent != null) {
      // Allow the parent to try.
      try {
        #5.如果#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;
      }
    }
    #6. 是否擦出敏感資訊
    if (result != null) {
      if (eraseCredentialsAfterAuthentication
          && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
      }

      eventPublisher.publishAuthenticationSuccess(result);
      return result;
    }

    // Parent was null, or didn't authenticate (or throw an exception).

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

    prepareException(lastException, authentication);

    throw lastException;
  }      
  • 1.周遊所有的 Providers,然後依次執行該 Provider 的驗證方法

    如果某一個 Provider 驗證成功,則跳出循環不再執行後續的驗證;

    如果驗證成功,會将傳回的 result 既 Authentication 對象進一步封裝為 Authentication Token; 比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;這些 Authentication Token 也都繼承自 Authentication 對象;

  • 2.如果 #1 沒有任何一個 Provider 驗證成功,則試圖使用其 parent Authentication Manager 進行驗證;
  • 3.是否需要擦除密碼等敏感資訊;

5.3、AuthenticationProvider

ProviderManager 通過 AuthenticationProvider 擴充出更多的驗證提供的方式;而 AuthenticationProvider 本身也就是一個接口,從類圖中我們可以看出它的實作類AbstractUserDetailsAuthenticationProvider 和AbstractUserDetailsAuthenticationProvider的子類DaoAuthenticationProvider 。DaoAuthenticationProvider 是Spring Security中一個核心的Provider,對所有的資料庫提供了基本方法和入口。

DaoAuthenticationProvider

DaoAuthenticationProvider主要做了以下事情

對使用者身份盡心加密操作;

#1.可直接傳回BCryptPasswordEncoder,也可以自己實作該接口使用自己的加密算法核心方法String encode(CharSequence rawPassword);和boolean matches(CharSequence rawPassword, String encodedPassword);

private PasswordEncoder passwordEncoder;

實作了 AbstractUserDetailsAuthenticationProvider 兩個抽象方法,

擷取使用者資訊的擴充點

protected final UserDetails retrieveUser(String username,

UsernamePasswordAuthenticationToken authentication)

throws AuthenticationException {

UserDetails loadedUser;

try {

loadedUser = this.getUserDetailsService().loadUserByUsername(username);

}

主要是通過注入UserDetailsService接口對象,并調用其接口方法 loadUserByUsername(String username) 擷取得到相關的使用者資訊。UserDetailsService接口非常重要。

實作 additionalAuthenticationChecks 的驗證方法(主要驗證密碼);

AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider為DaoAuthenticationProvider提供了基本的認證方法;

public Authentication authenticate(Authentication authentication)

throws AuthenticationException {

Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,

messages.getMessage(

“AbstractUserDetailsAuthenticationProvider.onlySupports”,

“Only UsernamePasswordAuthenticationToken is supported”));

// Determine username
  String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
      : authentication.getName();

  boolean cacheWasUsed = true;
  UserDetails user = this.userCache.getUserFromCache(username);

  if (user == null) {
    cacheWasUsed = false;

    try {
      #1.擷取使用者資訊由子類實作即DaoAuthenticationProvider
      user = retrieveUser(username,
          (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (UsernameNotFoundException notFound) {
      logger.debug("User '" + username + "' not found");

      if (hideUserNotFoundExceptions) {
        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
      }
      else {
        throw notFound;
      }
    }

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

  try {
    #2.前檢查由DefaultPreAuthenticationChecks類實作(主要判斷目前使用者是否鎖定,過期,當機User接口)
    preAuthenticationChecks.check(user);
    #3.子類實作
    additionalAuthenticationChecks(user,
        (UsernamePasswordAuthenticationToken) authentication);
  }
  catch (AuthenticationException exception) {
    if (cacheWasUsed) {
      // There was a problem, so try again after checking
      // we're using latest data (i.e. not from the cache)
      cacheWasUsed = false;
      user = retrieveUser(username,
          (UsernamePasswordAuthenticationToken) authentication);
      preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user,
          (UsernamePasswordAuthenticationToken) authentication);
    }
    else {
      throw exception;
    }
  }
  #4.檢測使用者密碼是否過期對應#2 的User接口
  postAuthenticationChecks.check(user);

  if (!cacheWasUsed) {
    this.userCache.putUserInCache(user);
  }

  Object principalToReturn = user;

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

  return createSuccessAuthentication(principalToReturn, authentication, user);
}      

AbstractUserDetailsAuthenticationProvider主要實作了AuthenticationProvider的接口方法 authenticate 并提供了相關的驗證邏輯;

擷取使用者傳回UserDetails AbstractUserDetailsAuthenticationProvider定義了一個抽象的方法

protected abstract UserDetails retrieveUser(String username,

UsernamePasswordAuthenticationToken authentication)

throws AuthenticationException;

三步驗證工作

preAuthenticationChecks

additionalAuthenticationChecks(抽象方法,子類實作)

postAuthenticationChecks

将已認證驗證的使用者資訊封裝成 UsernamePasswordAuthenticationToken 對象并傳回;該對象封裝了使用者的身份資訊,以及相應的權限資訊,相關源碼如下,

protected Authentication createSuccessAuthentication(Object principal,

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(

principal, authentication.getCredentials(),

authoritiesMapper.mapAuthorities(user.getAuthorities()));

result.setDetails(authentication.getDetails());

return result;      

六、驗證流程圖,分析