Spring Security Parsing (I) — Authorization process

In learning Spring Cloud, encountered authorization service oAUth related content, always half-understanding, so I decided to first Spring Security, Spring Security Oauth2 and other permissions, authentication related content, principle and design study and sort out. This series of articles is written in the process of learning to strengthen the impression and understanding, if there is infringement, please inform.

Project Environment:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

A simple Security Demo

1. Implementation of user-defined UserDetailsService

The MyUserDetailsUserService class is customized to implement the loadUserByUsername() method of the UserDetailsService interface. Here, it simply returns a User object provided by Spring Security. Behind in order to illustrate the Spring Security access control, used here AuthorityUtils.com maSeparatedStringToAuthorityList (” admin “) set up a user account with an admin role permission information. In actual projects, users and their roles and permissions can be obtained by accessing the database.

@Component public class MyUserDetailsUserService implements UserDetailsService { @Override public UserDetails LoadUserByUsername (String username) throws UsernameNotFoundException {/ / cannot be used directly to create BCryptPasswordEncoder object to encrypt, This way of encryption No {bcrypt} prefix, / / will cause lead to get less than encryption algorithm appear when in matches. / / Java lang. IllegalArgumentException: There is no PasswordEncoder mappedfor the id "null"Problem / / cause is the Spring Security5 use DelegatingPasswordEncoder replace NoOpPasswordEncoder (delegate), / / and use the default BCryptPasswordEncoder encryption (note DelegatingPasswordEncoder entrust encryption methods BCryptPasswordEncoder encryption before adding the encryption type prefix) https://blog.csdn.net/alinyua/article/details/80219500return new User("user",  PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); }}Copy the code

Note 5 start didn’t use NoOpPasswordEncoder Spring Security as its default password encoder, but the default DelegatingPasswordEncoder as its password encoder, Encode method is implemented through the name of the cipher encoder as prefix + entrust all kinds of cipher encoders.

public String encode(CharSequence rawPassword) {
        return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
    }
Copy the code

So idForEncode here is shorthand for the cipher encoder, Can pass PasswordEncoderFactories. CreateDelegatingPasswordEncoder () internal implementation to see which is the default is to use the prefix bcrypt is BCryptPasswordEncoder

public class PasswordEncoderFactories {
    public static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new LdapShaPasswordEncoder());
        encoders.put("MD4", new Md4PasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new StandardPasswordEncoder());
        returnnew DelegatingPasswordEncoder(encodingId, encoders); }}Copy the code

2. Set up Spring Security configuration

Define SpringSecurityConfig configuration class and inheritance WebSecurityConfigurerAdapter covering the configure (HttpSecurity HTTP) method.

@Configuration @EnableWebSecurity //1 public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Override  protected void configure(HttpSecurity http) throws Exception { http.formLogin() //2 .and() .authorizeRequests() //3 .antMatchers("/index"."/").permitAll() //4
                .anyRequest().authenticated(); //6
    }
}
Copy the code

Configuration resolution:

  • @ EnableWebSecurity view its annotations to the source code, mainly citing WebSecurityConfiguration. Class and joined the @ EnableGlobalAuthentication annotations, will not covered here, We just need to understand that adding the @enableWebSecurity annotation will enable Security.
  • FormLogin () uses a formLogin(the default request address is /login). In Spring Security 5, formLogin() has been replaced with the old httpBasic() default, which is configured once again to indicate formLogin.
  • AuthorizeRequests () begins requesting permission configuration
  • AntMatchers () uses Ant style path matching, where match/and /index are configured
  • PermitAll () can be accessed arbitrarily by the user
  • AnyRequest () matches all paths
  • Authenticated () Is accessible after a user logs in

3. Configure HTML and test interfaces

Create a new index.html file under the Resources /static directory that defines an internal button to access the test interface

<! DOCTYPE html> <html lang="en" >
<head>
    <meta charset="UTF-8"</title> </head> <body> Spring Security welcomes you! <p> <a href="/get_user/test"</a></p> </body> </ HTML >Copy the code

Create a REST-style interface to get user information

@RestController
public class TestController {

    @GetMapping("/get_user/{username}")
    public String getUser(@PathVariable  String username){
        returnusername; }}Copy the code

4. Start the project test

1. Access localhost:8080 without any blocking

2. Clicking the Test Verify permission control button redirects you to the default login page of Security

3. Use the default account user: 123456 defined by MyUserDetailsUserService to log in. The /get_user interface is displayed


