Spring Security UserDetails實現(xiàn)原理詳解
1. 前言
今天開始我們來一步步窺探它是如何工作的。我們又該如何駕馭它。本篇將通過 Spring Boot 2.x 來講解 Spring Security 中的用戶主體UserDetails。以及從中找點樂子。
2. Spring Boot 集成 Spring Security
這個簡直老生常談了。不過為了照顧大多數(shù)還是說一下。集成 Spring Security 只需要引入其對應(yīng)的 Starter 組件。Spring Security 不僅僅能保護(hù)Servlet Web 應(yīng)用,也可以保護(hù)Reactive Web應(yīng)用,本文我們講前者。我們只需要在 Spring Security 項目引入以下依賴即可:
<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> <!-- 測試 --> <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 (默認(rèn)值為user)和密碼 Password 。密碼在springboot控制臺會打印出類似 Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37 的字樣,后面的長串就是密碼,當(dāng)然這不是生產(chǎn)可用的。如果你足夠細(xì)心會從控制臺打印日志發(fā)現(xiàn)該隨機(jī)密碼是由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ù)會將該接口提供的用戶信息封裝到認(rèn)證對象Authentication中去。UserDetails 默認(rèn)提供了:
- 用戶的權(quán)限集, 默認(rèn)需要添加ROLE_ 前綴
- 用戶的加密后的密碼, 不加密會使用{noop}前綴
- 應(yīng)用內(nèi)唯一的用戶名
- 賬戶是否過期
- 賬戶是否鎖定
- 憑證是否過期
- 用戶是否可用
如果以上的信息滿足不了你使用,你可以自行實現(xiàn)擴(kuò)展以存儲更多的用戶信息。比如用戶的郵箱、手機(jī)號等等。通常我們使用其實現(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
源碼如下:
@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 類型負(fù)責(zé)對安全用戶實體抽象UserDetails的增刪查改操作。同時還繼承了UserDetailsService接口。
明白了上面這些讓我們把目光再回到UserDetailsServiceAutoConfiguration 上來。該類初始化了一個名為InMemoryUserDetailsManager 的內(nèi)存用戶管理器。該管理器通過配置注入了一個默認(rèn)的UserDetails存在內(nèi)存中,就是我們上面用的那個user ,每次啟動user都是動態(tài)生成的。那么問題來了如果我們定義自己的UserDetailsManager Bean是不是就可以實現(xiàn)我們需要的用戶管理邏輯呢?
3.4 自定義UserDetailsManager
我們來自定義一個UserDetailsManager 來看看能不能達(dá)到自定義用戶管理的效果。首先我們針對UserDetailsManager 的所有方法進(jìn)行一個代理的實現(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 自行實現(xiàn)具體的更新密碼邏輯 } public boolean userExists(String username) { return users.containsKey(username); } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return users.get(username); } }
該類負(fù)責(zé)具體對UserDetails 的增刪改查操作。我們將其注入Spring 容器:
@Bean public UserDetailsRepository userDetailsRepository() { UserDetailsRepository userDetailsRepository = new UserDetailsRepository(); // 為了讓我們的登錄能夠運(yùn)行 這里我們初始化一個用戶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作為加密方式。默認(rèn)Spring Security使用的也是此方式。authorities 一定不能為null 這代表用戶的角色權(quán)限集合。接下來我們實現(xiàn)一個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); } }; }
這樣實際執(zhí)行委托給了UserDetailsRepository 來做。我們重復(fù) 章節(jié)3. 的動作進(jìn)入登陸頁面分別輸入Felordcn和12345 成功進(jìn)入。
3.5 數(shù)據(jù)庫管理用戶
經(jīng)過以上的配置,相信聰明的你已經(jīng)知道如何使用數(shù)據(jù)庫來管理用戶了 。只需要將 UserDetailsRepository 中的 users 屬性替代為抽象的Dao接口就行了,無論你使用Jpa還是Mybatis來實現(xiàn)。
4. 總結(jié)
今天我們對Spring Security 中的用戶信息 UserDetails 相關(guān)進(jìn)行的一些解讀。并自定義了用戶信息處理服務(wù)。相信你已經(jīng)對在Spring Security中如何加載用戶信息,如何擴(kuò)展用戶信息有所掌握了。后面我們會由淺入深慢慢解讀Spring Security。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- SpringSecurity 自定義表單登錄的實現(xiàn)
- 解析SpringSecurity自定義登錄驗證成功與失敗的結(jié)果處理問題
- spring security自定義認(rèn)證登錄的全過程記錄
- spring security自定義登錄頁面
- Spring Security保護(hù)用戶密碼常用方法詳解
- SpringSecurity自定義成功失敗處理器的示例代碼
- Spring Security如何優(yōu)雅的增加OAuth2協(xié)議授權(quán)模式
- Spring Security實現(xiàn)不同接口安全策略方法詳解
- Spring Security自定義登錄原理及實現(xiàn)詳解
相關(guān)文章
JavaWeb實現(xiàn)同一帳號同一時間只能一個地點登陸(類似QQ登錄的功能)
最近做了企業(yè)項目,其中有這樣的需求要求同一帳號同一時間只能一個地點登陸類似QQ登錄的功能。下面小編通過本文給大家分享實現(xiàn)思路,感興趣的朋友參考下吧2016-11-11一篇文章帶你學(xué)會Spring?MVC表單標(biāo)簽
Spring MVC表單標(biāo)簽是網(wǎng)頁的可配置和可重復(fù)使用的構(gòu)建塊,下面這篇文章主要給大家介紹了如何通過一篇文章學(xué)會Spring?MVC表單標(biāo)簽的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03SpringBoot實現(xiàn)過濾器、攔截器與切片的實現(xiàn)和區(qū)別
本文詳細(xì)介紹了使用過濾器、攔截器與切片實現(xiàn)每個請求耗時的統(tǒng)計,并比較三者的區(qū)別與聯(lián)系,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02Java入門絆腳石之Override和Overload的區(qū)別詳解
重寫是子類對父類的允許訪問的方法的實現(xiàn)過程進(jìn)行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!重寫的好處在于子類可以根據(jù)需要,定義特定于自己的行為。重載是在一個類里面,方法名字相同,而參數(shù)不同。返回類型可以相同也可以不同2021-10-10MyBatis-Plus UpdateWrapper 使用常見陷阱和解決方案
MyBatis-Plus是Mybatis的一個增強(qiáng),簡化了Mybatis的開發(fā)過程,不僅保持了Mybatis原有的功能,而且在無代碼侵略下增加了許多的增強(qiáng)的功能,提供了豐富的CRUD操作,單表的CRUD操作無需編寫SQL語句,本文介紹的是UpdateWrapper的常見陷阱和對應(yīng)的解決方案,感興趣的朋友一起看看吧2024-08-08