Authorization process is mainly completed by FilterSecurityInterceptor blocker. So let’s develop and analyze its source code, I suggest you take a look at the previous article

, understand the concept of authorization, this is my SpringSecurity configuration class

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    /** * Create user information in memory **@return* /
    @Bean
    public UserDetailsService userDetailService(a) {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(
                User.withUsername("zhangsan")
                        .password("123")
                        .authorities("r1").build()
        );
        userDetailsManager.createUser(
                User.withUsername("lisi")
                        .password("123")
                        .roles("r2")
                        .build()
        );
        userDetailsManager.createUser(
                User.withUsername("wangwu")
                        .password("123")
                        .authorities("r3")
                        .build()
        );
        return userDetailsManager;
    }

    /** * Password encoder */
    @Bean
    public PasswordEncoder passwordEncoder(a) {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService())
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf();
        httpSecurity.authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("r1")
                .antMatchers("/r/**")
                .authenticated()
                .anyRequest()
                .permitAll()
                .and()
                .formLogin().successForwardUrl("/r/login-success")
                .and()
                .logout()
                .logoutUrl("/logout"); httpSecurity.exceptionHandling(); }}Copy the code

So let’s take a look at FilterSecurityInterceptor

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter {
	// ~ Static fields/initializers
	/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

	private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

	// ~ Instance fields
	/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

	/**
	 * Method that is actually called by the filter chain. Simply delegates to the
	 * {@link #invoke(FilterInvocation)} method.
	 *
	 * @param request the servlet request
	 * @param response the servlet response
	 * @param chain the filter chain
	 *
	 * @throws IOException if the filter chain fails
	 * @throws ServletException if the filter chain fails
	 */
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // The FilterInvocation saves the object associated with the Http Filter
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource(a) {
		return this.securityMetadataSource;
	}

	public SecurityMetadataSource obtainSecurityMetadataSource(a) {
		return this.securityMetadataSource;
	}

	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
		this.securityMetadataSource = newSource;
	}

	publicClass<? > getSecureObjectClass() {return FilterInvocation.class;
	}

	public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if((fi.getRequest() ! =null) && (fi.getRequest().getAttribute(FILTER_APPLIED) ! =null)
				&& observeOncePerRequest) {
            // Indicates that the request has already passed through the filter and is a one-time request, so it is allowed directly
			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);
			}
			// Call the superclass method, the thing to do before calling the actual business logic
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
                // Interceptor is interceptor from the handler Mapping chain. ->controller
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}
			// Call the superclass method to do after calling the real business logic
			super.afterInvocation(token, null); }}}Copy the code

As # # beforeInvocation and afterInvocation is the logic in the parent class, so let’s watch the AbstractSecurityInterceptor again

public abstract class AbstractSecurityInterceptor implements InitializingBean.ApplicationEventPublisherAware.MessageSourceAware {
            
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private ApplicationEventPublisher eventPublisher;
	private AccessDecisionManager accessDecisionManager;
	private AfterInvocationManager afterInvocationManager;
	private AuthenticationManager authenticationManager = new NoOpAuthenticationManager();
	private RunAsManager runAsManager = new NullRunAsManager();

	private boolean alwaysReauthenticate = false;
	private boolean rejectPublicInvocations = false;
	private boolean validateConfigAttributes = true;
	private boolean publishAuthorizationSuccess = false;

	protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		final boolean debug = logger.isDebugEnabled();
		
        // Determine if the incoming object is the FilterInvocation object
		if(! getSecureObjectClass().isAssignableFrom(object.getClass())) {throw new IllegalArgumentException(
					"Security invocation attempted for object "
							+ object.getClass().getName()
							+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
							+ getSecureObjectClass());
		}
		
        // Get authorization information attributes
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'");
			}

			if (debug) {
				logger.debug("Public object - authentication not attempted");
			}

			publishEvent(new PublicInvocationEvent(object));

			return null; // no further work post-invocation
		}

		if (debug) {
			logger.debug("Secure object: " + object + "; Attributes: " + attributes);
		}

		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound"."An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}
		// Get the encapsulated authentication information
		Authentication authenticated = authenticateIfRequired();

		// Attempt authorization
		try {
            // The license manager decides whether to approve or not
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}

		if (debug) {
			logger.debug("Authorization successful");
		}

		if (publishAuthorizationSuccess) {
			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
		}

		// Attempt to run as a different user
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
				attributes);

		if (runAs == null) {
			if (debug) {
				logger.debug("RunAsManager did not change Authentication object");
			}

			// no further work post-invocation
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
		}
		else {
			if (debug) {
				logger.debug("Switching to RunAs Authentication: " + runAs);
			}

			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object); }}}Copy the code

