前一篇介紹了 Spring Security
入門的基礎準備。從今天開始咱們來一步步窺探它是如何工做的。咱們又該如何駕馭它。請多多關注公衆號: Felordcn
。本篇將經過 Spring Boot 2.x
來說解 Spring Security
中的用戶主體UserDetails
。以及從中找點樂子。java
這個簡直老生常談了。不過爲了照顧大多數仍是說一下。集成 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>
複製代碼
啓動項目,訪問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
UserDetailsService
接口。該接口只提供了一個方法:數據庫
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
複製代碼
該方法很容易理解:經過用戶名來加載用戶 。這個方法主要用於從系統數據中查詢並加載具體的用戶到Spring Security中。安全
從上面UserDetailsService
能夠知道最終交給Spring Security的是UserDetails
。該接口是提供用戶信息的核心接口。該接口實現僅僅存儲用戶的信息。後續會將該接口提供的用戶信息封裝到認證對象Authentication
中去。UserDetails
默認提供了:springboot
ROLE_
前綴{noop}
前綴若是以上的信息知足不了你使用,你能夠自行實現擴展以存儲更多的用戶信息。好比用戶的郵箱、手機號等等。一般咱們使用其實現類:mvc
org.springframework.security.core.userdetails.User
複製代碼
該類內置一個建造器UserBuilder
會很方便地幫助咱們構建UserDetails
對象,後面咱們會用到它。ide
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是否是就能夠實現咱們須要的用戶管理邏輯呢?
咱們來自定義一個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
成功進入。
通過以上的配置,相信聰明的你已經知道如何使用數據庫來管理用戶了 。只須要將 UserDetailsRepository
中的 users
屬性替代爲抽象的Dao接口就好了,不管你使用Jpa
仍是Mybatis
來實現。
今天咱們對Spring Security 中的用戶信息 UserDetails
相關進行的一些解讀。並自定義了用戶信息處理服務。相信你已經對在Spring Security中如何加載用戶信息,如何擴展用戶信息有所掌握了。後面咱們會由淺入深慢慢解讀Spring Security。相關代碼已經上傳git倉庫,關注公衆號Felordcn
後回覆ss01
獲取demo源碼。 後續也能夠及時獲取更多相關乾貨教程。
關注公衆號:Felordcn或者https://felord.cn獲取更多資訊