Spring Security 源碼分析(一)【結構總覽】:https://yuanyu.blog.csdn.net/article/details/105469059
1 認證流程
- 用戶提交用戶名、密碼被SecurityFilterChain中的UsernamePasswordAuthenticationFilter過濾器獲取到,封裝爲請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。
- 然後過濾器將Authentication提交至認證管理器(AuthenticationManager)進行認證
- 認證成功後, AuthenticationManager身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼通常會被移除) Authentication實例
- SecurityContextHolder安全上下文容器將第3步填充了信息的 Authentication ,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中;可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它的實現類爲ProviderManager;而Spring Security支持多種認證方式,因此ProviderManager維護着一個List<AuthenticationProvider> 列表,存放多種認證方式,最終實際的認證工作是由AuthenticationProvider完成的;咱們知道web表單的對應的AuthenticationProvider實現類爲DaoAuthenticationProvider,它的內部又維護着一個UserDetailsService負責UserDetails的獲取;最終AuthenticationProvider將UserDetails填充至Authentication
//org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
//...
Authentication authResult;
try {
//
authResult = attemptAuthentication(request, response);
if (authResult == null) {return;}
//
essionStrategy.onAuthentication(authResult, request, response);
}
//...
//SecurityContextHolder中設置Authentication
successfulAuthentication(request, response, chain, authResult);
}
//org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult) {
SecurityContextHolder.getContext().setAuthentication(authResult);
}
//org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { //打斷點
//...
String username = obtainUsername(request);
String password = obtainPassword(request);
//...
//封裝Authentication
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//...
//AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}
認證
//public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
//org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
public Authentication authenticate(Authentication authentication) {
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) { //緩存沒有
cacheWasUsed = false;
try {
//查詢數據庫 打斷點
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
}
try {
preAuthenticationChecks.check(user);
//比對密碼 打斷點
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
protected final UserDetails retrieveUser(String username, //打斷點
UsernamePasswordAuthenticationToken authentication)
//根據賬號查詢用戶信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
//org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication){
//
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { //打斷點
2 AuthenticationProvider
通過前面的Spring Security認證流程我們得知,認證管理器(AuthenticationManager)委託 AuthenticationProvider完成認證工作
AuthenticationProvider是一個接口,定義如下:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
authenticate()方法定義了認證的實現過程,它的參數是一個Authentication,裏面包含了登錄用戶所提交的用戶名、密碼等;而返回值也是一個Authentication,這個Authentication則是在認證成功後,將用戶的權限及其他信 息重新組裝後生成
Spring Security中維護着一個List<AuthenticationProvider> 列表,存放多種認證方式,不同的認證方式使用不 同的AuthenticationProvider;如使用用戶名密碼登錄時,使用AuthenticationProvider1,短信登錄時使用 AuthenticationProvider2等等這樣的例子很多
每個AuthenticationProvider需要實現supports()方法來表明自己支持的認證方式,如我們使用表單方式認證, 在提交請求時Spring Security會生成UsernamePasswordAuthenticationToken,它是一個Authentication,裏面封裝着用戶提交的用戶名、密碼信息;而對應的,哪個AuthenticationProvider來處理它?
我們在DaoAuthenticationProvider的基類AbstractUserDetailsAuthenticationProvider發現以下代碼:
//org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#supports
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
也就是說當web表單提交用戶名密碼時,Spring Security由DaoAuthenticationProvider處理
最後,我們來看一下Authentication(認證信息)的結構,它是一個接口,我們之前提到的UsernamePasswordAuthenticationToken就是它的實現之一
//org.springframework.security.core.Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
- Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位於 java.security包中的;它是表示着一個抽象主體身份,任何主體都有一個名稱,因此包含一個getName()方法
- getAuthorities(),權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系列字符串
- getCredentials(),憑證信息,用戶輸入的密碼字符串,在認證過後通常會被移除,用於保障安全
- getDetails(),細節信息,web應用中的實現接口通常爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
- getPrincipal(),身份信息,大部分情況下返回的是UserDetails接口的實現類,UserDetails代表用戶的詳細信息,那從Authentication中取出來的UserDetails就是當前登錄用戶信息,它也是框架中的常用接口之一
3 UserDetailsService
3.1 認識UserDetailsService
現在咱們現在知道DaoAuthenticationProvider處理了web表單的認證邏輯,認證成功後既得到一個 Authentication(UsernamePasswordAuthenticationToken實現),裏面包含了身份信息(Principal);這個身份 信息就是一個 Object ,大多數情況下它可以被強轉爲UserDetails對象
DaoAuthenticationProvider中包含了一個UserDetailsService實例,它負責根據用戶名提取用戶信息 UserDetails(包含密碼),而後DaoAuthenticationProvider會去對比UserDetailsService提取的用戶密碼與用戶提交 的密碼是否匹配作爲認證成功的關鍵依據,因此可以通過將自定義的 UserDetailsService 公開爲spring bean來定義自定義身份驗證
//org.springframework.security.core.userdetails.UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
很多人把DaoAuthenticationProvider和UserDetailsService的職責搞混淆,其實UserDetailsService只負責從特定 的地方(通常是數據庫)加載用戶信息,僅此而已;而DaoAuthenticationProvider的職責更大,它完成完整的認 證流程,同時會把UserDetails填充至Authentication
上面一直提到UserDetails是用戶信息,咱們看一下它的真面目:
//org.springframework.security.core.userdetails.UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很類似,比如它們都擁有username,authorities;Authentication的getCredentials()與 UserDetails中的getPassword()需要被區分對待,前者是用戶提交的密碼憑證,後者是用戶實際存儲的密碼,認證 其實就是對這兩者的比對;Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形 成的;還記得Authentication接口中的getDetails()方法嗎?其中的UserDetails用戶詳細信息便是經過了 AuthenticationProvider認證之後被填充的
通過實現UserDetailsService和UserDetails,我們可以完成對用戶信息獲取方式以及用戶信息字段的擴展
Spring Security提供的InMemoryUserDetailsManager(內存認證),JdbcUserDetailsManager(jdbc認證)就是 UserDetailsService的實現類,主要區別無非就是從內存還是從數據庫加載用戶
4 PasswordEncoder
4.1 認識PasswordEncoder
DaoAuthenticationProvider認證處理器通過UserDetailsService獲取到UserDetails後,它是如何與請求 Authentication中的密碼做對比呢?
在這裏Spring Security爲了適應多種多樣的加密類型,又做了抽象,DaoAuthenticationProvider通過 PasswordEncoder接口的matches方法進行密碼的對比,而具體的密碼對比細節取決於實現:
//org.springframework.security.crypto.password.PasswordEncoder
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
而Spring Security提供很多內置的PasswordEncoder,能夠開箱即用,使用某種PasswordEncoder只需要進行如 下聲明即可,如下:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
NoOpPasswordEncoder採用字符串匹配方法,不對密碼進行加密比較處理,密碼比較流程如下:
- 用戶輸入密碼(明文 )
- DaoAuthenticationProvider獲取UserDetails(其中存儲了用戶的正確密碼)
- DaoAuthenticationProvider使用PasswordEncoder對輸入的密碼和正確的密碼進行校驗,密碼一致則校驗通 過,否則校驗失敗
NoOpPasswordEncoder的校驗規則拿 輸入的密碼和UserDetails中的正確密碼進行字符串比較,字符串內容一致 則校驗通過,否則 校驗失敗
實際項目中推薦使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感興趣 的大家可以看看這些PasswordEncoder的具體實現。
4.2 使用BCryptPasswordEncoder
配置BCryptPasswordEncoder(在安全配置類中定義)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
密碼格式不對會報錯:Encoded password does not look like BCrypt
//org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
//
}
String presentedPassword = authentication.getCredentials().toString();
//passwordEncoder
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
@Test
public void testBCrypt(){
//對密碼進行加密
String hashpw = BCrypt.hashpw("123456", BCrypt.gensalt());
System.out.println(hashpw);
//校驗密碼
boolean checkpw = BCrypt.checkpw("123456", "$2a$10$lVFG0yY/LsonTXN9dV/czOdIbiwbu9g7Mjno0mqDFRXy5b2k0TGUu");
System.out.println(checkpw);
}