2. @enableWebSecurity configuration parsing

Remember before about @ EnableWebSecurity cited WebSecurityConfiguration configuration classes and @ EnableGlobalAuthentication comments? WebSecurityConfiguration is associated with the authorization of configuration, @ EnableGlobalAuthentication configured authentication related our fine win again next day.

First we see WebSecurityConfiguration source code, can be found clearly springSecurityFilterChain () method.

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers ! = null && ! webSecurityConfigurers.isEmpty();if(! hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(newWebSecurityConfigurerAdapter() {}); webSecurity.apply(adapter); }returnwebSecurity.build(); / / 1}Copy the code

This method first will determine whether webSecurityConfigurers is empty, empty load a default WebSecurityConfigurerAdapter object, Because of the custom SpringSecurityConfig itself is inherited WebSecurityConfigurerAdapter object, So we custom Security configuration will be loaded in (if you want to know how to load in can see WebSecurityConfiguration setFilterChainProxySecurityConfigurer () method).

We look at the webSecurity. The build () method actually calls is AbstractConfiguredSecurityBuilder doBuild () method, the internal implementation method is as follows:

@Override
	protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; beforeInit(); init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); / / 1 create DefaultSecurityFilterChain (the Security Filter chain of responsibility) buildState = buildState. BUILT;returnresult; }}Copy the code

We put the focus on the performBuild () method, to see its implementation subclass HttpSecurity. PerformBuild () method, the internal sorting filters and created DefaultSecurityFilterChain object.

    @Override
	protected DefaultSecurityFilterChain performBuild() throws Exception {
		Collections.sort(filters, comparator);
		return new DefaultSecurityFilterChain(requestMatcher, filters);
	}
Copy the code

See DefaultSecurityFilterChain constructor, we can see there are logged.

public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
		logger.info("Creating filter chain: " + requestMatcher + ","+ filters); This. RequestMatcher = requestMatcher; this.filters = new ArrayList<>(filters); }Copy the code

We can go back to the project launch log. You can see that the following image clearly prints this log, with all Filter names printed out. == (Note the filter chain printed here, which will be used to expand all our authorization procedures) ==

There is a doubt: HttpSecurity performBuild filters in the () method is how to load? This time need to look at WebSecurityConfigurerAdapter. The init () method, This method internally calls the getHttp() method to return the HttpSecurity object.

public void init(final WebSecurity web) throws Exception {
		final HttpSecurity http = getHttp(); // 1 
		web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
			public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); }}); }Copy the code

With the parsing @ EnableWebSecurity for such a long time, is the key point is to create a DefaultSecurityFilterChain namely we often security filter chain of responsibility, Next we around the DefaultSecurityFilterChain filters for authorization process of parsing.

Iii. Authorization process analysis

The authorization process of Security can be understood as various filter processing to complete an authorization. So let’s take a look at the filter chain printed before. Here, for convenience, the picture is posted again

Here we focus only on the following important filters:

  • SecurityContextPersistenceFilter
  • UsernamePasswordAuthenticationFilter (AbstractAuthenticationProcessingFilter)
  • BasicAuthenticationFilter
  • AnonymousAuthenticationFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

1, SecurityContextPersistenceFilter

SecurityContextPersistenceFilter this filter is mainly responsible for the following things:

  • Through (SecurityContextRepository) repo. LoadContext () method from the request Session to obtain SecurityContext (Security context, Similar to ApplicaitonContext, if the request Session does not create a SecurityContext object with null authentication by default
  • SecurityContextHolder. SetContext () will SecurityContext object into the manage SecurityContextHolder (SecurityContextHolder using ThreadLocal by default Policies to store authentication information)
  • Because in the finally achieve will be in the final by SecurityContextHolder. ClearContext () will be cleared from SecurityContextHolder SecurityContext object
  • Because the implementation in finally puts the SecurityContext object into the Session at the end via repo.savecontext ()
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); // Get the SecurityContxt object from Session, If the Session does not create an authtication attributes as null object of SecurityContext SecurityContext contextBeforeChainExecution = repo.loadContext(holder); Try {// Put the SecurityContext object into the SecurityContextHolder for management (The SecurityContextHolder uses the ThreadLocal policy by default to store authentication information) SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); / / removing SecurityContextHolder. SecurityContext object from SecurityContextHolder clearContext (); / / SecurityContext objects to be included in the Session as repo. SaveContext (contextAfterChainExecution, holder getRequest (), holder.getResponse()); request.removeAttribute(FILTER_APPLIED);if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed"); }}Copy the code

