Spring Security 實戰乾貨:用戶信息UserDetails相關入門

1. 前言

前一篇介紹了 Spring Security 入門的基礎準備。從今天開始咱們來一步步窺探它是如何工做的。咱們又該如何駕馭它。請多多關注公衆號: Felordcn 。本篇將經過 Spring Boot 2.x 來說解 Spring Security 中的用戶主體UserDetails。以及從中找點樂子。java

2. Spring Boot 集成 Spring Security

這個簡直老生常談了。不過爲了照顧大多數仍是說一下。集成 Spring Security 只須要引入其對應的 Starter 組件。Spring Security 不只僅能保護Servlet Web 應用,也能夠保護Reactive Web應用,本文咱們講前者。咱們只須要在 Spring Security 項目引入如下依賴便可:git

<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以下:web

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

3.1 UserDetailsService

UserDetailsService接口。該接口只提供了一個方法:數據庫

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
複製代碼

該方法很容易理解:經過用戶名來加載用戶 。這個方法主要用於從系統數據中查詢並加載具體的用戶到Spring Security中。安全

3.2 UserDetails

從上面UserDetailsService 能夠知道最終交給Spring Security的是UserDetails 。該接口是提供用戶信息的核心接口。該接口實現僅僅存儲用戶的信息。後續會將該接口提供的用戶信息封裝到認證對象Authentication中去。UserDetails 默認提供了:springboot

  • 用戶的權限集, 默認須要添加ROLE_ 前綴
  • 用戶的加密後的密碼, 不加密會使用{noop}前綴
  • 應用內惟一的用戶名
  • 帳戶是否過時
  • 帳戶是否鎖定
  • 憑證是否過時
  • 用戶是否可用

若是以上的信息知足不了你使用,你能夠自行實現擴展以存儲更多的用戶信息。好比用戶的郵箱、手機號等等。一般咱們使用其實現類:mvc

org.springframework.security.core.userdetails.User
複製代碼

該類內置一個建造器UserBuilder 會很方便地幫助咱們構建UserDetails 對象,後面咱們會用到它。ide

3.3 UserDetailsServiceAutoConfiguration

UserDetailsServiceAutoConfiguration 全限定名爲:spring-boot

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 密碼爲12345UserDetails用戶,密碼採用明文 當你在密碼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. 的動做進入登錄頁面分別輸入Felordcn12345 成功進入。

3.5 數據庫管理用戶

通過以上的配置,相信聰明的你已經知道如何使用數據庫來管理用戶了 。只須要將 UserDetailsRepository 中的 users 屬性替代爲抽象的Dao接口就好了,不管你使用Jpa仍是Mybatis來實現。

4. 總結

今天咱們對Spring Security 中的用戶信息 UserDetails 相關進行的一些解讀。並自定義了用戶信息處理服務。相信你已經對在Spring Security中如何加載用戶信息,如何擴展用戶信息有所掌握了。後面咱們會由淺入深慢慢解讀Spring Security。相關代碼已經上傳git倉庫,關注公衆號Felordcn 後回覆ss01 獲取demo源碼。 後續也能夠及時獲取更多相關乾貨教程。

關注公衆號:Felordcn或者https://felord.cn獲取更多資訊

相關文章
相關標籤/搜索