#beforeInvocation the approach is

  • First check whether the object passed in isFilterInvocationIf not, throw an exception (the argument type must be correct).
  • Gets a collection of authorization information attributes
  • Obtaining certification Information
  • willFilterInvocation.Authentication.ConfigAttributeSubmit to the authentication manager for authentication

You might be wondering what ConfigAttribute is, but let’s see

public interface ConfigAttribute extends Serializable {

	String getAttribute(a);
Copy the code

There is only one interface#getAttribute()Method, let’s look at its implementation class





The names of the following three classes seem familiar

  • PreInvocationExpressionAttributeIs the corresponding@PreAuthorizeConfiguration properties of
  • PostInvocationExpressionAttributeIs the corresponding@PostAuthorizeConfiguration properties of
  • SecurityConfigIs the corresponding@SecuredConfiguration properties of
  • WebExpressionConfigAttributeIs a configuration property declared in a configuration file
  • Jsr250SecurityConfigIs an implementation of jsr250 annotations, which are not commonly used in SpringSecurity


So after we get the authorization properties, let’s see how the authorization manager makes decisions, and Spring is usually interface oriented, so let’s seeAccessDecisionManagerWhich implementation classes



public interface AccessDecisionManager {
	
	void decide(Authentication authentication, Object object, Collection
       
         configAttributes)
        throws AccessDeniedException,
			InsufficientAuthenticationException;
	
	boolean supports(ConfigAttribute attribute);

	boolean supports(Class
        clazz);
}
Copy the code

Let’s start with AffirmativeBased, which is the default implementation class of AccessDecisionManager

public class AffirmativeBased extends AbstractAccessDecisionManager {

	public AffirmativeBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	public void decide(Authentication authentication, Object object, Collection
       
         configAttributes)
        throws AccessDeniedException {
		int deny = 0;
		// The decision voting machine starts voting
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
     		// One in favor of it is considered approved
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;
			// There is a rejection to record first
			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break; }}// If no vote is approved and one vote is rejected, an exception is thrown
		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied"."Access is denied"));
		}

		// Check whether it is allowed to abstain, default yescheckAllowIfAllAbstainDecisions(); }}Copy the code
  • If there is a vote to approve it, it will be approved
  • None of the voters voted yes, and one rejected threw an exception
  • If they all abstain, they pass

Now that we’ve seen the logic of passing through a series of voting machines, let’s look at the logic of voting rights, and later the logic of the other two implementation classes

public interface AccessDecisionVoter<S> {
	// ~ Static fields/initializers
	/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class
        clazz);

	int vote(Authentication authentication, S object, Collection
       
         attributes)
       ;
}
Copy the code

It has many implementation classes, let’s pick a WebExpressionVoter

public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
	private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler();

	public int vote(Authentication authentication, FilterInvocation fi, Collection
       
         attributes)
        {
		assertauthentication ! =null;
		assertfi ! =null;
		assertattributes ! =null;
		
        / / get WebExpressionConfigAttribute properties
		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}
		
		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);
		For example, /r/r1 must have R1 permission to be compared with the request path /r/ R1
		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

	private WebExpressionConfigAttribute findConfigAttribute( Collection
       
         attributes)
        {
		for (ConfigAttribute attribute : attributes) {
            // This is also a minor detail. Only the first matching configuration is returned, so if multiple similar configurations are configured in the Security configuration file, the first one is taken
            // httpSecurity.authorizeRequests()
            // .antMatchers("/r/r1").hasAuthority("r1")
            // .antMatchers("/r/**")
            // .authenticated()
            // Put the smaller permissions in front and the larger permissions in back
			if (attribute instanceof WebExpressionConfigAttribute) {
				return(WebExpressionConfigAttribute) attribute; }}return null;
	}

	public boolean supports(ConfigAttribute attribute) {
		return attribute instanceof WebExpressionConfigAttribute;
	}

	public boolean supports(Class
        clazz) {
		return FilterInvocation.class.isAssignableFrom(clazz);
	}

	public void setExpressionHandler( SecurityExpressionHandler
       
         expressionHandler)
        {
		this.expressionHandler = expressionHandler; }}Copy the code
