Hello, everyone. I’m Chen

Authentication and authorization is an essential part of the actual combat project, and Spring Security will be the preferred Security component, so Chen has opened the “Spring Security Advanced” column, write from the single architecture to OAuth2 distributed architecture authentication and authorization.

I will not introduce Spring Security too much, I believe you have used it, but also fear, compared to Shiro, Spring Security is more heavyweight, the SSM project before more enterprises are using Shiro, but after Spring Boot, Integrating Spring Security is easier, and more enterprises are using it.

Today Chen will introduce how to use Spring Security for login authentication in a project where the front and back ends are separated. The contents of the article are as follows:

The idea of separating authentication between front and back ends

The separation of front and back ends is different from traditional Web services, which cannot use session. Therefore, we use the stateless mechanism of JWT to generate tokens. The general idea is as follows:

  1. The client invokes the login interface of the server and enters the user name and password to log intokenThat is as follows:
    1. AccessToken: the client uses this token to access the server’s resources
    2. RefreshToken: refreshToken, once accessToken expires, the client needs to use refreshToken to retrieve another accessToken. Therefore, the expiration time of refreshToken is generally greater than accessToken.
  2. The client request header carries accessToken to access the resources of the server, and the server authenticates accessToken (check the signature and whether it is invalid….) If there is no problem with accessToken, pass.
  3. Once accessToken expires, the client needs to call the interface of refreshing token with refreshToken to obtain a new accessToken.

Project structures,

Chen uses the Spring Boot framework and creates two modules in the demonstration project, namely common-base and security-authentication-JWT.

1. Common-base module

This is an abstract out of the public module, this module mainly put some common classes, directory as follows:

2. Security-authentication-jwt module

Some classes that need to be customized, such as the global configuration class for Security and the configuration class for Jwt login filters, are listed below:

3. Five tables

Permission design varies according to business requirements. The RBAC specification Chen uses mainly involves five tables, namely, user table, role table, permission table, user <-> role table, and role <-> permission table, as shown below:

The SQL of the above table will be put in the source code of the case (these table fields in order to save trouble, the design is not complete, you can gradually expand according to the business)

Login Authentication Filter

Logon interface logic writing there are many kinds, today Chen introduced a definition of the login interface using filters.

Spring Security filter is UsernamePasswordAuthenticationFilter default login forms authentication, the filter does not apply to the separation of front and back side structure, so we need a custom filter.

Logic is simple, consult UsernamePasswordAuthenticationFilter this filter modification, some code is as follows:

Authentication processor AuthenticationSuccessHandler success

The filter interface once authentication is successful, will call AuthenticationSuccessHandler processing, so we can custom a successful authentication processor do their own business process, the code is as follows:

Chen only returned accessToken and refreshToken, and other business logic processing was perfected by himself.

The processor AuthenticationFailureHandler authentication failure

In the same way, once the login failed, such as a user name or password error, etc., will call AuthenticationFailureHandler processing, so we need a custom authentication failed processor, which according to return a specific exception information JSON data to the client, the code is as follows:

The logic is simple, AuthenticationException has different implementation classes and returns specific prompts based on the type of exception.

AuthenticationEntryPoint configuration

The AuthenticationEntryPoint interface calls the commerce () method when users access protected resources without authentication, such as client tokens being tampered with, So we need to customize an AuthenticationEntryPoint to return a specific prompt as follows:

AccessDeniedHandler configuration

AccessDeniedHandler When a user has successfully authenticated to access a protected resource but does not have sufficient permissions, the user will enter this handler for processing. We can implement this handler to return a specific prompt message to the client. The code is as follows:

UserDetailsService configuration

The UserDetailsService class is used to load user information, including user name, password, permission, and role set…. One of them is as follows:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
Copy the code

In the authentication logic, Spring Security will call this method to load the details of the user according to the username passed by the client. This method needs to complete the following logic:

  • Passwords match
  • Load permissions and role sets

We need to implement this interface to load user information from the database as follows:

LoginService queries the password, role, and permission from the database based on the user name. The code is as follows:

UserDetails is also an interface that defines several methods around the username, password, and permission + role set properties, so we can implement this class to extend these fields. The SecurityUser code looks like this:

Extension: The implementation of UserDetailsService generally involves five tables, namely, user table, role table, permission table, user <-> role mapping table, role <-> permission mapping table. Implementation in enterprises must follow the RBAC design rules. Chen will introduce this rule in detail later.

Token Check filter

The client request header carries a token, and the server must parse and verify the token for each request. Therefore, a token filter must be defined. The logic of the filter is as follows:

  • Get accessToken from the request header
  • Analyze, check and verify expiration time for accessToken
  • If the verification is successful, store authentication in ThreadLocal to obtain user details directly.

The above is just the basic logic, but there are specific processing in the actual development, such as putting user details into the Request attribute, Redis cache, which can implement feign’s token relay effect.

The code for verifying the filter is as follows:

Refresh the token interface

Once accessToken expires, the client must re-obtain the token with refreshToken. The traditional Web service is placed in cookie and only needs to be refreshed by the server, completely realizing no token renewal perception. However, in the backend separation architecture, the client must manually refresh with the refreshToken callback interface.

The code is as follows:

The main logic is simple, as follows:

  • Check refreshToken
  • Re-generate accessToken and refreshToken and return them to the client.

Note: In actual production, refreshToken generation method and encryption algorithm can be different from accessToken.

Login authentication filter interface configuration

The above defines an authentication filter JwtAuthenticationLoginFilter, this filter is used to log in, but not into join Spring Security in the filter chain, need to define the configuration, the code is as follows:

