Spring Security架構(gòu)以及源碼詳析
前言
現(xiàn)在流行的通用授權(quán)框架有apache的shiro和Spring家族的Spring Security,在涉及今天的微服務(wù)鑒權(quán)時(shí),需要利用我們的授權(quán)框架搭建自己的鑒權(quán)服務(wù),今天總理了Spring Security。
Spring Security 主要實(shí)現(xiàn)了Authentication(認(rèn)證,解決who are you? ) 和 Access Control(訪問控制,也就是what are you allowed to do?,也稱為Authorization)。Spring Security在架構(gòu)上將認(rèn)證與授權(quán)分離,并提供了擴(kuò)展點(diǎn)。
核心對象
主要代碼在spring-security-core包下面。要了解Spring Security,需要先關(guān)注里面的核心對象。
SecurityContextHolder, SecurityContext 和 Authentication
SecurityContextHolder 是 SecurityContext的存放容器,默認(rèn)使用ThreadLocal 存儲(chǔ),意味SecurityContext在相同線程中的方法都可用。
SecurityContext主要是存儲(chǔ)應(yīng)用的principal信息,在Spring Security中用Authentication 來表示。
獲取principal:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
在Spring Security中,可以看一下Authentication定義:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); /** * 通常是密碼 */ Object getCredentials(); /** * Stores additional details about the authentication request. These might be an IP * address, certificate serial number etc. */ Object getDetails(); /** * 用來標(biāo)識(shí)是否已認(rèn)證,如果使用用戶名和密碼登錄,通常是用戶名 */ Object getPrincipal(); /** * 是否已認(rèn)證 */ boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
在實(shí)際應(yīng)用中,通常使用UsernamePasswordAuthenticationToken:
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { } public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { }
一個(gè)常見的認(rèn)證過程通常是這樣的,創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken,然后交給authenticationManager認(rèn)證(后面詳細(xì)說明),認(rèn)證通過則通過SecurityContextHolder存放Authentication信息。
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); Authentication authentication = this.authenticationManager.authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails與UserDetailsService
UserDetails 是Spring Security里的一個(gè)關(guān)鍵接口,他用來表示一個(gè)principal。
public interface UserDetails extends Serializable { /** * 用戶的授權(quán)信息,可以理解為角色 */ Collection<? extends GrantedAuthority> getAuthorities(); /** * 用戶密碼 * * @return the password */ String getPassword(); /** * 用戶名 * */ String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
UserDetails提供了認(rèn)證所需的必要信息,在實(shí)際使用里,可以自己實(shí)現(xiàn)UserDetails,并增加額外的信息,比如email、mobile等信息。
在Authentication中的principal通常是用戶名,我們可以通過UserDetailsService來通過principal獲取UserDetails:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
GrantedAuthority
在UserDetails里說了,GrantedAuthority可以理解為角色,例如 ROLE_ADMINISTRATOR or ROLE_HR_SUPERVISOR。
小結(jié)
- SecurityContextHolder, 用來訪問 SecurityContext.
- SecurityContext, 用來存儲(chǔ)Authentication .
- Authentication, 代表憑證.
- GrantedAuthority, 代表權(quán)限.
- UserDetails, 用戶信息.
- UserDetailsService,獲取用戶信息.
Authentication認(rèn)證
AuthenticationManager
實(shí)現(xiàn)認(rèn)證主要是通過AuthenticationManager接口,它只包含了一個(gè)方法:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
authenticate()方法主要做三件事:
- 如果驗(yàn)證通過,返回Authentication(通常帶上authenticated=true)。
- 認(rèn)證失敗拋出AuthenticationException
- 如果無法確定,則返回null
AuthenticationException是運(yùn)行時(shí)異常,它通常由應(yīng)用程序按通用方式處理,用戶代碼通常不用特意被捕獲和處理這個(gè)異常。
AuthenticationManager的默認(rèn)實(shí)現(xiàn)是ProviderManager,它委托一組AuthenticationProvider實(shí)例來實(shí)現(xiàn)認(rèn)證。
AuthenticationProvider和AuthenticationManager類似,都包含authenticate,但它有一個(gè)額外的方法supports,以允許查詢調(diào)用方是否支持給定Authentication類型:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
ProviderManager包含一組AuthenticationProvider,執(zhí)行authenticate時(shí),遍歷Providers,然后調(diào)用supports,如果支持,則執(zhí)行遍歷當(dāng)前provider的authenticate方法,如果一個(gè)provider認(rèn)證成功,則break。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { 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 { 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; } } 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; }
從上面的代碼可以看出, ProviderManager有一個(gè)可選parent,如果parent不為空,則調(diào)用parent.authenticate(authentication)
AuthenticationProvider
AuthenticationProvider有多種實(shí)現(xiàn),大家最關(guān)注的通常是DaoAuthenticationProvider,繼承于AbstractUserDetailsAuthenticationProvider,核心是通過UserDetails來實(shí)現(xiàn)認(rèn)證,DaoAuthenticationProvider默認(rèn)會(huì)自動(dòng)加載,不用手動(dòng)配。
先來看AbstractUserDetailsAuthenticationProvider,看最核心的authenticate:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 必須是UsernamePasswordAuthenticationToken Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // 獲取用戶名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 從緩存獲取 UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // retrieveUser 抽象方法,獲取用戶 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 { // 預(yù)先檢查,DefaultPreAuthenticationChecks,檢查用戶是否被lock或者賬號(hào)是否可用 preAuthenticationChecks.check(user); // 抽象方法,自定義檢驗(yàn) 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; } } // 后置檢查 DefaultPostAuthenticationChecks,檢查isCredentialsNonExpired postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
上面的檢驗(yàn)主要基于UserDetails實(shí)現(xiàn),其中獲取用戶和檢驗(yàn)邏輯由具體的類去實(shí)現(xiàn),默認(rèn)實(shí)現(xiàn)是DaoAuthenticationProvider,這個(gè)類的核心是讓開發(fā)者提供UserDetailsService來獲取UserDetails以及 PasswordEncoder來檢驗(yàn)密碼是否有效:
private UserDetailsService userDetailsService; private PasswordEncoder passwordEncoder;
看具體的實(shí)現(xiàn),retrieveUser,直接調(diào)用userDetailsService獲取用戶:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); } throw notFound; } catch (Exception repositoryProblem) { throw new InternalAuthenticationServiceException( repositoryProblem.getMessage(), repositoryProblem); } if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }
再來看驗(yàn)證:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } // 獲取用戶密碼 String presentedPassword = authentication.getCredentials().toString(); // 比較passwordEncoder后的密碼是否和userdetails的密碼一致 if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
小結(jié):要自定義認(rèn)證,使用DaoAuthenticationProvider,只需要為其提供PasswordEncoder和UserDetailsService就可以了。
定制 Authentication Managers
Spring Security提供了一個(gè)Builder類AuthenticationManagerBuilder,借助它可以快速實(shí)現(xiàn)自定義認(rèn)證。
看官方源碼說明:
SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider's.
AuthenticationManagerBuilder可以用來Build一個(gè)AuthenticationManager,可以創(chuàng)建基于內(nèi)存的認(rèn)證、LDAP認(rèn)證、 JDBC認(rèn)證,以及添加UserDetailsService和AuthenticationProvider。
簡單使用:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class ApplicationSecurity extends WebSecurityConfigurerAdapter { public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userDetailsService = userDetailsService; this.tokenProvider = tokenProvider; this.corsFilter = corsFilter; this.problemSupport = problemSupport; } @PostConstruct public void init() { try { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } catch (Exception e) { throw new BeanInitializationException("Security configuration failed", e); } } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(problemSupport) .accessDeniedHandler(problemSupport) .and() .csrf() .disable() .headers() .frameOptions() .disable() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/register").permitAll() .antMatchers("/api/activate").permitAll() .antMatchers("/api/authenticate").permitAll() .antMatchers("/api/account/reset-password/init").permitAll() .antMatchers("/api/account/reset-password/finish").permitAll() .antMatchers("/api/profile-info").permitAll() .antMatchers("/api/**").authenticated() .antMatchers("/management/health").permitAll() .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/v2/api-docs/**").permitAll() .antMatchers("/swagger-resources/configuration/ui").permitAll() .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN) .and() .apply(securityConfigurerAdapter()); } }
授權(quán)與訪問控制
一旦認(rèn)證成功,我們可以繼續(xù)進(jìn)行授權(quán),授權(quán)是通過AccessDecisionManager來實(shí)現(xiàn)的??蚣苡腥N實(shí)現(xiàn),默認(rèn)是AffirmativeBased,通過AccessDecisionVoter決策,有點(diǎn)像ProviderManager委托給AuthenticationProviders來認(rèn)證。
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; // 遍歷DecisionVoter for (AccessDecisionVoter voter : getDecisionVoters()) { // 投票 int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } // 一票否決 if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }
來看AccessDecisionVoter:
boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
object是用戶要訪問的資源,ConfigAttribute則是訪問object要滿足的條件,通常payload是字符串,比如ROLE_ADMIN 。所以我們來看下RoleVoter的實(shí)現(xiàn),其核心就是從authentication提取出GrantedAuthority,然后和ConfigAttribute比較是否滿足條件。
public boolean supports(ConfigAttribute attribute) { if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getRolePrefix())) { return true; } else { return false; } } public boolean supports(Class<?> clazz) { return true; } public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if(authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN; // 獲取GrantedAuthority信息 Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { // 默認(rèn)拒絕訪問 result = ACCESS_DENIED; // Attempt to find a matching granted authority for (GrantedAuthority authority : authorities) { // 判斷是否有匹配的 authority if (attribute.getAttribute().equals(authority.getAuthority())) { // 可訪問 return ACCESS_GRANTED; } } } } return result; }
這里要疑問,ConfigAttribute哪來的?其實(shí)就是上面ApplicationSecurity的configure里的。
web security 如何實(shí)現(xiàn)
Web層中的Spring Security(用于UI和HTTP后端)基于Servlet Filters,下圖顯示了單個(gè)HTTP請求的處理程序的典型分層。
Spring Security通過FilterChainProxy作為單一的Filter注冊到web層,Proxy內(nèi)部的Filter。
FilterChainProxy相當(dāng)于一個(gè)filter的容器,通過VirtualFilterChain來依次調(diào)用各個(gè)內(nèi)部filter
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else { doFilterInternal(request, response, chain); } } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); } private static class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List<Filter> additionalFilters; private final FirewalledRequest firewalledRequest; private final int size; private int currentPosition = 0; private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) { this.originalChain = chain; this.additionalFilters = additionalFilters; this.size = additionalFilters.size(); this.firewalledRequest = firewalledRequest; } public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (currentPosition == size) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } // Deactivate path stripping as we exit the security filter chain this.firewalledRequest.reset(); originalChain.doFilter(request, response); } else { currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " at position " + currentPosition + " of " + size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); } nextFilter.doFilter(request, response, this); } } }
參考
https://spring.io/guides/topicals/spring-security-architecture/
https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#overall-architecture
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
SpringBoot實(shí)現(xiàn)接口等冪次校驗(yàn)的示例代碼
本文主要介紹了SpringBoot實(shí)現(xiàn)接口等冪次校驗(yàn)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01SpringBoot整合MybatisPlus實(shí)現(xiàn)增刪改查功能
MybatisPlus是國產(chǎn)的第三方插件,?它封裝了許多常用的CURDapi,免去了我們寫mapper.xml的重復(fù)勞動(dòng)。本文將整合MybatisPlus實(shí)現(xiàn)增刪改查功能,感興趣的可以了解一下2022-05-05Java web spring異步方法實(shí)現(xiàn)步驟解析
這篇文章主要介紹了Java web spring異步方法實(shí)現(xiàn)步驟解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Java中LinkedList的模擬實(shí)現(xiàn)
本文主要介紹了Java中LinkedList的模擬實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06導(dǎo)致MyEclipse內(nèi)存不足的原因分析及解決辦法
這篇文章主要介紹了導(dǎo)致MyEclipse內(nèi)存不足的原因分析及解決辦法的相關(guān)資料,需要的朋友可以參考下2016-01-01