Introduction: This article is the final part of the series “Design and Implementation of Authentication and API permission Control in Microservice Architecture”. The previous three parts have explained the process and main details of authentication and API permission control. This long article concludes this series by covering my experience of stomping on endpoints outside of the authorization and authentication process and the Spring Security filter part. Welcome to this article series.

1. Review

First, as usual, a review of the previous article. 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. This paper introduces the configuration of resource server and the configuration classes involved, and then focuses on token and API-level authentication.

This article covers the remaining two built-in endpoints: logout and refresh tokens. The cancellation token processing and Spring Security default endpoint provide some ‘/ logout’ some differences, not only the information in the empty SpringSecurityContextHolder, to increase storage token to empty. The other refresh token endpoint is actually the same API as the previous request authorization, except that the grant_type parameter is different.

In addition to the two built-in endpoints above, several Spring Security filters will be highlighted later. Api-level operation permission verification was supposed to be implemented through Spring Security filters, specifically here to learn again, step on a pit.

The last part is the summary of this series, and discusses the existing shortcomings and follow-up work.

2. Other endpoints

2.1 Deregistering an Endpoint

The Auth system’s built-in logout endpoint /logout was mentioned in article 1, and the following /logout configuration should be familiar if you remember the resource server configuration from Article 3.

            / /...
                .and().logout()
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .addLogoutHandler(customLogoutHandler());Copy the code

The main functions of the above configuration are as follows:

  • Set the URL for logging out
  • Clearing Authentication Information
  • Set the handling mode for successful logout
  • Set a custom logout processing mode

Of course, there are more options in LogoutConfigurer, and here I list the configuration items required for the project. These configuration items surround the LogoutFilter filter. By the way, Spring Security filters. Its use springSecurityFillterChian as the entrance security filter, all kinds of filters in order specific as follows:

  • SecurityContextPersistenceFilter: associated with SecurityContext security context information
  • HeaderWriterFilter: Adds some headers to the HTTP response
  • CsrfFilter: prevents CSRF attacks. This function is enabled by default
  • LogoutFilter: the filter that processes the logout
  • UsernamePasswordAuthenticationFilter: forms authentication filter
  • RequestCacheAwareFilter: indicates the cache request request
  • SecurityContextHolderAwareRequestFilter: this filter to a package of ServletRequest, making the request has a richer API
  • Filter AnonymousAuthenticationFilter: anonymous identity
  • SessionManagementFilter: Session-specific filter, used to prevent session-fixation protection attacks and to limit the number of sessions that can be opened by the same user
  • ExceptionTranslationFilter: exception handling filter
  • The key to FilterSecurityInterceptor: web application security Filter

The various filters are briefly labeled, and a few of them will be highlighted in the next section. The LogoutFilter comes first, so let’s take a look at the UML class diagram for LogoutFilter.

logoutFilter

HttpSecurity creates LogoutConfigurer, and we configure some of the LogoutConfigurer properties here. LogoutConfigurer also creates a LogoutFilter based on these attributes.

The first and second points of the LogoutConfigurer configuration need not be explained in detail. One is to set the endpoint and the other is to clear the authentication information. For the third point, configure the handling of logout success. Since the project is separated from the front and back, the client only needs to know the status of the API interface after successful execution, and does not need to return to a specific page or pass the request down. This way is configured with the default HttpStatusReturningLogoutSuccessHandler success, therefore, direct return a status code of 200. For the fourth configuration, customize the method of logout handling. In this case, TokenStore is used to operate the token. TokenStore, as described in the configuration of the previous article, uses JdbcTokenStore. First check the validity of the request, if it is valid, then operate on it, remove refreshToken and existingAccessToken.

public class CustomLogoutHandler implements LogoutHandler {

    / /...

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // Make sure tokenStore is injected
        Assert.notNull(tokenStore, "tokenStore must be set");
       // Get the authentication information for the header
        String token = request.getHeader("Authorization");
        Assert.hasText(token, "token must be set");
        // Verify whether the tokens conform to the JwtBearer format
        if (isJwtBearerToken(token)) {
            token = token.substring(6);
            OAuth2AccessToken existingAccessToken = tokenStore.readAccessToken(token);
            OAuth2RefreshToken refreshToken;
            if(existingAccessToken ! =null) {
                if(existingAccessToken.getRefreshToken() ! =null) {
                    LOGGER.info("remove refreshToken!", existingAccessToken.getRefreshToken());
                    refreshToken = existingAccessToken.getRefreshToken();
                    tokenStore.removeRefreshToken(refreshToken);
                }
                LOGGER.info("remove existingAccessToken!", existingAccessToken);
                tokenStore.removeAccessToken(existingAccessToken);
            }
            return;
        } else {
            throw newBadClientCredentialsException(); }}/ /...
}Copy the code

