溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Spring Security架構以及源碼的示例分析

發(fā)布時間:2021-09-03 15:04:46 來源:億速云 閱讀:100 作者:小新 欄目:編程語言

這篇文章給大家分享的是有關Spring Security架構以及源碼的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

前言

現(xiàn)在流行的通用授權框架有apache的shiro和Spring家族的Spring Security,在涉及今天的微服務鑒權時,需要利用我們的授權框架搭建自己的鑒權服務,今天總理了Spring Security。

Spring Security 主要實現(xiàn)了Authentication(認證,解決who are you? ) 和 Access Control(訪問控制,也就是what are you allowed to do?,也稱為Authorization)。Spring Security在架構上將認證與授權分離,并提供了擴展點。

核心對象

主要代碼在spring-security-core包下面。要了解Spring Security,需要先關注里面的核心對象。

SecurityContextHolder, SecurityContext 和 Authentication

SecurityContextHolder 是 SecurityContext的存放容器,默認使用ThreadLocal 存儲,意味SecurityContext在相同線程中的方法都可用。

SecurityContext主要是存儲應用的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();

 /**
 * 用來標識是否已認證,如果使用用戶名和密碼登錄,通常是用戶名 
 */
 Object getPrincipal();
 /**
 * 是否已認證
 */
 boolean isAuthenticated();
 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

在實際應用中,通常使用UsernamePasswordAuthenticationToken:

public abstract class AbstractAuthenticationToken implements Authentication,
 CredentialsContainer {
 }
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
}

一個常見的認證過程通常是這樣的,創(chuàng)建一個UsernamePasswordAuthenticationToken,然后交給authenticationManager認證(后面詳細說明),認證通過則通過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里的一個關鍵接口,他用來表示一個principal。

public interface UserDetails extends Serializable {
 /**
 * 用戶的授權信息,可以理解為角色
 */
 Collection<? extends GrantedAuthority> getAuthorities();
 /**
 * 用戶密碼
 *
 * @return the password
 */
 String getPassword();
 /**
 * 用戶名 
 * */
 String getUsername();
 boolean isAccountNonExpired();
 boolean isAccountNonLocked();
 boolean isCredentialsNonExpired();
 boolean isEnabled();
}

UserDetails提供了認證所需的必要信息,在實際使用里,可以自己實現(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。

小結

  • SecurityContextHolder, 用來訪問 SecurityContext.

  • SecurityContext, 用來存儲Authentication .

  • Authentication, 代表憑證.

  • GrantedAuthority, 代表權限.

  • UserDetails, 用戶信息.

  • UserDetailsService,獲取用戶信息.

Authentication認證

AuthenticationManager

實現(xiàn)認證主要是通過AuthenticationManager接口,它只包含了一個方法:

public interface AuthenticationManager {
 Authentication authenticate(Authentication authentication)
 throws AuthenticationException;
}

authenticate()方法主要做三件事:

  • 如果驗證通過,返回Authentication(通常帶上authenticated=true)。

  • 認證失敗拋出AuthenticationException

  • 如果無法確定,則返回null

AuthenticationException是運行時異常,它通常由應用程序按通用方式處理,用戶代碼通常不用特意被捕獲和處理這個異常。

AuthenticationManager的默認實現(xiàn)是ProviderManager,它委托一組AuthenticationProvider實例來實現(xiàn)認證。
AuthenticationProvider和AuthenticationManager類似,都包含authenticate,但它有一個額外的方法supports,以允許查詢調用方是否支持給定Authentication類型:

public interface AuthenticationProvider {
 Authentication authenticate(Authentication authentication)
 throws AuthenticationException;
 boolean supports(Class<?> authentication);
}

ProviderManager包含一組AuthenticationProvider,執(zhí)行authenticate時,遍歷Providers,然后調用supports,如果支持,則執(zhí)行遍歷當前provider的authenticate方法,如果一個provider認證成功,則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有一個可選parent,如果parent不為空,則調用parent.authenticate(authentication)

AuthenticationProvider

AuthenticationProvider有多種實現(xiàn),大家最關注的通常是DaoAuthenticationProvider,繼承于AbstractUserDetailsAuthenticationProvider,核心是通過UserDetails來實現(xiàn)認證,DaoAuthenticationProvider默認會自動加載,不用手動配。

先來看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 {
 // 預先檢查,DefaultPreAuthenticationChecks,檢查用戶是否被lock或者賬號是否可用
 preAuthenticationChecks.check(user);
 
 // 抽象方法,自定義檢驗
 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);
 }

上面的檢驗主要基于UserDetails實現(xiàn),其中獲取用戶和檢驗邏輯由具體的類去實現(xiàn),默認實現(xiàn)是DaoAuthenticationProvider,這個類的核心是讓開發(fā)者提供UserDetailsService來獲取UserDetails以及 PasswordEncoder來檢驗密碼是否有效:

private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;

看具體的實現(xiàn),retrieveUser,直接調用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;
 }

再來看驗證:

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"));
 }
 }

小結:要自定義認證,使用DaoAuthenticationProvider,只需要為其提供PasswordEncoder和UserDetailsService就可以了。

定制 Authentication Managers

Spring Security提供了一個Builder類AuthenticationManagerBuilder,借助它可以快速實現(xià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一個AuthenticationManager,可以創(chuàng)建基于內存的認證、LDAP認證、 JDBC認證,以及添加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());
 }
}

授權與訪問控制

一旦認證成功,我們可以繼續(xù)進行授權,授權是通過AccessDecisionManager來實現(xiàn)的。框架有三種實現(xiàn),默認是AffirmativeBased,通過AccessDecisionVoter決策,有點像ProviderManager委托給AuthenticationProviders來認證。

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的實現(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)) {
 // 默認拒絕訪問
 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哪來的?其實就是上面ApplicationSecurity的configure里的。

web security 如何實現(xiàn)

Web層中的Spring Security(用于UI和HTTP后端)基于Servlet Filters,下圖顯示了單個HTTP請求的處理程序的典型分層。

Spring Security架構以及源碼的示例分析

Spring Security通過FilterChainProxy作為單一的Filter注冊到web層,Proxy內部的Filter。

Spring Security架構以及源碼的示例分析

FilterChainProxy相當于一個filter的容器,通過VirtualFilterChain來依次調用各個內部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);
  }
 }
 }

感謝各位的閱讀!關于“Spring Security架構以及源碼的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI