Spring Security 是一個能夠爲基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反轉),DI(Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減少了爲企業系統安全控制編寫大量重複代碼的工作。
Spring Security 擁有以下特性:
- 對身份認證和授權的全面且可擴展的支持
- 防禦會話固定、點擊劫持,跨站請求僞造等攻擊
- 支持 Servlet API 集成
- 支持與 Spring Web MVC 集成
- 其他的特性
Spring Security與Spring和Spring Boot的關係如下:
目前Spring Security提供以下安全技術或支持與現有技術集成:
- In-Memory認證
- JDBC認證
- LDAP認證
- Active Directory認證
- Remember-Me認證
- OpenID
- 匿名認證
- JAAS(Java Authentication and Authorization) Provider
- CAS認證
- X.509認證
- Basic And Digest認證
- OAuth 2.0
- SAML2
接下來先介紹Spring Security的核心組件開始。
1. 核心組件 - SecurityContextHolder, SecurityContext and Authentication
最基本的對象是SecurityContextHolder
,它存儲當前應用程序安全上下文的詳細信息,其中包括當前使用應用程序的主體(通常是用戶)的詳細信息。如當前操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限等。默認情況下,SecurityContextHolder
使用 ThreadLocal
來存儲這些詳細信息,這意味着 Security Context 始終可用於同一執行線程中的方法,即使 Security Context 未作爲這些方法的參數顯式傳遞。考慮到在用戶請求被處理後,Spring Security會自動清除線程,因此使用ThreadLocal
是線程安全的。
SecurityContextHolder支持三種安全策略:
SecurityContextHolder.MODE_THREADLOCAL
: 每個線程有其自己的SecurityContextHolder
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
: 繼承自安全線程的線程與安全線程有相同的安全標識SecurityContextHolder.MODE_GLOBAL
:所有線程共享相同的SecurityContextHolder
可能通過配置spring.security.strategy
系統屬性來設置SecurityContextHolder
的安全策略,但是大多數應用程序不需要修改SecurityContextHolder
安全策略。
1.1 獲取當前用戶信息
SecurityContextHolder
存儲了當前與應用程序交互的用戶信息,並且用戶信息與當前執行線程已綁定。 在Spring Security中使用Authentication
類代表用戶信息,並且可以使用如下代碼塊在代碼的任意處獲得當前已驗證用戶的用戶名:
(1)Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
(1) getContext()
獲得的是SecurityContext
接口的實例,而該實例存儲在ThreadLocal
中(分析見附錄-代碼1),代表當前線程需要的最少的安全信息。SecurityContext
接口中定義了兩個方法,代碼如下:
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
並且通過跟蹤代碼可以確getPrincipal()
返回的是UserDetails
實例(分析見附錄-代碼1)。
1.2 Authentication
SecurityContext.getAuthentication()
返回的Authentication
也是一個接口,它的定義如下:
// org.springframework.security.core.Authentication.java
public interface Authentication extends Principal, Serializable {
// 權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系列字符串
Collection<? extends GrantedAuthority> getAuthorities();
// 憑證信息以證明主體的正確性,如用戶在前端輸入的密碼
Object getCredentials();
// 其他信息,如IP地址,證書序列號等
Object getDetails();
// 主體的標識,如用戶名,大部分情況下返回的是UserDetails接口的實例
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication
直接繼承自 Principal
類,而Principal
是位於 java.security 包中。通過Authentication
接口的實現類,我們可以得到用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息等。
1.2.1 UserDetailService
從Authentication
代碼可知,可以通過其中的getPrincipal()
方法獲得安全主體,雖然返回的是Object對象,但大多數情況下我們可以將其轉爲UserDetails
對象。UserDetails
是Spring Security的核心類,代表一個安全主體並且是高度可擴展的,代碼如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
這裏需要注意的是UserDetails
的getPassword()
和Authentication
的 getCredentials()
的不同:前者是用戶正確的密碼,後者是用戶提交的密碼憑證。
在SecurityContextHolder
中用UserDetails
存儲安全主體信息,但是在應用程序中我們需要的安全主體信息可能更多(如需要email, empolyeeNumber等),此時我們可以通過繼承UserDetails
接口實現自定義安全主體並存儲在SecurityContextHolder
中,從而在使用時可以將SecurityContextHolder
中獲得的UserDetails
實例轉換爲自定義的實例。因此我們可以將UserDetails
認爲是應用程序和Spring Security框架之間的適配器。
爲了向SecurityContextHolder
中提供自定義的UserDetails
,只需要實向Spring容器中註冊一個實現了UserDetailsService
接口的Bean即可,模板代碼如下:
@Service
publiic class AuthUserDetailsService implements UserDetailsService {
@Override
(2)public UserDetails loadUserByUsername(userName: String) {
//你的邏輯
}
}
(2)只需要在loadUserByUsername
接口中添加定製的業務邏輯即可
Spring Security也提供了一些UserDetailsService
的實現,如InMemoryDaoImpl
)和JdbcDaoImpl
。但是不管如何提供UserDetailsService
的實現,都可以通過SecurityContextHolder
獲得UserDetailsService
返回的數據。
1.2.2 GrantedAuthority
除了主體,另一個Authentication
提供的重要方法是getAuthorities()
。這個方法提供了GrantedAuthority
對象數組。GrantedAuthority
是賦予到主體的權限,這些權限通常使用角色表示,比如ROLE_ADMINISTRATOR
或ROLE_HR_SUPERVISOR
。這些角色會用於web驗證,方法驗證和領域對象驗證。GrantedAuthority
對象通常使用UserDetailsService
讀取,即在loadUserByUsername()
方法中返回的UserDetails
實例時設置。
1.3 總結
上面介紹的Spring Security中使用的核心組件及其功能如下:
SecurityContextHolder
:提供幾種保存SecurityContext
的方式SecurityContext
:保存Authentication
信息Authentication
:代表Spring Security中的主體GrantedAuthority
:主體的權限UserDetails
:代表主體信息UserDetailsService
:加載UserDetails
2. 核心服務 - AuthenticationManager, ProviderManager 和 AuthenticationProvider
AuthenticationManager
接口是認證相關的核心接口,也是認證發起的出發點,在實際需求中,應用可能即允許用戶使用用戶名 + 密碼登錄,又允許用戶使用郵箱 + 密碼,手機號碼 + 密碼等形式登錄,所以要求認證系統要支持多種認證方式,因此需要一個接口定義認證的基本功能。AuthenticationManager
的定義如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Spring Security 中 AuthenticationManager
接口的默認實現是 ProviderManager
, 其對Authentication authenticate(Authentication authentication)
方法實現如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
(3)for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
(4)copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
(5)result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
(6)((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
(7)throw lastException;
}
(3) ProviderManager
本身並不直接處理身份認證請求,而是將認證委託給AuthenticationProvider
,依次查詢每個列表項是否可以執行身份認證。每個 Provider 要麼拋出異常要麼返回一個完全填充的 Authentication
對象
(4) 認證成功後,會將原始的認證信息拷貝到Provider的返回結果中
(5) 若當前ProviderManager無法完成認證操作,且其包含父級認證器,則轉交給父級認證器嘗試進行認證
(6) 完成認證,從authentication對象中刪除私密數據,防止一些機密數據(如用戶密碼)過長時間保留在內存中
(7) 如果認證失敗,則拋出AuthenticationException
Spring Security提供了很多認證Provider,如:
DaoAuthenticationProvider
AnonymousAuthenticationProvider
RememberMeAuthenticationProvider
所有的Provider都繼承自AuthenticationProvider
接口,代碼如下:
public interface AuthenticationProvider {
// 驗證請求
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// 判斷是否支持對authentication的認證
boolean supports(Class<?> authentication);
}
比如在DaoAuthenticationProvider
中使用UserDetailsService
根據用戶名獲得UserDetails
,再通過比對用戶密碼判斷用戶是否合法。
但是需要注意的是,在使用相應的認證機制時,必須爲其提供相應的認證Provider,否則會導致認證失敗。如JA-SIG CAS認證,其必須使用CasAuthenticationProvider
。
3. 總結
- 應用中可以通過
SecurityContextHolder
獲得認證信息Authentication
- 應用可以從獲得的
Authentication
實例中獲得認證後的用戶信息,如用戶名和權限 - 應用通過繼承
UserDetails
接口定製合適的UserDetails(如新增getEmail()函數) - 應用通過繼承
UserDetailsService
接口將系統中存儲的用戶信息轉換爲Spring Security的UserDetails實例 - Spring Security中
ProviderManager
爲認證的入口 ProviderManager
通過AuthenticationProvider
完成認證
參考
附錄
代碼1
SecurityContextHolder.getContext()
代碼如下:
public static SecurityContext getContext() {
(1)return strategy.getContext();
}
(1) 默認情況下strategy的指向的是ThreadLocalSecurityContextHolderStrategy實例,代碼如下:
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
(2) strategy = new ThreadLocalSecurityContextHolderStrategy();
}
......
}
(2) 可以確定最終調用的是ThreadLocalSecurityContextHolderStrategy實例中的getContexxt()
方法,代碼如下:
public SecurityContext getContext() {
(3)SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
(3) contextHolder
的定義如下:
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
因此默認獲得的SecurityContext實例是存放在ThreadLocal中的。