We play on in SecurityContextPersistenceFilter breakpoints, start the project, access localhost: 8080, to debug the implementation:

You’ll notice that the authtication in SecurityContxt here is an authentication message called anonymousUser, This is because the request to call AnonymousAuthenticationFilter, Security created an anonymous users to access by default.

2, UsernamePasswordAuthenticationFilter (AbstractAuthenticationProcessingFilter)

A filter that authorizes a request by obtaining the password of the account used in the request.

  • RequiresAuthentication () determines whether /login is requested in POST mode
  • Call attemptAuthentication certification () method, the internal created authenticated attribute to false (unauthorized) UsernamePasswordAuthenticationToken objects, And passed to the AuthenticationManager().authenticate() method for authentication, After the success of the certification Returns an authenticated = true (that is, the authorized successful) UsernamePasswordAuthenticationToken object
  • Through sessionStrategy. OnAuthentication () Authentication to be included in the Session
  • Through successfulAuthentication () call AuthenticationSuccessHandler onAuthenticationSuccess interface for successful processing (by inheritance AuthenticationSuccessHandler to write successful processing logic) successfulAuthentication (request, response, chain, authResult);
  • Through unsuccessfulAuthentication () call AuthenticationFailureHandler onAuthenticationFailure interface Failure processing (can through inheritance AuthenticationFailureHandler write failure processing logic)

Let’s take a look at the official source code processing logic:

/ / 1 AbstractAuthenticationProcessingFilterdoFilter method public voiddoFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; / / 2 to determine whether request address/login and request for the POST (UsernamePasswordAuthenticationFilter structure method to determine)if(! requiresAuthentication(request, response)) { chain.doFilter(request, response);return; } Authentication authResult; Try {/ / / / 3 call subclasses UsernamePasswordAuthenticationFilter attemptAuthentication method attemptAuthentication created within a method Authenticated properties forfalseUsernamePasswordAuthenticationToken object (unauthorized) and transmitted to the AuthenticationManager (). The authenticate () method of authentication, // Returns authenticated = after successful authenticationtrue(authorized) to the success of UsernamePasswordAuthenticationToken object authResult = attemptAuthentication (request, response);if (authResult == null) {
				return; } / / 4 will be successful certification Authentication in Session sessionStrategy. OnAuthentication (authResult, request, response); } the catch (InternalAuthenticationServiceException failed) {/ / 5 calls AuthenticationFailureHandler after authentication failed OnAuthenticationFailure interface failure processing (can through inheritance AuthenticationFailureHandler write failure processing logic) unsuccessfulAuthentication(request, response, failed);return; } the catch (AuthenticationException failed) {/ / 5 call AuthenticationFailureHandler onAuthenticationFailure interface authentication failed Failure processing (can through inheritance AuthenticationFailureHandler write failure processing logic) unsuccessfulAuthentication (request, response, failed);return; }... / / 6 certification after a successful call AuthenticationSuccessHandler onAuthenticationSuccess interface failure processing (by AuthenticationSuccessHandler inheritance SuccessfulAuthentication (request, response, chain, authResult); }Copy the code

From the source, the whole process is actually very clear: from the judgment of whether to deal with, to the certification, the final judgment of the certification results, respectively, to make a successful certification and certification failure of the processing.

This time we will request localhast:8080/get_user/test. Since there is no permission, the login screen will be redirected to the user. We will enter the wrong account and password first to see if the authentication failure is consistent with our conclusion.

The result is as expected, you may wonder why the prompt is in Chinese, this has to say that Security 5 started to support Chinese, which means that Chinese programmers are becoming more and more important in the world!!

Enter the correct password this time and look at the returned Authtication object information:

As you can see, this success returns an Authticated = ture, with no password for the user account and an admin permission we defined. Let go of the breakpoints, due to the success of the Security default processor is SimpleUrlAuthenticationSuccessHandler, the processor will redirect to visit before the address, namely localhast: 8080 / the get_user/test. This is the end of the process. No, we still need a Session, which we see in the browser Cookie:

3, BasicAuthenticationFilter

BasicAuthenticationFilter like UsernameAuthticationFilter, but the difference is clear, BasicAuthenticationFilter main parameters to obtain Authorization from the Header information, and then call certification, certification after the success of the last direct access to the interface, Don’t jump through AuthenticationSuccessHandler like UsernameAuthticationFilter process. Here is not posted code, want to understand the students can directly look at the source code. It is one thing to note, however, that the BasicAuthenticationFilter onSuccessfulAuthentication () successfully processed method is an empty method.