Perform the following request:

method: get
url: http://localhost:9000/logout
header:
{
    Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=
}Copy the code

If the logout succeeds, 200 is returned, clearing the token and SecurityContextHolder.

2.2 Refreshing Endpoints

As mentioned in the first article, the validity period of the token is not very long, and the refresh period of the token is very long. In order not to affect the user experience, you can use the refresh token to dynamically refresh the token. Refresh tokens are primarily associated with refreshTokengranters, which manage a List of grantTypes for a specific true grantor. The granter of refresh_ token is RefreshTokenGranter, and granters are distinguished by grantType. Perform the following request:

method: post url: http://localhost:12000/oauth/token? grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMt NGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZh LTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoi ZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE
header:
{
    Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=
}Copy the code

In the case of refresh_ token being correct, the response returned is the same as the normal response received by /oauth/token. See article 2 for detailed code.

3. Spring SecurityThe filter

In the previous section we covered the implementation details of the two built-in endpoints and also mentioned the HttpSecurity filter, because that’s how the implementation of the deregistration endpoint is implemented. Core filters are:

  • FilterSecurityInterceptor
  • UsernamePasswordAuthenticationFilter
  • SecurityContextPersistenceFilter
  • ExceptionTranslationFilter

This section focuses on the UsernamePasswordAuthenticationFilter and FilterSecurityInterceptor.

3.1 UsernamePasswordAuthenticationFilter

The author at first to see about the filter, to introduce to UsernamePasswordAuthenticationFilter many articles. If you just introduced Spring-Security, you would be familiar with the /login endpoint. SpringSecurity enforces that our form login page must POST requests to the /login URL, and that the username and password arguments must be username and password. If not, it will not work properly. The reason is that when we call the formLogin HttpSecurity object method, it will give us a filter UsernamePasswordAuthenticationFilter registration. Take a look at the source code for the filter.

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    // User name and password
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    / / post request/login
    public UsernamePasswordAuthenticationFilter(a) {
        super(new AntPathRequestMatcher("/login"."POST"));
    }
    / / implementation abstract class AbstractAuthenticationProcessingFilter abstract methods, try to test and verify
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if(postOnly && ! request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        / /...

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        / /...
        return this.getAuthenticationManager().authenticate(authRequest); }}Copy the code
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware.MessageSourceAware {
    / /...

    // call requiresAuthentication to determine whether the request requiresAuthentication and, if so, call attemptAuthentication
    // There are three possible results:
    / / 1. Authentication object
    //2. AuthenticationException
    //3. The Authentication object is empty
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        // No validation required, continue to pass
        if(! requiresAuthentication(request, response)) { chain.doFilter(request, response);return;
        }
        Authentication authResult;

        try {
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        / /...
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        successfulAuthentication(request, response, chain, authResult);
    }

    // The inheritance class must implement this abstract method for the actual execution of authentication
    public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException,
            ServletException;
    // The default behavior for successful authentication
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        / /...
    }
    // Default behavior of failed authentication
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
    / /...}.../ / set the AuthenticationManager
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager; }... }Copy the code

Because UsernamePasswordAuthenticationFilter inherited AbstractAuthenticationProcessingFilter have the function of the filter. Set a the authenticationManager AbstractAuthenticationProcessingFilter requirement, the authenticationManager implementation class will be treated as the actual request of certification. AbstractAuthenticationProcessingFilter will intercept conforms to the request of filtering rules, and attempts to perform authentication. Subclasses must implement the attemptAuthentication method, which performs specific authentication. The processing after authentication and logout is similar. If successful, the returned Authentication object is stored in the SecurityContext, and SuccessHandler is called. You can also set the specified URL and specify a custom SuccessHandler. If the authentication fails, the 401 code is returned to the client by default. You can also set the URL to specify a customized FailureHandler.

Based on UsernamePasswordAuthenticationFilter custom AuthenticationFilte very many cases, recommend a blog here Spring Security (5) – began to implement a IP_Login, write more detail.

3.2 FilterSecurityInterceptor

FilterSecurityInterceptor are comparatively complicated, filterchain is the core of the filter, mainly responsible for web application security authorization. First look at the for custom FilterSecurityInterceptor configuration.

    @Override
    public void configure(HttpSecurity http) throws Exception {.../ / add CustomSecurityFilter, on FilterSecurityInterceptor filter order
        http.antMatcher("/oauth/check_token").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);

    }
    // Provide instantiated custom filters
    @Bean
    public CustomSecurityFilter customSecurityFilter(a) {
        return new CustomSecurityFilter();
    }Copy the code

Can see from the above configuration, in FilterSecurityInterceptor CustomSecurityFilter, the location for the match to/request/check_token, will invoke the access to the filter. Below for FilterSecurityInterceptor class diagram, in which also adds CustomSecurityFilter and related implementation of the interface class, for easy to see.