public class ConsensusBased extends AbstractAccessDecisionManager {
	// // The number of approvals and rejections can be equal
	private boolean allowIfEqualGrantedDeniedDecisions = true;

	public ConsensusBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	public void decide(Authentication authentication, Object object, Collection
       
         configAttributes)
        throws AccessDeniedException {
		int grant = 0;
		int deny = 0;

		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:
				grant++;

				break;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break; }}// The number of endorsements is greater than the number of rejections
		if (grant > deny) {
			return;
		}
		// The number of approvals is less than the number of rejections
		if (deny > grant) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied"."Access is denied"));
		}
		// The number of approvals is the same as the number of rejections, and neither of them is 0
		if((grant == deny) && (grant ! =0)) {
            // The number of approvals and rejections can be equal, pass. The default allows
			if (this.allowIfEqualGrantedDeniedDecisions) {
				return;
			}
            // If it is not allowed, it is rejected
			else {
				throw new AccessDeniedException(messages.getMessage(
						"AbstractAccessDecisionManager.accessDenied"."Access is denied")); }}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions(); }}Copy the code
  • The number of endorsements is greater than the number of denials, pass
  • The number of approvals is less than the number of rejections, rejections
  • The number of approvals is equal to the number of rejections, and neither of them is zero, allowing the number of approvals and rejections to be equal. Pass. The default allows
  • If they all abstain, they pass
public class UnanimousBased extends AbstractAccessDecisionManager {

	public UnanimousBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	public void decide(Authentication authentication, Object object, Collection
       
         attributes)
        throws AccessDeniedException {

		int grant = 0;

		List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
		singleAttributeList.add(null);
		// Attribute by attribute validation
		for (ConfigAttribute attribute : attributes) {
			singleAttributeList.set(0, attribute);

			for (AccessDecisionVoter voter : getDecisionVoters()) {
				int result = voter.vote(authentication, object, singleAttributeList);

				if (logger.isDebugEnabled()) {
					logger.debug("Voter: " + voter + ", returned: " + result);
				}

				switch (result) {
				case AccessDecisionVoter.ACCESS_GRANTED:
					grant++;

					break;

				case AccessDecisionVoter.ACCESS_DENIED:
					throw new AccessDeniedException(messages.getMessage(
							"AbstractAccessDecisionManager.accessDenied"."Access is denied"));

				default:
					break; }}}// To get this far, there were no deny votes
		if (grant > 0) {
			return;
		}

		// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions(); }}Copy the code
  • If there is a negative vote, it is rejected
  • If there are no dissenting votes and there are affirmative votes, the bill shall be adopted
  • If they abstain, they pass by default


UnanimousThere is another difference from the above implementation class. When it does authorization, it will send the authorization information to the voting rights one by one, and let the voting machine confirm the authorization information, and only when there is no objection will it be approved. The above two implementation classes directly pass the collection of authorization information to the voter, so that if the configuration is similar, only take the first one, the first one will vote for the approval



Finally, summarize through the flow chart





  • First of all,FilterSecurityInterceptorfromSecurityContextHolderTo deriveAuthentication
  • FilterSecurityInterceptorcreateFilterInvocation(including,HttpServletRequest.HttpServletResponse.FilterChain)
  • willFilterInvocationPassed to theSecurityMetaDataSourceTo obtainConfigAttributes
  • willFilterInvocation.Authentication.ConfigAttributesPassed to theAccessDecisionManager
    • If authorization fails, it is thrownAccessDeniedExceptionAnd thenExceptionTranslationFilterHandle this exception
    • If the authorization succeedsFilterSecurityInteceptorContinue to performFilterChainThe logic behind

Finally, to add the annotation to the Controller @ Secured, @ PreAuthorize method, Spring is by creating a MethodSecurityInterceptor interceptor realization of rights management, it with FilterSecurityInte Rceptor is a sibling class.

Permissions are checked when a request goes into a DispatchServlet and the interceptor chain is retrieved based on the request path