1. 前言
今天開始我們來一步步窺探它是如何工作的。我們又該如何駕馭它。本篇將通過 Spring Boot 2.x 來講解 Spring Security 中的用戶主體UserDetails。以及從中找點樂子。
2. Spring Boot 集成 Spring Security
這個簡直老生常談了。不過為了照顧大多數(shù)還是說一下。集成 Spring Security 只需要引入其對應(yīng)的 Starter 組件。Spring Security 不僅僅能保護Servlet Web 應(yīng)用,也可以保護Reactive Web應(yīng)用,本文我們講前者。我們只需要在 Spring Security 項目引入以下依賴即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
< dependencies > <!-- actuator 指標監(jiān)控 非必須 --> < 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會跳轉(zhuǎn)到一個登錄頁面http://localhost:8080/login如下:
要求你輸入用戶名 Username (默認值為user)和密碼 Password 。密碼在springboot控制臺會打印出類似 Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37 的字樣,后面的長串就是密碼,當(dāng)然這不是生產(chǎn)可用的。如果你足夠細心會從控制臺打印日志發(fā)現(xiàn)該隨機密碼是由UserDetailsServiceAutoConfiguration 配置類生成的,我們就從它開始順藤摸瓜來一探究竟。
3.1 UserDetailsService
UserDetailsService接口。該接口只提供了一個方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
該方法很容易理解:通過用戶名來加載用戶 。這個方法主要用于從系統(tǒng)數(shù)據(jù)中查詢并加載具體的用戶到Spring Security中。
3.2 UserDetails
從上面UserDetailsService 可以知道最終交給Spring Security的是UserDetails 。該接口是提供用戶信息的核心接口。該接口實現(xiàn)僅僅存儲用戶的信息。后續(xù)會將該接口提供的用戶信息封裝到認證對象Authentication中去。UserDetails 默認提供了:
- 用戶的權(quán)限集, 默認需要添加ROLE_ 前綴
- 用戶的加密后的密碼, 不加密會使用{noop}前綴
- 應(yīng)用內(nèi)唯一的用戶名
- 賬戶是否過期
- 賬戶是否鎖定
- 憑證是否過期
- 用戶是否可用
如果以上的信息滿足不了你使用,你可以自行實現(xiàn)擴展以存儲更多的用戶信息。比如用戶的郵箱、手機號等等。通常我們使用其實現(xiàn)類:
org.springframework.security.core.userdetails.User
該類內(nèi)置一個建造器UserBuilder 會很方便地幫助我們構(gòu)建UserDetails 對象,后面我們會用到它。
3.3 UserDetailsServiceAutoConfiguration
UserDetailsServiceAutoConfiguration 全限定名為:
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
源碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
@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的情況下生效。千萬不要糾結(jié)這些類干嘛用的! 該類只初始化了一個UserDetailsManager 類型的Bean。UserDetailsManager 類型負責(zé)對安全用戶實體抽象UserDetails的增刪查改操作。同時還繼承了UserDetailsService接口。
明白了上面這些讓我們把目光再回到UserDetailsServiceAutoConfiguration 上來。該類初始化了一個名為InMemoryUserDetailsManager 的內(nèi)存用戶管理器。該管理器通過配置注入了一個默認的UserDetails存在內(nèi)存中,就是我們上面用的那個user ,每次啟動user都是動態(tài)生成的。那么問題來了如果我們定義自己的UserDetailsManager Bean是不是就可以實現(xiàn)我們需要的用戶管理邏輯呢?
3.4 自定義UserDetailsManager
我們來自定義一個UserDetailsManager 來看看能不能達到自定義用戶管理的效果。首先我們針對UserDetailsManager 的所有方法進行一個代理的實現(xiàn),我們依然將用戶存在內(nèi)存中,區(qū)別就是這是我們自定義的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
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 自行實現(xiàn)具體的更新密碼邏輯 } public boolean userExists(String username) { return users.containsKey(username); } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return users.get(username); } } |
該類負責(zé)具體對UserDetails 的增刪改查操作。我們將其注入Spring 容器:
1
2
3
4
5
6
7
8
9
|
@Bean public UserDetailsRepository userDetailsRepository() { UserDetailsRepository userDetailsRepository = new UserDetailsRepository(); // 為了讓我們的登錄能夠運行 這里我們初始化一個用戶Felordcn 密碼采用明文 當(dāng)你在密碼12345上使用了前綴{noop} 意味著你的密碼不使用加密,authorities 一定不能為空 這代表用戶的角色權(quán)限集合 UserDetails felordcn = User.withUsername( "Felordcn" ).password( "{noop}12345" ).authorities(AuthorityUtils.NO_AUTHORITIES).build(); userDetailsRepository.createUser(felordcn); return userDetailsRepository; } |
為了方便測試 我們也內(nèi)置一個名稱為Felordcn 密碼為12345的UserDetails用戶,密碼采用明文 當(dāng)你在密碼12345上使用了前綴{noop} 意味著你的密碼不使用加密,這里我們并沒有指定密碼加密方式你可以使用PasswordEncoder 來指定一種加密方式。通常推薦使用Bcrypt作為加密方式。默認Spring Security使用的也是此方式。authorities 一定不能為null 這代表用戶的角色權(quán)限集合。接下來我們實現(xiàn)一個UserDetailsManager 并注入Spring 容器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@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); } }; } |
這樣實際執(zhí)行委托給了UserDetailsRepository 來做。我們重復(fù) 章節(jié)3. 的動作進入登陸頁面分別輸入Felordcn和12345 成功進入。
3.5 數(shù)據(jù)庫管理用戶
經(jīng)過以上的配置,相信聰明的你已經(jīng)知道如何使用數(shù)據(jù)庫來管理用戶了 。只需要將 UserDetailsRepository 中的 users 屬性替代為抽象的Dao接口就行了,無論你使用Jpa還是Mybatis來實現(xiàn)。
4. 總結(jié)
今天我們對Spring Security 中的用戶信息 UserDetails 相關(guān)進行的一些解讀。并自定義了用戶信息處理服務(wù)。相信你已經(jīng)對在Spring Security中如何加載用戶信息,如何擴展用戶信息有所掌握了。后面我們會由淺入深慢慢解讀Spring Security。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://blog.51cto.com/14901317/2529093