Spring security architecture and source code analysis

Spring security mainly implements authentication (authentication, which are you?) and access control (access control, that is, what are you allowed to do?, also known as authorization). Spring security separates authentication from authorization in architecture and provides extension points.

Core object

The main code is under the spring security core package. To understand spring security, you need to focus on the core objects inside.

Securitycontextholder, securitycontext and authentication

Securitycontextholder is the storage container of securitycontext. ThreadLocal storage is used by default, which means that all methods of securitycontext in the same thread are available. Securitycontext is mainly used to store the principal information of the application, which is represented by authentication in spring security.

Get principal:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

In spring security, you can take a look at the authentication definition:

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

In practical applications, usernamepasswordauthenticationtoken is usually used:

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

A common authentication process is usually as follows: create a usernamepasswordauthenticationtoken and submit it to the AuthenticationManager for authentication (described in detail later). If the authentication is passed, the authentication information is stored through the securitycontextholder.

 UsernamePasswordAuthenticationToken authenticationToken =
            new UsernamePasswordAuthenticationToken(loginVM.getUsername(),loginVM.getpassword());

Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);

Userdetails and userdetailsservice

Userdetails is a key interface in spring security. It is used to represent a 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 provides the necessary information for authentication. In actual use, you can implement userdetails and add additional information, such as email, mobile and so on.

The principal in authentication is usually the user name. We can obtain userdetails through the principal through userdetailsservice:

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

GrantedAuthority

As stated in user details, the granted authority can be understood as a role, such as role_ ADMINISTRATOR or ROLE_ HR_ SUPERVISOR。

Summary

Authentication authentication

AuthenticationManager

Authentication is mainly realized through the AuthenticationManager interface, which contains only one method:

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

The authenticate () method mainly does three things:

Authenticationexception is a runtime exception, which is usually handled by the application in a common way. User code usually does not need to be caught and handled deliberately.

The default implementation of AuthenticationManager is providermanager, which delegates a group of authenticationprovider instances to implement authentication. Similar to AuthenticationManager, authenticationprovider contains authenticate, but it has an additional method support to allow the caller to query whether the given authentication type is supported:

public interface AuthenticationProvider {

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

ProviderManager contains a set of AuthenticationProvider. When executing authenticate, it traverses Providers and then calls supports. If supported, the authenticate method that traverses the current provider is executed. If a provider authentication is successful, then 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;
	}

As can be seen from the above code, providermanager has an optional parent. If the parent is not empty, call parent authenticate(authentication)

AuthenticationProvider

There are many implementations of authenticationprovider. The daoauthenticationprovider is usually the most concerned one, which inherits from abstractuserdetailsauthenticationprovider. The core is to realize authentication through userdetails. Daoauthenticationprovider will be loaded automatically by default without manual configuration.

Let's take a look at abstractuserdetailsauthenticationprovider and the core 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,user);
	}

The above verification is mainly based on the implementation of userdetails. The user acquisition and verification logic are implemented by specific classes. The default implementation is daoauthenticationprovider. The core of this class is to let developers provide userdetailsservice to obtain userdetails and passwordencoder to verify whether the password is valid:

private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;

Look at the specific implementation, retrieveuser, and directly call userdetailsservice to obtain the user:

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

Let's look at validation:

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(),salt)) {
			logger.debug("Authentication Failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
		}
	}

Summary: to customize authentication, you can use daoauthenticationprovider by providing passwordencoder and userdetailsservice.

Customizing authentication managers

Spring security provides a builder class, authenticationmanagerbuilder, with which you can quickly implement custom authentication.

See the official source code Description:

Authentication manager builder can be used to build an authentication manager, create memory based authentication, LDAP authentication, JDBC authentication, and add userdetailsservice and authentication provider.

Easy to use:

@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());

    }
}

Authorization and access control

Once the authentication is successful, we can continue to authorize. The authorization is realized through the access decision manager. There are three implementations of the framework. By default, it is confirmed based. It makes decisions through the accessdecisionvoter, which is a bit like the authentication providers entrusted by the providermanager.

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

Look at accessdecisionvoter:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int Vote(Authentication authentication,S object,Collection<ConfigAttribute> attributes);

Object is the resource to be accessed by the user, and configattribute is the condition to be met to access the object. Usually, payload is a string, such as role_ ADMIN 。 So let's look at the implementation of rolevoter. Its core is to extract the grantedauthority from the authentication, and then compare it with configattribute to see whether it meets the conditions.


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

Here's the question: where did configattribute come from? In fact, it is in the configuration of application security above.

How to implement web security

Spring security (for UI and HTTP backend) in the web layer is based on servlet filters. The following figure shows a typical hierarchy of handlers for a single HTTP request.

Spring security registers to the web layer as a single filter through filterchainproxy, which is the internal filter of the proxy.

Filterchainproxy is equivalent to a container of filters. Each internal filter is called successively through virtualfilterchain



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

	private void doFilterInternal(ServletRequest request,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,this);
			}
		}
	}
	

reference resources

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>