1. 程式人生 > 程式設計 >Spring Security UserDetails實現原理詳解

Spring Security UserDetails實現原理詳解

1. 前言

今天開始我們來一步步窺探它是如何工作的。我們又該如何駕馭它。本篇將通過 Spring Boot 2.x 來講解 Spring Security 中的使用者主體UserDetails。以及從中找點樂子。

2. Spring Boot 整合 Spring Security

這個簡直老生常談了。不過為了照顧大多數還是說一下。整合 Spring Security 只需要引入其對應的 Starter 元件。Spring Security 不僅僅能保護Servlet Web 應用,也可以保護Reactive Web應用,本文我們講前者。我們只需要在 Spring Security 專案引入以下依賴即可:

  <dependencies>
    <!-- actuator 指標監控 非必須 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- spring security starter 必須 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- spring mvc servlet web 必須 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--  lombok 外掛 非必須    -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <!-- 測試  -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

3. UserDetailsServiceAutoConfiguration

啟動專案,訪問Actuator端點http://localhost:8080/actuator會跳轉到一個登入頁面http://localhost:8080/login如下:

Spring Security UserDetails實現原理詳解

要求你輸入使用者名稱 Username (預設值為user)和密碼 Password 。密碼在springboot控制檯會打印出類似 Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37 的字樣,後面的長串就是密碼,當然這不是生產可用的。如果你足夠細心會從控制檯列印日誌發現該隨機密碼是由UserDetailsServiceAutoConfiguration 配置類生成的,我們就從它開始順藤摸瓜來一探究竟。

3.1 UserDetailsService

UserDetailsService介面。該介面只提供了一個方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

該方法很容易理解:通過使用者名稱來載入使用者 。這個方法主要用於從系統資料中查詢並載入具體的使用者到Spring Security中。

3.2 UserDetails

從上面UserDetailsService 可以知道最終交給Spring Security的是UserDetails 。該介面是提供使用者資訊的核心介面。該介面實現僅僅儲存使用者的資訊。後續會將該介面提供的使用者資訊封裝到認證物件Authentication中去。UserDetails 預設提供了:

  • 使用者的許可權集, 預設需要新增ROLE_ 字首
  • 使用者的加密後的密碼, 不加密會使用{noop}字首
  • 應用內唯一的使用者名稱
  • 賬戶是否過期
  • 賬戶是否鎖定
  • 憑證是否過期
  • 使用者是否可用

如果以上的資訊滿足不了你使用,你可以自行實現擴充套件以儲存更多的使用者資訊。比如使用者的郵箱、手機號等等。通常我們使用其實現類:

org.springframework.security.core.userdetails.User

該類內建一個建造器UserBuilder 會很方便地幫助我們構建UserDetails 物件,後面我們會用到它。

3.3 UserDetailsServiceAutoConfiguration

UserDetailsServiceAutoConfiguration 全限定名為:

org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration

原始碼如下:

@Configuration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean({ AuthenticationManager.class,AuthenticationProvider.class,UserDetailsService.class })
public class UserDetailsServiceAutoConfiguration {

  private static final String NOOP_PASSWORD_PREFIX = "{noop}";

  private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

  private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

  @Bean
  @ConditionalOnMissingBean(
      type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
  @Lazy
  public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,ObjectProvider<PasswordEncoder> passwordEncoder){
    SecurityProperties.User user = properties.getUser();
    List<String> roles = user.getRoles();
    return new InMemoryUserDetailsManager(
        User.withUsername(user.getName()).password(getOrDeducePassword(user,passwordEncoder.getIfAvailable()))
            .roles(StringUtils.toStringArray(roles)).build());
  }

  private String getOrDeducePassword(SecurityProperties.User user,PasswordEncoder encoder) {
    String password = user.getPassword();
    if (user.isPasswordGenerated()) {
      logger.info(String.format("%n%nUsing generated security password: %s%n",user.getPassword()));
    }
    if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
      return password;
    }
    return NOOP_PASSWORD_PREFIX + password;
  }

}

我們來簡單解讀一下該類,從@Conditional系列註解我們知道該類在類路徑下存在AuthenticationManager、在Spring 容器中存在Bean ObjectPostProcessor並且不存在Bean AuthenticationManager,AuthenticationProvider,UserDetailsService的情況下生效。千萬不要糾結這些類幹嘛用的! 該類只初始化了一個UserDetailsManager 型別的Bean。UserDetailsManager 型別負責對安全使用者實體抽象UserDetails的增刪查改操作。同時還繼承了UserDetailsService介面。

明白了上面這些讓我們把目光再回到UserDetailsServiceAutoConfiguration 上來。該類初始化了一個名為InMemoryUserDetailsManager 的記憶體使用者管理器。該管理器通過配置注入了一個預設的UserDetails存在記憶體中,就是我們上面用的那個user ,每次啟動user都是動態生成的。那麼問題來了如果我們定義自己的UserDetailsManager Bean是不是就可以實現我們需要的使用者管理邏輯呢?

3.4 自定義UserDetailsManager

我們來自定義一個UserDetailsManager 來看看能不能達到自定義使用者管理的效果。首先我們針對UserDetailsManager 的所有方法進行一個代理的實現,我們依然將使用者存在記憶體中,區別就是這是我們自定義的:

package cn.felord.spring.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.HashMap;
import java.util.Map;

/**
 * 代理 {@link org.springframework.security.provisioning.UserDetailsManager} 所有功能
 *
 * @author Felordcn
 */
public class UserDetailsRepository {

  private Map<String,UserDetails> users = new HashMap<>();

  public void createUser(UserDetails user) {
    users.putIfAbsent(user.getUsername(),user);
  }

  public void updateUser(UserDetails user) {
    users.put(user.getUsername(),user);
  }

  public void deleteUser(String username) {
    users.remove(username);
  }

  public void changePassword(String oldPassword,String newPassword) {
    Authentication currentUser = SecurityContextHolder.getContext()
        .getAuthentication();

    if (currentUser == null) {
      // This would indicate bad coding somewhere
      throw new AccessDeniedException(
          "Can't change password as no Authentication object found in context "
              + "for current user.");
    }

    String username = currentUser.getName();

    UserDetails user = users.get(username);

    if (user == null) {
      throw new IllegalStateException("Current user doesn't exist in database.");
    }

    // todo copy InMemoryUserDetailsManager 自行實現具體的更新密碼邏輯
  }

  public boolean userExists(String username) {

    return users.containsKey(username);
  }

  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return users.get(username);
  }
}

該類負責具體對UserDetails 的增刪改查操作。我們將其注入Spring 容器:

  @Bean
  public UserDetailsRepository userDetailsRepository() {
    UserDetailsRepository userDetailsRepository = new UserDetailsRepository();

    // 為了讓我們的登入能夠執行 這裡我們初始化一個使用者Felordcn 密碼採用明文 當你在密碼12345上使用了字首{noop} 意味著你的密碼不使用加密,authorities 一定不能為空 這代表使用者的角色許可權集合
    UserDetails felordcn = User.withUsername("Felordcn").password("{noop}12345").authorities(AuthorityUtils.NO_AUTHORITIES).build();
    userDetailsRepository.createUser(felordcn);
    return userDetailsRepository;
  }

為了方便測試 我們也內建一個名稱為Felordcn 密碼為12345的UserDetails使用者,密碼採用明文 當你在密碼12345上使用了字首{noop} 意味著你的密碼不使用加密,這裡我們並沒有指定密碼加密方式你可以使用PasswordEncoder 來指定一種加密方式。通常推薦使用Bcrypt作為加密方式。預設Spring Security使用的也是此方式。authorities 一定不能為null 這代表使用者的角色許可權集合。接下來我們實現一個UserDetailsManager 並注入Spring 容器:

  @Bean
  public UserDetailsManager userDetailsManager(UserDetailsRepository userDetailsRepository) {
    return new UserDetailsManager() {
      @Override
      public void createUser(UserDetails user) {
        userDetailsRepository.createUser(user);
      }

      @Override
      public void updateUser(UserDetails user) {
        userDetailsRepository.updateUser(user);
      }

      @Override
      public void deleteUser(String username) {
        userDetailsRepository.deleteUser(username);
      }

      @Override
      public void changePassword(String oldPassword,String newPassword) {
        userDetailsRepository.changePassword(oldPassword,newPassword);
      }

      @Override
      public boolean userExists(String username) {
        return userDetailsRepository.userExists(username);
      }

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userDetailsRepository.loadUserByUsername(username);
      }
    };
  }

這樣實際執行委託給了UserDetailsRepository 來做。我們重複 章節3. 的動作進入登陸頁面分別輸入Felordcn和12345 成功進入。

3.5 資料庫管理使用者

經過以上的配置,相信聰明的你已經知道如何使用資料庫來管理使用者了 。只需要將 UserDetailsRepository 中的 users 屬性替代為抽象的Dao介面就行了,無論你使用Jpa還是Mybatis來實現。

4. 總結

今天我們對Spring Security 中的使用者資訊 UserDetails 相關進行的一些解讀。並自定義了使用者資訊處理服務。相信你已經對在Spring Security中如何載入使用者資訊,如何擴充套件使用者資訊有所掌握了。後面我們會由淺入深慢慢解讀Spring Security。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。