Spring Security 實(shí)戰(zhàn)干貨——搞清楚UserDetails
1. 前言
前一篇介紹了 Spring Security 入門的基礎(chǔ)準(zhǔn)備。從今天開始我們來(lái)一步步窺探它是如何工作的。我們又該如何駕馭它。請(qǐng)多多關(guān)注公眾號(hào): Felordcn 。本篇將通過(guò) Spring Boot 2.x 來(lái)講解 Spring Security 中的用戶主體UserDetails。以及從中找點(diǎn)樂(lè)子。
2. Spring Boot 集成 Spring Security
這個(gè)簡(jiǎn)直老生常談了。不過(guò)為了照顧大多數(shù)還是說(shuō)一下。集成 Spring Security 只需要引入其對(duì)應(yīng)的 Starter 組件。Spring Security 不僅僅能保護(hù)Servlet Web 應(yīng)用,也可以保護(hù)Reactive Web應(yīng)用,本文我們講前者。我們只需要在 Spring Security 項(xiàng)目引入以下依賴即可:
<dependencies>
<!-- actuator 指標(biāo)監(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>
<!-- 測(cè)試 -->
<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
啟動(dòng)項(xiàng)目,訪問(wèn)Actuator端點(diǎn)http://localhost:8080/actuator會(huì)跳轉(zhuǎn)到一個(gè)登錄頁(yè)面http://localhost:8080/login如下:
要求你輸入用戶名 Username (默認(rèn)值為user)和密碼 Password 。密碼在springboot控制臺(tái)會(huì)打印出類似 Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37 的字樣,后面的長(zhǎng)串就是密碼,當(dāng)然這不是生產(chǎn)可用的。如果你足夠細(xì)心會(huì)從控制臺(tái)打印日志發(fā)現(xiàn)該隨機(jī)密碼是由UserDetailsServiceAutoConfiguration 配置類生成的,我們就從它開始順藤摸瓜來(lái)一探究竟。
3.1 UserDetailsService
UserDetailsService接口。該接口只提供了一個(gè)方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
該方法很容易理解:通過(guò)用戶名來(lái)加載用戶 。這個(gè)方法主要用于從系統(tǒng)數(shù)據(jù)中查詢并加載具體的用戶到Spring Security中。
3.2 UserDetails
從上面UserDetailsService 可以知道最終交給Spring Security的是UserDetails 。該接口是提供用戶信息的核心接口。該接口實(shí)現(xiàn)僅僅存儲(chǔ)用戶的信息。后續(xù)會(huì)將該接口提供的用戶信息封裝到認(rèn)證對(duì)象Authentication中去。UserDetails 默認(rèn)提供了:
用戶的權(quán)限集, 默認(rèn)需要添加ROLE_ 前綴
用戶的加密后的密碼, 不加密會(huì)使用{noop}前綴
應(yīng)用內(nèi)唯一的用戶名
賬戶是否過(guò)期
賬戶是否鎖定
憑證是否過(guò)期
用戶是否可用
如果以上的信息滿足不了你使用,你可以自行實(shí)現(xiàn)擴(kuò)展以存儲(chǔ)更多的用戶信息。比如用戶的郵箱、手機(jī)號(hào)等等。通常我們使用其實(shí)現(xiàn)類:
org.springframework.security.core.userdetails.User
該類內(nèi)置一個(gè)建造器UserBuilder 會(huì)很方便地幫助我們構(gòu)建UserDetails 對(duì)象,后面我們會(huì)用到它。
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;
}
}
我們來(lái)簡(jiǎn)單解讀一下該類,從@Conditional系列注解我們知道該類在類路徑下存在AuthenticationManager、在Spring 容器中存在Bean ObjectPostProcessor并且不存在Bean AuthenticationManager, AuthenticationProvider, UserDetailsService的情況下生效。千萬(wàn)不要糾結(jié)這些類干嘛用的! 該類只初始化了一個(gè)UserDetailsManager 類型的Bean。UserDetailsManager 類型負(fù)責(zé)對(duì)安全用戶實(shí)體抽象UserDetails的增刪查改操作。同時(shí)還繼承了UserDetailsService接口。
明白了上面這些讓我們把目光再回到UserDetailsServiceAutoConfiguration 上來(lái)。該類初始化了一個(gè)名為InMemoryUserDetailsManager 的內(nèi)存用戶管理器。該管理器通過(guò)配置注入了一個(gè)默認(rèn)的UserDetails存在內(nèi)存中,就是我們上面用的那個(gè)user ,每次啟動(dòng)user都是動(dòng)態(tài)生成的。那么問(wèn)題來(lái)了如果我們定義自己的UserDetailsManager Bean是不是就可以實(shí)現(xiàn)我們需要的用戶管理邏輯呢?
3.4 自定義UserDetailsManager
我們來(lái)自定義一個(gè)UserDetailsManager 來(lái)看看能不能達(dá)到自定義用戶管理的效果。首先我們針對(duì)UserDetailsManager 的所有方法進(jìn)行一個(gè)代理的實(shí)現(xiàn),我們依然將用戶存在內(nèi)存中,區(qū)別就是這是我們自定義的:
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 自行實(shí)現(xiàn)具體的更新密碼邏輯
}
public boolean userExists(String username) {
return users.containsKey(username);
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.get(username);
}
}
該類負(fù)責(zé)具體對(duì)UserDetails 的增刪改查操作。我們將其注入Spring 容器:
@Bean
public UserDetailsRepository userDetailsRepository() {
UserDetailsRepository userDetailsRepository = new UserDetailsRepository();
// 為了讓我們的登錄能夠運(yùn)行 這里我們初始化一個(gè)用戶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;
}
為了方便測(cè)試 我們也內(nèi)置一個(gè)名稱為Felordcn 密碼為12345的UserDetails用戶,密碼采用明文 當(dāng)你在密碼12345上使用了前綴{noop} 意味著你的密碼不使用加密,這里我們并沒(méi)有指定密碼加密方式你可以使用PasswordEncoder 來(lái)指定一種加密方式。通常推薦使用Bcrypt作為加密方式。默認(rèn)Spring Security使用的也是此方式。authorities 一定不能為null 這代表用戶的角色權(quán)限集合。接下來(lái)我們實(shí)現(xiàn)一個(gè)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);
}
};
}
這樣實(shí)際執(zhí)行委托給了UserDetailsRepository 來(lái)做。我們重復(fù) 章節(jié)3. 的動(dòng)作進(jìn)入登陸頁(yè)面分別輸入Felordcn和12345 成功進(jìn)入。
3.5 數(shù)據(jù)庫(kù)管理用戶
經(jīng)過(guò)以上的配置,相信聰明的你已經(jīng)知道如何使用數(shù)據(jù)庫(kù)來(lái)管理用戶了 。只需要將 UserDetailsRepository 中的 users 屬性替代為抽象的Dao接口就行了,無(wú)論你使用Jpa還是Mybatis來(lái)實(shí)現(xiàn)。
4. 總結(jié)
今天我們對(duì)Spring Security 中的用戶信息 UserDetails 相關(guān)進(jìn)行的一些解讀。并自定義了用戶信息處理服務(wù)。相信你已經(jīng)對(duì)在Spring Security中如何加載用戶信息,如何擴(kuò)展用戶信息有所掌握了。后面我們會(huì)由淺入深慢慢解讀Spring Security。相關(guān)代碼已經(jīng)上傳git倉(cāng)庫(kù),關(guān)注公眾號(hào)Felordcn 后回復(fù)ss01 獲取demo源碼。 后續(xù)也可以及時(shí)獲取更多相關(guān)干貨教程。
作者:碼農(nóng)小胖哥
歡迎關(guān)注:碼農(nóng)小胖哥