想要深入spring security的authentication (身份驗證)和access-control(訪問許可權控制)工作流程,必須清楚spring security的主要技術點包括關鍵介面、類以及抽象類如何協同工作進行authentication 和access-control的實現。
1.spring security 認證和授權流程
常見認證和授權流程可以分成:
- A user is prompted to log in with a username and password (使用者用賬密碼登入)
- The system (successfully) verifies that the password is correct for the username(校驗密碼正確性)
- The context information for that user is obtained (their list of roles and so on).(獲取使用者資訊context,如許可權)
- A security context is established for the user(為使用者建立security context)
- The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.(訪問許可權控制,是否具有訪問許可權)
1.1 spring security 認證
上述前三點為spring security認證驗證環節:
- 通常通過AbstractAuthenticationProcessingFilter過濾器將賬號密碼組裝成Authentication實現類UsernamePasswordAuthenticationToken;
- 將token傳遞給AuthenticationManager驗證是否有效,而AuthenticationManager通常使用ProviderManager實現類來檢驗;
- AuthenticationManager認證成功後將返回一個擁有詳細資訊的Authentication object(包括許可權資訊,身份資訊,細節資訊,但密碼通常會被移除);
- 通過SecurityContextHolder.getContext().getAuthentication().getPrincipal()將Authentication設定到security context中。
1.2 spring security訪問授權
- 通過FilterSecurityInterceptor過濾器入口進入;
- FilterSecurityInterceptor通過其繼承的抽象類的AbstractSecurityInterceptor.beforeInvocation(Object object)方法進行訪問授權,其中涉及了類AuthenticationManager、AccessDecisionManager、SecurityMetadataSource等。
根據上述描述的過程,我們接下來主要去分析其中涉及的一下Component、Service、Filter。
2.核心元件(Core Component )
2.1 SecurityContextHolder
SecurityContextHolder提供對SecurityContext的訪問,儲存security context(使用者資訊、角色許可權等),而且其具有下列儲存策略即工作模式:
-
SecurityContextHolder.MODE_THREADLOCAL(預設):使用ThreadLocal,資訊可供此執行緒下的所有的方法使用,一種與執行緒繫結的策略,此天然很適合Servlet Web應用。
-
SecurityContextHolder.MODE_GLOBAL:使用於獨立應用
-
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:具有相同安全標示的執行緒
修改SecurityContextHolder的工作模式有兩種方法 :
- 設定一個系統屬性(system.properties) : spring.security.strategy;
- 呼叫SecurityContextHolder靜態方法setStrategyName()
在預設ThreadLocal策略中,SecurityContextHolder為靜態方法獲取使用者資訊為:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
複製程式碼
但是一般不需要自身去獲取。 其中getAuthentication()返回一個Authentication認證主體,接下來分析Authentication、UserDetails細節。
2.2 Authentication
Spring Security使用一個Authentication物件來描述當前使用者的相關資訊,其包含使用者擁有的許可權資訊列表、使用者細節資訊(身份資訊、認證資訊)。Authentication為認證主體在spring security中時最高階別身份/認證的抽象,常見的實現類UsernamePasswordAuthenticationToken。Authentication介面原始碼:
public interface Authentication extends Principal, Serializable {
//許可權資訊列表,預設GrantedAuthority介面的一些實現類
Collection<? extends GrantedAuthority> getAuthorities();
//密碼資訊
Object getCredentials();
//細節資訊,web應用中的實現介面通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值
Object getDetails();
//通常返回值為UserDetails實現類
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
複製程式碼
前面兩個元件都涉及了UserDetails,以及GrantedAuthority其到底是什麼呢?2.3小節分析。
2.3 UserDetails&GrantedAuthority
UserDetails提供從應用程式的DAO或其他安全資料來源構建Authentication物件所需的資訊,包含GrantedAuthority。其官方實現類為User,開發者可以實現其介面自定義UserDetails實現類。其介面原始碼:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
複製程式碼
UserDetails與Authentication介面功能類似,其實含義即是Authentication為使用者提交的認證憑證(賬號密碼),UserDetails為系統中使用者正確認證憑證,在UserDetailsService中的loadUserByUsername方法獲取正確的認證憑證。 其中在getAuthorities()方法中獲取到GrantedAuthority列表是代表使用者訪問應用程式許可權範圍,此類許可權通常是“role(角色)”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。GrantedAuthority介面常見的實現類SimpleGrantedAuthority。
3. 核心服務類(Core Services)
3.1 AuthenticationManager、ProviderManager以及AuthenticationProvider
AuthenticationManager是認證相關的核心介面,是認證一切的起點。但常見的認證流程都是AuthenticationManager實現類ProviderManager處理,而且ProviderManager實現類基於委託者模式維護AuthenticationProvider 列表用於不同的認證方式。例如:
- 使用賬號密碼認證方式DaoAuthenticationProvider實現類(繼承了AbstractUserDetailsAuthenticationProvide抽象類),其為預設認證方式,進行資料庫庫獲取認證資料資訊。
- 遊客身份登入認證方式AnonymousAuthenticationProvider實現類
- 從cookies獲取認證方式RememberMeAuthenticationProvider實現類
AuthenticationProvider為
ProviderManager原始碼分析:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
//AuthenticationProvider列表依次認證
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
//每個AuthenticationProvider進行認證
result = provider.authenticate(authentication)
if (result != null) {
copyDetails(authentication, result);
break;
}
}
....
catch (AuthenticationException e) {
lastException = e;
}
}
//進行父類AuthenticationProvider進行認證
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication資訊,則直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//清除密碼
((CredentialsContainer) result).eraseCredentials();
}
//釋出登入成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
//如果都沒認證成功,丟擲異常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
複製程式碼
ProviderManager 中的AuthenticationProvider列表,會依照次序去認證,預設策略下,只需要通過一個AuthenticationProvider的認證,即可被認為是登入成功,而且AuthenticationProvider認證成功後返回一個Authentication實體,併為了安全會進行清除密碼。如果所有認證器都無法認證成功,則ProviderManager 會丟擲一個ProviderNotFoundException異常。
3.2 UserDetailsService
UserDetailsService介面作用是從特定的地方獲取認證的資料來源(賬號、密碼)。如何獲取到系統中正確的認證憑證,通過loadUserByUsername(String username)獲取認證資訊,而且其只有一個方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
複製程式碼
其常見的實現類從資料獲取的JdbcDaoImpl實現類,從記憶體中獲取的InMemoryUserDetailsManager實現類,不過我們可以實現其介面自定義UserDetailsService實現類,如下:
public class CustomUserService implements UserDetailsService {
@Autowired
//使用者mapper
private UserInfoMapper userInfoMapper;
@Autowired
//使用者許可權mapper
private PermissionInfoMapper permissionInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfoDTO userInfo = userInfoMapper.getUserInfoByUserName(username);
if (userInfo != null) {
List<PermissionInfoDTO> permissionInfoDTOS = permissionInfoMapper.findByAdminUserId(userInfo.getId());
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
//組裝許可權GrantedAuthority object
for (PermissionInfoDTO permissionInfoDTO : permissionInfoDTOS) {
if (permissionInfoDTO != null && permissionInfoDTO.getPermissionName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(
permissionInfoDTO.getPermissionName());
grantedAuthorityList.add(grantedAuthority);
}
}
//返回使用者資訊
return new User(userInfo.getUserName(), userInfo.getPasswaord(), grantedAuthorityList);
}else {
//丟擲使用者不存在異常
throw new UsernameNotFoundException("admin" + username + "do not exist");
}
}
}
複製程式碼
3.3 AccessDecisionManager&SecurityMetadataSource
AccessDecisionManager是由AbstractSecurityInterceptor呼叫,負責做出最終的訪問控制決策。
AccessDecisionManager介面原始碼:
//訪問控制決策
void decide(Authentication authentication, Object secureObject,Collection<ConfigAttribute> attrs)
throws AccessDeniedException;
//是否支援處理傳遞的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//確認class是否為AccessDecisionManager
boolean supports(Class clazz);
複製程式碼
SecurityMetadataSource包含著AbstractSecurityInterceptor訪問授權所需的後設資料(動態url、動態授權所需的資料),在AbstractSecurityInterceptor授權模組中結合AccessDecisionManager進行訪問授權。其涉及了ConfigAttribute。 SecurityMetadataSource介面:
Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes();
boolean supports(Class<?> clazz);
複製程式碼
我們還可以自定義SecurityMetadataSource資料來源,實現介面FilterInvocationSecurityMetadataSource。例:
public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
public List<ConfigAttribute> getAttributes(Object object) {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
String httpMethod = fi.getRequest().getMethod();
List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
// Lookup your database (or other source) using this information and populate the
// list of attributes
return attributes;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
複製程式碼
3.4 PasswordEncoder
為了儲存安全,一般要對密碼進行演算法加密,而spring security提供了加密PasswordEncoder介面。其實現類有使用BCrypt hash演算法實現的BCryptPasswordEncoder,SCrypt hashing 演算法實現的SCryptPasswordEncoder實現類,實現類內部實現可看原始碼分析。而PasswordEncoder介面只有兩個方法:
public interface PasswordEncoder {
//密碼加密
String encode(CharSequence rawPassword);
//密碼配對
boolean matches(CharSequence rawPassword, String encodedPassword);
}
複製程式碼
4 核心 Security 過濾器(Core Security Filters)
4.1 FilterSecurityInterceptor
FilterSecurityInterceptor是Spring security授權模組入口,該類根據訪問的使用者的角色,許可權授權訪問那些資源(訪問特定路徑應該具備的許可權)。
FilterSecurityInterceptor封裝FilterInvocation物件進行操作,所有的請求到了這一個filter,如果這個filter之前沒有執行過的話,那麼首先執行其父類AbstractSecurityInterceptor提供的InterceptorStatusToken token = super.beforeInvocation(fi),在此方法中使用AuthenticationManager獲取Authentication中使用者詳情,使用ConfigAttribute封裝已定義好訪問許可權詳情,並使用AccessDecisionManager.decide()方法進行訪問許可權控制。
FilterSecurityInterceptor原始碼分析:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//回撥其繼承的抽象類AbstractSecurityInterceptor的方法
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
複製程式碼
AbstractSecurityInterceptor原始碼分析:
protected InterceptorStatusToken beforeInvocation(Object object) {
....
//獲取所有訪問許可權(url-role)屬性列表(已定義在資料庫或者其他地方)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
....
//獲取該使用者訪問資訊(包括url,訪問許可權)
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
//進行授權訪問
this.accessDecisionManager.decide(authenticated, object, attributes);
}catch
....
}
複製程式碼
4.2 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter使用username和password表單登入使用的過濾器,也是最為常用的過濾器。其原始碼:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//獲取表單中的使用者名稱和密碼
String username = obtainUsername(request);
String password = obtainPassword(request);
...
username = username.trim();
//組裝成username+password形式的token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//交給內部的AuthenticationManager去認證,並返回認證資訊
return this.getAuthenticationManager().authenticate(authRequest);
}
複製程式碼
其主要程式碼為建立UsernamePasswordAuthenticationToken的Authentication實體以及呼叫AuthenticationManager進行authenticate認證,根據認證結果執行successfulAuthentication或者unsuccessfulAuthentication,無論成功失敗,一般的實現都是轉發或者重定向等處理,不再細究AuthenticationSuccessHandler和AuthenticationFailureHandle。興趣的可以研究一下其父類AbstractAuthenticationProcessingFilter過濾器。
4.3 AnonymousAuthenticationFilter
AnonymousAuthenticationFilter是匿名登入過濾器,它位於常用的身份認證過濾器(如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之後,意味著只有在上述身份過濾器執行完畢後,SecurityContext依舊沒有使用者資訊,AnonymousAuthenticationFilter該過濾器才會有意義——基於使用者一個匿名身份。 AnonymousAuthenticationFilter原始碼分析:
public class AnonymousAuthenticationFilter extends GenericFilterBean implements
InitializingBean {
...
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//建立匿名登入Authentication的資訊
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
...
}
chain.doFilter(req, res);
}
//建立匿名登入Authentication的資訊方法
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
}
複製程式碼
4.4 SecurityContextPersistenceFilter
SecurityContextPersistenceFilter的兩個主要作用便是request來臨時,建立SecurityContext安全上下文資訊和request結束時清空SecurityContextHolder。原始碼後續分析。
小節總結:
. AbstractAuthenticationProcessingFilter:主要處理登入
. FilterSecurityInterceptor:主要處理鑑權
總結
經過上面對核心的Component、Service、Filter分析,初步瞭解了Spring Security工作原理以及認證和授權工作流程。Spring Security認證和授權還有很多負責的過程需要深入瞭解,所以下次會對認證模組和授權模組進行更具體工作流程分析以及案例呈現。最後以上純粹個人結合部落格和官方文件總結,如有錯請指出!