Spring Security是一個能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面程式設計)功能,為應用系統提供宣告式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作。
類圖
為了方便理解Spring Security認證流程,特意畫了如下的類圖,包含相關的核心認證類
概述
核心驗證器
AuthenticationManager
該物件提供了認證方法的入口,接收一個Authentiaton
物件作為引數;
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
複製程式碼
ProviderManager
它是 AuthenticationManager
的一個實現類,提供了基本的認證邏輯和方法;它包含了一個 List<AuthenticationProvider>
物件,通過 AuthenticationProvider 介面來擴充套件出不同的認證提供者(當Spring Security
預設提供的實現類不能滿足需求的時候可以擴充套件AuthenticationProvider
覆蓋supports(Class<?> authentication)
方法);
驗證邏輯
AuthenticationManager
接收 Authentication
物件作為引數,並通過 authenticate(Authentication)
方法對其進行驗證;AuthenticationProvider
實現類用來支撐對 Authentication
物件的驗證動作;UsernamePasswordAuthenticationToken
實現了 Authentication
主要是將使用者輸入的使用者名稱和密碼進行封裝,並供給 AuthenticationManager
進行驗證;驗證完成以後將返回一個認證成功的 Authentication
物件;
Authentication
Authentication
物件中的主要方法
public interface Authentication extends Principal, Serializable {
//#1.許可權結合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")返回字串許可權集合
Collection<? extends GrantedAuthority> getAuthorities();
//#2.使用者名稱密碼認證時可以理解為密碼
Object getCredentials();
//#3.認證時包含的一些資訊。
Object getDetails();
//#4.使用者名稱密碼認證時可理解時使用者名稱
Object getPrincipal();
#5.是否被認證,認證為true
boolean isAuthenticated();
#6.設定是否能被認證
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
複製程式碼
ProviderManager
ProviderManager
是AuthenticationManager
的實現類,提供了基本認證實現邏輯和流程;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//#1.獲取當前的Authentication的認證型別
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//#2.遍歷所有的providers使用supports方法判斷該provider是否支援當前的認證型別,不支援的話繼續遍歷
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
#3.支援的話呼叫provider的authenticat方法認證
result = provider.authenticate(authentication);
if (result != null) {
#4.認證通過的話重新生成Authentication對應的Token
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
#5.如果#1 沒有驗證通過,則使用父型別AuthenticationManager進行驗證
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
#6. 是否擦出敏感資訊
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
複製程式碼
- 遍歷所有的 Providers,然後依次執行該 Provider 的驗證方法
- 如果某一個 Provider 驗證成功,則跳出迴圈不再執行後續的驗證;
- 如果驗證成功,會將返回的 result 既 Authentication 物件進一步封裝為 Authentication Token; 比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;這些 Authentication Token 也都繼承自 Authentication 物件;
- 如果 #1 沒有任何一個 Provider 驗證成功,則試圖使用其 parent Authentication Manager 進行驗證;
- 是否需要擦除密碼等敏感資訊;
AuthenticationProvider
ProviderManager
通過 AuthenticationProvider
擴充套件出更多的驗證提供的方式;而 AuthenticationProvider
本身也就是一個介面,從類圖中我們可以看出它的實現類AbstractUserDetailsAuthenticationProvider
和AbstractUserDetailsAuthenticationProvider
的子類DaoAuthenticationProvider
。DaoAuthenticationProvider
是Spring Security
中一個核心的Provider
,對所有的資料庫提供了基本方法和入口。
DaoAuthenticationProvider
DaoAuthenticationProvider
主要做了以下事情
- 對使用者身份盡心加密操作;
#1.可直接返回BCryptPasswordEncoder,也可以自己實現該介面使用自己的加密演算法核心方法String encode(CharSequence rawPassword);和boolean matches(CharSequence rawPassword, String encodedPassword); 複製程式碼
private PasswordEncoder passwordEncoder;
2. 實現了 `AbstractUserDetailsAuthenticationProvider` 兩個抽象方法,
1. 獲取使用者資訊的擴充套件點
```java
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
複製程式碼
主要是通過注入`UserDetailsService`介面物件,並呼叫其介面方法 `loadUserByUsername(String username)` 獲取得到相關的使用者資訊。`UserDetailsService`介面非常重要。
2. 實現 additionalAuthenticationChecks 的驗證方法(主要驗證密碼);
複製程式碼
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
為DaoAuthenticationProvider
提供了基本的認證方法;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
#1.獲取使用者資訊由子類實現即DaoAuthenticationProvider
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
#2.前檢查由DefaultPreAuthenticationChecks類實現(主要判斷當前使用者是否鎖定,過期,凍結User介面)
preAuthenticationChecks.check(user);
#3.子類實現
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
#4.檢測使用者密碼是否過期對應#2 的User介面
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
複製程式碼
AbstractUserDetailsAuthenticationProvider
主要實現了AuthenticationProvider
的介面方法authenticate
並提供了相關的驗證邏輯;
- 獲取使用者返回
UserDetails
AbstractUserDetailsAuthenticationProvider
定義了一個抽象的方法複製程式碼
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
2. 三步驗證工作
1. preAuthenticationChecks
2. additionalAuthenticationChecks(抽象方法,子類實現)
3. postAuthenticationChecks
3. 將已通過驗證的使用者資訊封裝成 UsernamePasswordAuthenticationToken 物件並返回;該物件封裝了使用者的身份資訊,以及相應的許可權資訊,相關原始碼如下,
```java
protected Authentication createSuccessAuthentication(Object principal,
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
複製程式碼
UserDetailsService
UserDetailsService
是一個介面,提供了一個方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
複製程式碼
通過使用者名稱 username 呼叫方法 loadUserByUsername 返回了一個UserDetails介面物件(對應AbstractUserDetailsAuthenticationProvider
的三步驗證方法);
public interface UserDetails extends Serializable {
#1.許可權集合
Collection<? extends GrantedAuthority> getAuthorities();
#2.密碼
String getPassword();
#3.使用者民
String getUsername();
#4.使用者是否過期
boolean isAccountNonExpired();
#5.是否鎖定
boolean isAccountNonLocked();
#6.使用者密碼是否過期
boolean isCredentialsNonExpired();
#7.賬號是否可用(可理解為是否刪除)
boolean isEnabled();
}
複製程式碼
Spring 為UserDetailsService
預設提供了一個實現類 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl
JdbcUserDetailsManager
該實現類主要是提供基於JDBC
對 User 進行增、刪、查、改的方法
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager,
GroupManager {
// ~ Static fields/initializers
// =====================================================================================
// UserDetailsManager SQL
#1.定義了一些列對資料庫操作的語句
public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";
public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";
public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";
複製程式碼
InMemoryUserDetailsManager
該實現類主要是提供基於記憶體
對 User 進行增、刪、查、改的方法
`public class InMemoryUserDetailsManager implements UserDetailsManager {
protected final Log logger = LogFactory.getLog(getClass());
#1.用MAP 儲存
private final Map<String, MutableUserDetails> users = new HashMap<String, MutableUserDetails>();
private AuthenticationManager authenticationManager;
public InMemoryUserDetailsManager() {
}
public InMemoryUserDetailsManager(Collection<UserDetails> users) {
for (UserDetails user : users) {
createUser(user);
}
}`
複製程式碼
總結
UserDetailsService
介面作為橋樑,是DaoAuthenticationProvier
與特定使用者資訊來源進行解耦的地方,UserDetailsService
由UserDetails
和UserDetailsManage
r所構成;UserDetails
和UserDetailsManager
各司其責,一個是對基本使用者資訊進行封裝,一個是對基本使用者資訊進行管理;
特別注意
,UserDetailsService
、UserDetails
以及UserDetailsManager
都是可被使用者自定義的擴充套件點,我們可以繼承這些介面提供自己的讀取使用者來源和管理使用者的方法,比如我們可以自己實現一個 與特定 ORM 框架,比如 Mybatis 或者 Hibernate,相關的UserDetailsService
和UserDetailsManager
;