To test BasicAuthenticationFilter, we need to conceal the SpringSecurityConfig formLogin () replace httpBasic () to support BasicAuthenticationFilter, ‘localhast:8080/get_user/test’; ‘localhast:8080/get_user/test’; ‘localhast:8080/get_user/test’;

At this point, we can obtain the Authorization parameters, and then parse and obtain the account and password information for authentication. We view the certification after a successful return Authtication of object information is and the same UsernamePasswordAuthticationFilter, finally calling the next filter again, Because have certification success goes straight to the FilterSecurityInterceptor for authentication.

4, AnonymousAuthenticationFilter

Why want to bring down AnonymousAuthenticationFilter here, because does not exist in the Security account not a claim (here may be describe not clear, but it is roughly), On this, a Security official specifically designated the AnonymousAuthenticationFilter used in front of all the filter authentication failed, the automatically create a default anonymous users, anonymous access. Remember we see anonymous when explaining SecurityContextPersistenceFilter autication information? If you don’t remember it, you have to go back and look at it.

5, ExceptionTranslationFilter

ExceptionTranslationFilter actually didn’t do any filtering processing, but don’t look down upon it, it is the largest and the most great place is that it captures AuthenticationException and AccessDeniedException, If there is abnormal is the two exceptions will call handleSpringSecurityException () method for processing. Use the /get_user interface to modify the /get_user/AccessDeniedException:

  • Add @ EnableGlobalMethodSecurity on the Controller (prePostEnabled = true) to enable the Security access control method level
  • Add @preauthorize (“hasRole(‘user’)”) to the interface to allow only accounts with user roles to access.
@RestController
@EnableGlobalMethodSecurity(prePostEnabled =truePublic class TestController {@preauthorize () public class TestController {@preauthorize ()"hasRole('user')") // Only user is allowed to access @getMapping ("/get_user/{username}")
    public String getUser(@PathVariable  String username){
        returnusername; }}Copy the code

Restart the project, re-access the /get_user interface, enter the correct account password, and a 403 status error page is returned, as we did in the previous flow. Debug:

The exception object is AccessDeniedException. We’ll look at accessDeniedHandler AccessDeniedException abnormal after processing method. The handle (), into the AccessDeniedHandlerImpl’s handle () method, This method checks whether the system is configured with an errorPage, and if not, directly sets the 403 status code in response.

6, FilterSecurityInterceptor

FilterSecurityInterceptor is the last in the Security filter chain, is also one of the most important, its main function is to judge authentication success if users have permission to access the interface, Its main processing method is to call the superclass (AbstractSecurityInterceptor) super. BeforeInvocation (fi), we come to comb the process of this method:

  • Through the obtainSecurityMetadataSource (). The getAttributes () gets the current permissions required to access address information
  • Obtain the permission information of the current access user by authenticateIfRequired()
  • Through the accessDecisionManager. Decide () using the right of voting mechanism, sentenced to power failure directly thrown AccessDeniedException anomalies
protected InterceptorStatusToken beforeInvocation(Object object) { ...... / / 1 for permission to access the address information Collection < ConfigAttribute > attributes = this. ObtainSecurityMetadataSource (). The getAttributes (object);if (attributes == null || attributes.isEmpty()) {
		
		    ......
		    
			returnnull; }... // 2 Obtain the permission information of the current access user Authentication = authenticateIfRequired(); Try {// 3 By default, the main room base.decide () method is called and the AccessDecisionVoter object is used internally to determine the voting mechanism. Sentenced to power failure directly thrown AccessDeniedException abnormal enclosing the accessDecisionManager. Decide (authenticated, object, attributes). } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; }...return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
	}
Copy the code

In fact, the whole process does not seem complicated. It is mainly divided into three parts. The first one is to obtain the permission information of the address, the second one is to obtain the permission information of the current user, and the last one is to judge whether the user has the right through the voting mechanism.

Iv. Personal summary

The core of the entire authorization process lies in the processing of these core filters. Here I use the sequence diagram to summarize the authorization processCopy the code

The code that describes the authorization process in this article can access the security module in the code repository at the github address of the project: github.com/BUG9/spring…

If you are interested in these, welcome to star, follow, favorites, forward to give support!


Welcome to the next Spring Security parsing part II – The authentication process