“致"高級"工程師(BUG工程師)
一顆折騰的心💗
原創不易,點個贊💗,支持支持
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
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認證和授權還有很多負責的過程需要深入了解,所以下次會對認證模塊和授權模塊進行更具體工作流程分析以及案例呈現。
各位看官還可以嗎?喜歡的話,動動手指點個贊💗,點個關注唄!!謝謝支持!