FilterSecurityInterceptor

CustomSecurityFilter is imitation FilterSecurityInterceptor implementation, inherit the AbstractSecurityInterceptor and implement the Filter interface. The whole process need to rely on the AuthenticationManager, the AccessDecisionManager and FilterInvocationSecurityMetadataSource. AuthenticationManager is an AuthenticationManager, which realizes the entrance of user authentication. AccessDecisionManager is an access decision that determines whether a user has sufficient permissions to access a resource. FilterInvocationSecurityMetadataSource’s definition, sources of data resources by defining a resource which roles can be visited. From the class diagram above you can see the custom CustomSecurityFilter implements the AccessDecisionManager and FilterInvocationSecurityMetadataSource again at the same time. SecureResourceFilterInvocationDefinitionSource and SecurityAccessDecisionManager respectively. The following describes the main configurations.

// Secure HTTP resources with an implemented filter
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    // The method actually called by the Filter chain, via the Invoke proxy
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    // Proxy methods
    public void invoke(FilterInvocation fi) throws IOException, ServletException     {
        / /... omit}}Copy the code

The code above is the implementation in FilterSecurityInterceptor, no lists the specific implementation details, we will focus on implementation of the custom here.

public class CustomSecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    SecureResourceFilterInvocationDefinitionSource invocationSource;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private SecurityAccessDecisionManager decisionManager;

    // Set the attributes in the parent class
    @PostConstruct
    public void init(a) {
        super.setAccessDecisionManager(decisionManager);
        super.setAuthenticationManager(authenticationManager);
    }
    // The main filter method is the same as the original
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //logger.info("doFilter in Security ");
        Invocation (request, Response, chain)
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //beforeInvocation calls the logic in SecureResourceDataSource, similar to before in AOP
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            // Execute the next interceptor
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());            
        } finally {
            // Complete the follow-up, similar to after in AOP
            super.afterInvocation(token, null); }}/ /...

    / / resource source data definition, set to custom SecureResourceFilterInvocationDefinitionSource
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource(a) {
        returninvocationSource; }}Copy the code

The above custom CustomSecurityFilter is the same process as we explained before. The three interfaces that are the main dependencies have instantiation injection in the implementation. Take a look at the beforeInvocation method of the parent class, which omits some unimportant snippets of code.

protected InterceptorStatusToken beforeInvocation(Object object) {  
    // Obtain configured permission properties based on SecurityMetadataSource
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);  
    / /...
    // Determine whether to re-authenticate the authentication entity. The default value is no
    Authentication authenticated = authenticateIfRequired();  

    // Attempt authorization  
    try {  
        // The decision manager starts to decide whether to authorize or not, and throws AccessDeniedException if authorization fails
        this.accessDecisionManager.decide(authenticated, object, attributes);  
    }  
    catch (AccessDeniedException accessDeniedException) {  
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,  
                accessDeniedException));  

        throwaccessDeniedException; }}Copy the code

As you can see from the above code, the first step is to get the configured permission properties based on SecurityMetadataSource. The accessDecisionManager uses the permission list information. Then check whether the authentication entity needs to be re-authenticated. The default value is no. The second step is for the decision manager to start deciding whether to authorize or not, and if authorization fails, throw an AccessDeniedException.

(1). Obtain the configured permission attributes

public class SecureResourceFilterInvocationDefinitionSource implements FilterInvocationSecurityMetadataSource.InitializingBean {
    private PathMatcher matcher;
    //map saves the permission set corresponding to the configured URL
    private static Map<String, Collection<ConfigAttribute>> map = new HashMap<>();

    // Loop based on the passed object URL
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        logger.info("getAttributes");
        // Do instanceof instead
        FilterInvocation filterInvocation = (FilterInvocation) o;
        //String method = filterInvocation.getHttpRequest().getMethod();
        String requestURI = filterInvocation.getRequestUrl();
        // Circulates the resource path. If the accessed Url matches the resource path Url, returns the permission required by the Url
        for (Iterator<Map.Entry<String, Collection<ConfigAttribute>>> iterator = map.entrySet().iterator(); iter.hasNext(); ) {
            Map.Entry<String, Collection<ConfigAttribute>> entry = iterator.next();
            String url = entry.getKey();

            if (matcher.match(url, requestURI)) {
                returnmap.get(requestURI); }}return null;
    }

    / /...

    // Set the permission set, that is, map
    @Override
    public void afterPropertiesSet(a) throws Exception {
        logger.info("afterPropertiesSet");
        // To match the access resource path
        this.matcher = new AntPathMatcher();
        // Can have multiple permissions
        Collection<ConfigAttribute> atts = new ArrayList<>();
        ConfigAttribute c1 = new SecurityConfig("ROLE_ADMIN");
        atts.add(c1);
        map.put("/oauth/check_token", atts); }}Copy the code

