Spring Security 架構與源碼分析


Spring Security 主要實現了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 {
}

一個常見的認證過程通常是這樣的,創建一個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提供了認證所需的必要信息,在實際使用里,可以自己實現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

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

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

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

  1. 如果驗證通過,返回Authentication(通常帶上authenticated=true)。
  2. 認證失敗拋出AuthenticationException
  3. 如果無法確定,則返回null

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

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

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	boolean supports(Class<?> authentication);
}

ProviderManager包含一組AuthenticationProvider,執行authenticate時,遍歷Providers,然后調用supports,如果支持,則執行遍歷當前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有多種實現,大家最關注的通常是DaoAuthenticationProvider,繼承於AbstractUserDetailsAuthenticationProvider,核心是通過UserDetails來實現認證,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實現,其中獲取用戶和檢驗邏輯由具體的類去實現,默認實現是DaoAuthenticationProvider,這個類的核心是讓開發者提供UserDetailsService來獲取UserDetails以及 PasswordEncoder來檢驗密碼是否有效:

private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;

看具體的實現,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,借助它可以快速實現自定義認證。

看官方源碼說明:

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,可以創建基於內存的認證、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());

    }
}

授權與訪問控制

一旦認證成功,我們可以繼續進行授權,授權是通過AccessDecisionManager來實現的。框架有三種實現,默認是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的實現,其核心就是從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 如何實現

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

過濾鏈委托給一個Servlet

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

Spring安全篩選器

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

參考


作者:Jadepeng
出處:jqpeng的技術記事本--http://www.cnblogs.com/xiaoqi
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM