Introduction: This article is the third part of the “Design and Implementation of Authentication and API Permission Control in Microservices Architecture” series, focusing on token and API-level authentication. This article examines most of the code involved, so feel free to subscribe to this series.

1. Review

Before I start this article, let me remind you of the previous two articles. In the first part, the design and implementation of authentication and API authority control in microservice architecture (I) introduces the background of the project, technical research and final selection. The design and implementation of authentication authentication and API permission control in micro-service architecture (ii) draws a brief flow chart of login and verification, and focuses on the implementation of user identity authentication and token issuance.

check

This article focuses on authentication, including two aspects: token validity and API-level operation rights. Firstly, the legitimacy of token is easy to understand. The second article explains a series of procedures for obtaining authorization token. Whether the token is issued by the authentication server must be verified. Secondly, for apI-level operation permissions, the request that the context information does not have operation permissions is directly rejected. Of course, the validity check of the design token comes first, and then the operation permissions are verified. If the previous verification is rejected, the operation permissions are verified.

2. Configure the resource server

ResourceServer configuration is listed in the first article, before entering authentication, make clear the configuration here, even if some configurations are not used in this project, you may use in your own project.

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    // HTTP security configuration
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // Disable CSRF and set session policy
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()// Access is allowed by default
                .requestMatchers().antMatchers("/ * *")
                .and().authorizeRequests()
                .antMatchers("/ * *").permitAll()
                .anyRequest().authenticated()
                .and().logout() //logout Logout of the endpoint configuration
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .addLogoutHandler(customLogoutHandler());

    }

    // Add a custom CustomLogoutHandler
    @Bean
    public CustomLogoutHandler customLogoutHandler(a) {
        return new CustomLogoutHandler();
    }

    // Resource security configuration is related
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources); }}Copy the code

@enableresourceserver this is an important annotation, a handy annotation for the OAuth2 resource server. This causes Spring Security Filter to validate the request against the OAuth2 token in the request. The annotation is usually used with EnableWebSecurity created hard-coded @ Order (3) WebSecurityConfigurerAdapter, because the current spring’s technology, the Order of the Order is not easy to modify, So avoid having other configurations of order=3 in your project.

(2). Associated HttpSecurity, similar to the previous configuration of the “HTTP” element in Spring Security XML, allows you to configure Web-based Security for specific HTTP requests. The default is to apply to all requests, and the requestMatcher can specify specific URL ranges. The HttpSecurity class diagram is shown below.

HttpSecurity

To summarize: HttpSecurity is an implementation class of the SecurityBuilder interface, which, as the name suggests, is an HTTP security-related builder. Of course we may need some configuration at build time, and when we call the methods of the HttpSecurity object, we are actually doing configuration.

AuthorizeRequests (), , httpBasic formLogin () () the three method returns were ExpressionUrlAuthorizationConfigurer, FormLoginConfigurer, HttpBasicConfigurer, They are implementation classes for the SecurityConfigurer interface, and each represents a different type of security configurator. So, in general, when we configure, we need a SecurityBuilder, SecurityBuilder(such as HttpSecurity in our case), The creation of a SecurityBuilder instance requires the pairing of several SecurityConfigurer instances.

(3). The associated ResourceServerSecurityConfigurer, add special for the resource server configuration, the default is suitable for many applications, but this modification for the unit with the resourceId at least. The class diagram is shown below.

ResourceServerSecurityConfigurer

ResourceServerSecurityConfigurer created OAuth2 OAuth2AuthenticationProcessingFilter filter core, and its fixed OAuth2AuthenticationManager. Only blocked by OAuth2AuthenticationProcessingFilter to oauth2 related request is processed by a special identity authentication device. At the same time set up TokenExtractor, exception processing implementation.

OAuth2AuthenticationProcessingFilter is OAuth2 protect resources authentication filters in advance. Use with OAuth2AuthenticationManager according to request access to OAuth2 token, OAuth2Authentication will be used to fill the Spring Security context. OAuth2AuthenticationManager is given in the previous article, for class diagrams in the AuthenticationManager associated with token authentication. Here skip to post the source code to explain, readers can read by themselves.

3. To the endpoint

Authentication mainly uses the built-in endpoint /oauth/check_token. The author puts endpoint analysis first because it is the only entry point for authentication. Let’s take a look at the main code in the API interface.

    @RequestMapping(value = "/oauth/check_token")
    @ResponseBody
    publicMap<String, ? > checkToken(CheckTokenEntity checkTokenEntity) {//CheckTokenEntity is a user-defined DTO
        Assert.notNull(checkTokenEntity, "invalid token entity!");
        / / identification token
        OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(checkTokenEntity.getToken());
        // Check whether the token is empty
        if (token == null) {
            throw new InvalidTokenException("Token was not recognised");
        }
        / / not expired
        if (token.isExpired()) {
            throw new InvalidTokenException("Token has expired");
        }
        / / load OAuth2Authentication
        OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
        // Get response. The token validity is verified
        Map<String, Object> response = (Map<String, Object>) accessTokenConverter.convertAccessToken(token, authentication);

        //check for api permission
        if (response.containsKey("jti")) {
            // Verify context operation permissions
            Assert.isTrue(checkPermissions.checkPermission(checkTokenEntity));
        }

        response.put("active".true);    // Always true if token exists and not expired

        return response;
    }Copy the code

/oauth/check_token /oauth/ oauth/ oauth/ oauth/ oauth/ Oauth/Oauth/Oauth/Oauth/Oauth The main reason is to add the pre – API level permission verification.

4. Verify the token validity

As can be seen from the CheckTokenEndpoint above, token validity verification first identifies the tokens in the request body. Use the main method is ResourceServerTokenServices readAccessToken () method. The implementation class of this interface is DefaultTokenServices, where JDBC’s TokenStore is configured as described in the previous configuration.