Above are the details of the implementation of getAttributes(), which pulls the requested URL to match the predefined restricted resources and returns the desired permissions and roles. The system reads the configured map set at startup and matches intercepted requests. Comments in the code are more detailed, not to say here.

(2) decision manager

public class SecurityAccessDecisionManager implements AccessDecisionManager {
    / /...

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        logger.info("decide url and permission");
        // Set is empty
        if (collection == null) {
            return;
        }

        Iterator<ConfigAttribute> ite = collection.iterator();
        If UserDetailsService is implemented, the user permission is loadUserByUsername returns the corresponding permission of the user
        while (ite.hasNext()) {
            ConfigAttribute ca = ite.next();
            String needRole = ca.getAttribute();
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                logger.info("GrantedAuthority: {}", ga);
                if (needRole.equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        logger.error("AccessDecisionManager: no right!");
        throw new AccessDeniedException("no right!");
    }

    / /...
}Copy the code

The above code is an implementation of the decision manager, and its logic is relatively simple, matching the permissions of the request with those required by the set restricted resource, returning if it has, otherwise throwing an incorrect permission exception. Consensus – based and consensus – ous-based decision managers are limited in space and are not extended in our office.

In addition, the permission is obtained through the authentication mode previously configured, including password authentication and client authentication. We previously configured withClientDetails in the authorization server, so the permissions obtained with Frontend authentication are the authorities we pre-configured in the database.

4. To summarize

The main function of Auth system is authorization authentication and authentication. After the project is microservitized, the original single application based on HttpSession authentication cannot meet the requirements of the microservice architecture. Each microservice needs to authenticate access, and each microapplication needs to identify the current access user and their permissions. In particular, when there are multiple clients, including web and mobile terminals, the authentication mode in a single application architecture is not particularly appropriate. As a basic public service, authority service also needs microservitization.

In the author’s design, Auth service performs authorization authentication on the one hand, and authenticates identity legitimacy and API-level permissions based on tokens on the other hand. For a service request, the gateway invokes the Auth service to verify the validity of the token. At the same time, according to the overall situation of the current project, there are some legacy services, and these legacy services do not have enough time and manpower to carry out the micro-service transformation immediately, but also need to continue to operate. In order to adapt to the current new architecture, the scheme adopted is the operation API of these legacy services and apI-level operation permission identification in Auth service. Context information required for apI-level operation permission verification needs to be negotiated with the client based on the service. The corresponding information should be obtained from the token and transmitted to the Auth service. However, the context verification information obtained from the header should be minimized.

The author will be the development of Auth system involved in most of the code and source code analysis, as for some of the content and details, the reader can expand.

5. Deficiencies and follow-up work

5.1 Existing deficiencies

  • Universality of apI-level operation permission verification

    (1). For apI-level operation permission verification, the corresponding context information needs to be constructed when invoked at the gateway. Context information depends on the payload in the token. If there is too much information, the token is too long. As a result, the header length of each client request becomes longer.

    (2). Not all operation interfaces can be covered, and this problem is quite serious. According to the context set, it is likely that the permissions of many interfaces cannot be identified. Because the context involved in the interface is by no means fully available. Our project, at this stage, can barely cover the minimum set of contexts defined, but it is really not optimistic about the service interfaces that will be expanded later.

    (3). Each interface of each service registers its required permissions in the Auth service, which is too troublesome. The Auth service needs to maintain such information in addition.

  • System throughput bottleneck caused by calling Auth service at gateway

    (1). This is actually easy to understand, as Auth service is a public basic service, most service interfaces will require authentication, and Auth service needs to go through complexity.

    (2). The gateway calls the Auth service and blocks the call. Only after the Auth service returns the verification result, further processing will be done. Although The Auth service can be multi-instance deployment, but after a large amount of concurrency, its bottleneck is obvious, serious may cause the whole system is unusable.

5.2 Follow-up Work

  • From the point of view of the whole system design, the apI-level operation authority will be scattered on the interfaces of each service in the later period, and each interface will be responsible for the required permissions and identities. Spring Security also supports interface-level permission validation, which is intended to be compatible with new and legacy services, primarily legacy services, which are scattered across interfaces.
  • By spreading apI-level operation rights to each service interface, the response of Auth service can be improved accordingly. The gateway can forward or reject requests in a timely manner.
  • The context information required for apI-level operation permissions is really complex for each interface, and we did spend a lot of time managing permissions for hundreds of operation interfaces for mobile services at the same time. !

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. Configuring form Login
  2. Spring Security3 source – FilterSecurityInterceptor analysis
  3. Core Security Filters
  4. Spring Security(4)- Source code analysis of core filters

reading

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