文章目錄
1. 前言
前一篇介紹了 Spring Security
入門的基礎准備。從今天開始我們來一步步窺探它是如何工作的。我們又該如何駕馭它。請多多關注公眾號: Felordcn
。本篇將通過 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
如下:
要求你輸入用戶名 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。相關代碼已經上傳git倉庫,關注公眾號Felordcn
后回復ss01
獲取demo源碼。 后續也可以通過及時獲取更多相關干貨教程。
關注公眾號:Felordcn或者https://felord.cn獲取更多資訊