/ * * *@authorPublic number: code ape technical column * login filter configuration class */
@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    /** * userDetailService */
    @Qualifier("jwtTokenUserDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;

    /** * Successful login processor */
    @Autowired
    private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;

    /** * Failed to log in processor */
    @Autowired
    private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;

    /** * encryption */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /** * Configure the logon interface filter to the filter chain * 1. Configure the logon success and failure handlers * 2. Configure the user-defined userDetailService (Getting user data from the database) * 3. The custom filter configuration to spring security in the filter chain, configuration before UsernamePasswordAuthenticationFilter *@param http
     */
    @Override
    public void configure(HttpSecurity http) {
        JwtAuthenticationLoginFilter filter = new JwtAuthenticationLoginFilter();
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // Authenticate the successful handler
        filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
        // Failed to authenticate the processor
        filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
        / / use DaoAuthenticationProvider directly
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        / / set userDetailService
        provider.setUserDetailsService(userDetailsService);
        // Set the encryption algorithm
        provider.setPasswordEncoder(passwordEncoder);
        http.authenticationProvider(provider);
        / / add the filter to the UsernamePasswordAuthenticationFilter before executionhttp.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

All the logic is in the public void configure(HttpSecurity HTTP) method as follows:

  • Set authentication processor loginAuthenticationSuccessHandler success
  • The processor loginAuthenticationFailureHandler set authentication failure
  • Set the userDetailService JwtTokenUserDetailsService implementation class
  • Set the encryption algorithm passwordEncoder
  • Adding JwtAuthenticationLoginFilter this filter to the filter chain, directly added to the UsernamePasswordAuthenticationFilter before the filter.

Spring Security global configuration

The login filter is only configured above, there are some configurations that need to be done in the global configuration class as follows:

  • Apply the login filter configuration
  • The login interface and token refresh interface are allowed without interception
  • Configure AuthenticationEntryPoint and AccessDeniedHandler
  • If session is disabled, session is not required in front – end separation +JWT mode
  • Token to check filters are added to the filter chain TokenAuthenticationFilter, on UsernamePasswordAuthenticationFilter before.

The complete configuration is as follows:

/ * * *@authorPublic account: Code ape technology column *@EnableGlobalMethodSecurityEnable permission validation annotation */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
    @Autowired
    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
    @Autowired
    private RequestAccessDeniedHandler requestAccessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                // Disable form login
                .disable()
                // Apply the login filter configuration, configuration separation
                .apply(jwtAuthenticationSecurityConfig)

                .and()
                // Set URL authorization
                .authorizeRequests()
                PermitAll () indicates no interception, /login LOGIN URL, and /refreshToken refreshes the URL of the token
                // There are many more urls in the normal project, such as swagger related url, druid background URL, some static resources
                .antMatchers(   "/login"."/refreshToken")
                .permitAll()
                //hasRole() indicates that the specified role is required to access resources
                .antMatchers("/hello").hasRole("ADMIN")
                // anyRequest() All requests authenticated() must be authenticated
                .anyRequest()
                .authenticated()

                // Handle exceptions: Authentication failure and insufficient permissions
                .and()
                .exceptionHandling()
                // No access to the exception handler is allowed
                .authenticationEntryPoint(entryPointUnauthorizedHandler)
                // Authentication passed, but no permission handler
                .accessDeniedHandler(requestAccessDeniedHandler)

                .and()
                // Disable session. JWT validation does not require session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                / / configure TOKEN validation filter to the filter chain, otherwise don't take effect, in UsernamePasswordAuthenticationFilter before
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
                / / close CSRF
                .csrf().disable();
    }

    // User-defined Jwt Token filter
    @Bean
    public TokenAuthenticationFilter authenticationTokenFilterBean(a)  {
        return new TokenAuthenticationFilter();
    }

    /** * Encryption algorithm *@return* /
    @Bean
    public PasswordEncoder getPasswordEncoder(a){
        return newBCryptPasswordEncoder(); }}Copy the code

Notes of the very detailed, do not understand the serious look.

The source code of the case has been uploaded to GitHub, concerned public number: code ape technology column, reply keywords: 9529 access!

test

1, the first test login interface, the postman to http://localhost:2001/security-jwt/login, as follows:

As you can see, two tokens were successfully returned.

2, don’t carry token request headers, request http://localhost:2001/security-jwt/hello directly, as follows:

As you can see, directly into the EntryPointUnauthorizedHandler the processor.

3, carrying token to http://localhost:2001/security-jwt/hello, as follows:

The token is valid for successful access.

4, refresh token interface test, with an expired token access as follows:

5. Refresh the token interface test with unexpired token tests as follows:

As you can see, two new tokens were successfully returned.

The source code to track

More than a series of configuration is completely reference UsernamePasswordAuthenticationFilter this filter, this is the way web services login form.

Spring Security works as a series of filters, as does the login process, At first in the org. Springframework. Security. Web. Authentication. AbstractAuthenticationProcessingFilter# doFilter () method, certification matching, as follows:

AttemptAuthentication () this method the main role is to get the client pass the username, password, and encapsulate UsernamePasswordAuthenticationToken to ProviderManager authentication, The source code is as follows:

ProviderManager main process is called an abstract class AbstractUserDetailsAuthenticationProvider# authenticate () method, the diagram below:

The retrieveUser() method simply calls the userDetailService to retrieve the user information. Then authentication, once the authentication is successful or failed, the corresponding failure or success processor will be called for processing.

conclusion

Spring Security is heavy, but really useful, especially for implementing the Oauth2.0 specification, which is very simple and convenient.

The source code of the case has been uploaded to GitHub, concerned public number: code ape technology column, reply keywords: 9529 access!