public class JdbcTokenStore implements TokenStore {...public OAuth2AccessToken readAccessToken(String tokenValue) {
        OAuth2AccessToken accessToken = null;

        try {
            // The private extractTokenKey() method is called using the selectAccessTokenSql statement
            accessToken = jdbcTemplate.queryForObject(selectAccessTokenSql, new RowMapper<OAuth2AccessToken>() {
                public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {
                    return deserializeAccessToken(rs.getBytes(2));
                }
            }, extractTokenKey(tokenValue));
        }
        // Abnormal condition
        catch (EmptyResultDataAccessException e) {
            if (LOG.isInfoEnabled()) {
                LOG.info("Failed to find access token for token "+ tokenValue); }}catch (IllegalArgumentException e) {
            LOG.warn("Failed to deserialize access token for " + tokenValue, e);
            // The exception is removed
            removeAccessToken(tokenValue);
        }

        returnaccessToken; }...// Extract the TokenKey method
    protected String extractTokenKey(String value) {
        if (value == null) {
            return null;
        }
        MessageDigest digest;
        try {
            //MD5
            digest = MessageDigest.getInstance("MD5");
        }
        catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).");
        }

        try {
            byte[] bytes = digest.digest(value.getBytes("UTF-8"));
            return String.format("%032x".new BigInteger(1, bytes));
        }
        catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK)."); }}}Copy the code

ReadAccessToken () retrieves the complete information about the token value. The above code is relatively simple, and the logic involved is not complicated, which is explained briefly here. The following figure shows variable information about the DEBUG token verification. You can perform this operation by yourself. The screenshot is for reference only.

token

As for the next step, loadAuthentication() loads the credentials for specific Access tokens. The obtained credentials and tokens are used as convertAccessToken() arguments, and the response of the verified token is obtained.

5. Verify the API-level permission

At present, the author’s projects are all web-based authorization verification. A huge monomer application system left over before is gradually being dismantled, but it cannot be completely dismantled and perfected at present. In order to simultaneously compatible with the old and new services, minimize the intrusion on the business system, and realize the unity and independence of microservices. According to the business scenario, the author tries to verify the operation permission in Auth. The first thing that comes to mind is to configure the ResourceServer with ResourceServer, such as:

http.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")Copy the code

In this way, the API permission control of each operation interface should be placed in different business services. After receiving the request, each service should first take out the permission information such as role and scope corresponding to the token from the Auth service. This method is definitely feasible, but because the granularity of project authentication is finer, and I don’t want to change the original system for the time being, in addition to the previous gateway design, the gateway called Auth service to verify the validity of token, so I finally decided to complete the verification together in the Auth system call.

As can be seen from the configuration code of the resource server at the beginning of this article, there is no interception for all resources, because the gateway is the relevant endpoint of the Auth system, and not all request URLS will pass through the Auth system. Therefore, for all resources, the API permissions required by the authentication interface should be defined in the Auth system. Matches are then made based on context. This is the second approach adopted by the author. Of course, the disadvantages of this approach are also obvious. Once the amount of concurrency is large, the gateway also needs to spend time on the authentication of the Auth system, and TPS is bound to decrease a lot. For some service interfaces that do not need authentication, it will also cause unavailability. Another point is that for some special permissions of the interface, the need for a lot of context information, may not be completely covered, for this, the author’s solution is divided into two aspects: one is to try to classify these special cases, a certain kind of unified solution; The second is to reduce the severity of the check, for context check failure directly rejected, and pass, for some interfaces, before the operation in the interface, special places have to be checked again.

I mentioned earlier that the endpoint has been rewritten. CheckTokenEntity is a custom DTO, which defines the context for authentication. This class refers to the minimum set of operation permissions that can be checked, such as URI, roleId, affairId, and so on. The CheckPermissions interface is also defined, and its method checkPermission(CheckTokenEntity CheckTokenEntity) returns the result of the check. And its concrete implementation class is defined in Auth system. Examples of calls in my project are as follows:

@Component
public class CustomCheckPermission implements CheckPermissions {

    @Autowired
    private PermissionService permissionService;

    @Override
    public boolean checkPermission(CheckTokenEntity checkTokenEntity) {
        String url = checkTokenEntity.getUri();
        Long affairId = checkTokenEntity.getAffairId();
        Long roleId = checkTokenEntity.getRoleId();
        / / check
        if (StringUtils.isEmpty(url) || affairId <= 0 || roleId <= 0) {
            return true;
        } else {
            returnpermissionService.checkPermission(url, affairId, roleId); }}}Copy the code

For details of the changes in the Spring-Cloud-starter-OAuth2 JAR package, see my GitHub project at the end of this article. By customizing the CustomCheckPermission and overwriting the checkPermission() method, you can also verify the operation permission of your business. This aspect involves specific business, the author only provides interface in the project, the specific implementation needs to be completed by readers.

6. Summary

This article is relatively simple and focuses on token and API-level authentication. The legitimacy authentication of token is very common. Auth system designs apI-level authentication based on its own business needs and current situation. The checks of these two pieces are preloaded into Auth system, and their advantages and disadvantages are also described in the above section. Finally, the architecture design according to their own needs and status quo, the author’s solution ideas for reference only.

This article source address:

Making:Github.com/keets2012/A…

Code:Gitee.com/keets/Auth-…

Subscribe to the latest articles, welcome to follow my official account

Wechat official account


reference

  1. Technical architecture for microservices API-level permissions
  2. spring-security-oauth
  3. Spring-Security Docs

reading

Design and Implementation of Authentication And API Permission Control in Microservice Architecture (1) Design and Implementation of Authentication and API Permission Control in Microservice Architecture (2)