前言
Spring Security 是一個安全框架, 可以簡單地認為 Spring Security 是放在用戶和 Spring 應用之間的一個安全屏障, 每一個 web 請求都先要經過 Spring Security 進行 Authenticate 和 Authoration 驗證
核心組件
SecurityContextHolder
SecurityContextHolder它持有的是安全上下文
(security context)的信息。當前操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權等等,這些都被保存在SecurityContextHolder中。SecurityContextHolder默認使用ThreadLocal 策略來存儲認證信息。看到ThreadLocal
也就意味着,這是一種與線程綁定的策略。在web環境下,Spring Security在用戶登錄時自動綁定認證信息到當前線程,在用戶退出時,自動清除當前線程的認證信息
看源碼他有靜態方法
//獲取 上下文
public static SecurityContext getContext() {
return strategy.getContext();
}
//清除上下文
public static void clearContext() {
strategy.clearContext();
}
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
getAuthentication()
返回了認證信息,getPrincipal()
返回了身份信息
UserDetails
便是Spring對身份信息封裝的一個接口
SecurityContext
安全上下文,主要持有Authentication
對象,如果用戶未鑒權,那Authentication對象將會是空的。看源碼可知
package org.springframework.security.core.context;
import java.io.Serializable;
import org.springframework.security.core.Authentication;
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication var1);
}
Authentication
鑒權對象,該對象主要包含了用戶的詳細信息(UserDetails)
和用戶鑒權時所需要的信息,如用戶提交的用戶名密碼、Remember-me Token,或者digest hash值等,按不同鑒權方式使用不同的Authentication
實現
看源碼可知道
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
-
Authentication
是spring security包中的接口,直接繼承自Principal類,而Principal是位於java.security包中的。可以見得,Authentication在spring security中是最高級別的身份/認證的抽象。由這個頂級接口,我們可以得到用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。 -
getAuthorities()
,權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系列字符串。 -
getCredentials()
,密碼信息,用戶輸入的密碼字符串,在認證過后通常會被移除,用於保障安全。 -
getDetails()
,細節信息,web應用中的實現接口通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。 -
getPrincipal()
,敲黑板!!!最重要的身份信息,大部分情況下返回的是UserDetails接口的實現類,也是框架中的常用接口之一
注意
GrantedAuthority
該接口表示了當前用戶所擁有的權限(或者角色)信息。這些信息由授權負責對象AccessDecisionManager
來使用,並決定最終用戶是否可以訪問某資源
(URL或方法調用或域對象)。鑒權時並不會使用到該對象
UserDetails
這個接口規范了用戶詳細信息所擁有的字段,譬如用戶名、密碼、賬號是否過期、是否鎖定等。在Spring Security中,獲取當前登錄的用戶的信息,一般情況是需要在這個接口上面進行擴展
,用來對接自己系統的用戶
看源碼可知
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetailsService
這個接口只提供一個接口loadUserByUsername(String username)
,這個接口非常重要,
一般情況我們都是通過擴展
這個接口來顯示獲取我們的用戶信息,用戶登陸時傳遞的用戶名和密碼也是通過這里這查找出來的用戶名和密碼進行校驗,但是真正的校驗不在這里,而是由AuthenticationManager
以及AuthenticationProvider
負責的,需要強調的是,如果用戶不存在,不應返回NULL
,而要拋出異常UsernameNotFoundException
看源碼可知
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
Spring Security安全身份認證流程原理
-
用戶名和密碼被過濾器獲取到,封裝成
Authentication
,通常情況下是UsernamePasswordAuthenticationToken
這個實現類。 -
AuthenticationManager
身份管理器負責驗證這個Authentication
-
認證成功后,
AuthenticationManager
身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼通常會被移除)Authentication
實例。 -
SecurityContextHolder
安全上下文容器將第3步填充了信息的Authentication
,通過SecurityContextHolder.getContext().setAuthentication()
方法,設置到其中。
AuthenticationManager
初次接觸Spring Security的朋友相信會被AuthenticationManager,ProviderManager ,AuthenticationProvider …這么多相似的Spring認證類搞得暈頭轉向,但只要稍微梳理一下就可以理解清楚它們的聯系和設計者的用意。
AuthenticationManager
(接口)是認證相關的核心接口
,也是發起認證的出發點,因為在實際需求中,我們可能會允許用戶使用用戶名+密碼登錄,同時允許用戶使用郵箱+密碼,手機號碼+密碼登錄,甚至,可能允許用戶使用指紋登錄(還有這樣的操作?沒想到吧),所以說AuthenticationManager
一般不直接認證,
AuthenticationManager
接口的常用實現類ProviderManager
內部會維護一個List<AuthenticationProvider>
列表,存放多種認證方式,實際上這是委托者模式的應用(Delegate)。
也就是說,核心的認證入口始終只有一個:AuthenticationManager
,不同的認證方式:用戶名+密碼(UsernamePasswordAuthenticationToken
),郵箱+密碼,手機號碼+密碼登錄則對應了三個AuthenticationProvider
。這樣一來就好理解多了
UserDetails和UserDetailsService
UserDetails
上面不斷提到了UserDetails
這個接口,它代表了最詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,我們一般都需要對它進行必要的擴展。
它和Authentication
接口很類似,比如它們都擁有username,authorities,區分他們也是本文的重點內容之一。
Authentication的getCredentials()與UserDetails中的getPassword()
需要被區分對待,前者是用戶提交的密碼憑證,后者是用戶正確的密碼,認證器其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication接口中的getUserDetails()方法嗎?其中的UserDetails用戶詳細信息便是經過了AuthenticationProvider之后被填充的。
UserDetailsService
UserDetailsService和AuthenticationProvider兩者的職責常常被人們搞混,UserDetailsService只負責從特定的地方加載用戶信息,可以是數據庫、redis緩存、接口等
全局獲取用戶信息方式
- 通過注入 Principal 接口獲取用戶信息
在運行過程中,Spring 會將 Username、Password、Authentication、Token 注入到 Principal 接口中,我們可以直接在controller獲取使用
@GetMapping("/home")
@ApiOperation("用戶中心")
public Result getUserHome(Principal principal) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=(UsernamePasswordAuthenticationToken)principal;
return ResultResponse.success(usernamePasswordAuthenticationToken.getPrincipal());
}
- 使用 @AuthenticationPrincipal 注解參數的方式
@GetMapping("/home")
@ApiOperation("用戶中心")
public Result getUserHome(@AuthenticationPrincipal cn.soboys.kmall.security.entity.User user ) {
return ResultResponse.success(user);
}
- 全局上下文獲取
由於獲取當前用戶的用戶名是一種比較常見的需求,其實 Spring Security 在 Authentication 中的實現類中已經為我們做了相關實現,所以獲取當前用戶的用戶名有如下更簡單的方式
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "當前登錄用戶:" + SecurityContextHolder.getContext().getAuthentication().getName();
}
}
- 獲取當前登錄用戶的 UserDetails 實例,然后再轉換成自定義的用戶實體類 User,這樣便能獲取用戶的 ID 等信息
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = (User)principal;
return "當前登錄用戶信息:" + user.toString();
}
}
- 異步方法中獲取用戶信息
Spring Security在默認情況下無法在使用@Async注解的方法中獲取當前登錄用戶的。若想在@Async方法中獲取當前登錄用戶,則需要調用SecurityContextHolder.setStrategyName
方法並設置相關的策略
參考