User Authentication and Authorization of Micro Services

Keywords: Java Lombok Spring Apache

[TOC]

AOP Implements Logon Status Check

stay Chat about user authentication and authorization for micro services (Part 1) In this paper, several common authentication and authorization schemes under microservices are briefly introduced, and a minimalist demo is written using JWT to simulate Token issuance and verification.The purpose of this article is to continue with the above points, such as how Token can transfer between multiple microservices and how to use AOP to achieve unified verification of login status and rights.

In order to make the login checking logic universal, we usually choose to use filters, interceptors, AOP and other means to achieve this function.This section focuses on the use of AOP for login status checking, because AOP can also block protected resource access requests and do some necessary checks before accessing resources.

First you need to add AOP dependencies to your project:

<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Define a comment that identifies which methods require login checking before being accessed.The code is as follows:

package com.zj.node.usercenter.auth;

/**
 * Methods marked by this annotation need to check login status
 *
 * @author 01
 * @date 2019-09-08
 **/
public @interface CheckLogin {
}

Write a facet that implements the specific logic for login state checking as follows:

package com.zj.node.usercenter.auth;

import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * Logon State Check Face Class
 *
 * @author 01
 * @date 2019-09-08
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CheckLoginAspect {

    private static final String TOKEN_NAME = "X-Token";

    private final JwtOperator jwtOperator;

    /**
     * This method is executed before the @CheckLogin annotation identifies it
     */
    @Around("@annotation(com.zj.node.usercenter.auth.CheckLogin)")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        // Get the request object
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // Get Token from header
        String token = request.getHeader(TOKEN_NAME);

        // Verify Token is legal
        Boolean isValid = jwtOperator.validateToken(token);
        if (BooleanUtils.isFalse(isValid)) {
            log.warn("Logon state check failed, invalid Token: {}", token);
            // Throw a custom exception
            throw new SecurityException("Token Wrongful!");
        }

        // Verification passes to set user information into request
        Claims claims = jwtOperator.getClaimsFromToken(token);
        log.info("Logon status check passed with username:{}", claims.get("userName"));
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("userName", claims.get("userName"));
        request.setAttribute("role", claims.get("role"));

        return joinPoint.proceed();
    }
}

Two interfaces are then written to simulate protected resources and obtain token s.The code is as follows:

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    private final JwtOperator jwtOperator;

    /**
     * Resources that need to be checked for login status before they can be accessed
     */
    @CheckLogin
    @GetMapping("/{id}")
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }

    /**
     * Simulate generation token
     *
     * @return token
     */
    @GetMapping("gen-token")
    public String genToken() {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("id", 1);
        userInfo.put("userName", "Small Cup");
        userInfo.put("role", "user");

        return jwtOperator.generateToken(userInfo);
    }
}

Finally, let's do a simple test to see if a faceted method is executed to check login status when accessing protected resources.Start the project first to get token:

Bring token in the header when accessing protected resources:

Access succeeded, at which point the console output is as follows:

Tips:

Instead of using filters or interceptors for login verification, AOP is used because the code written using AOP is cleaner and pluggable using custom annotations, such as accessing a resource without having to do login checking, so you only need the @CheckLogin annotationRemove the solution.On the other hand, AOP is an important basic knowledge, which is often asked in interviews. Through this practical application example, we can have a certain understanding of the skills of using AOP.

Of course, you can also choose a filter or interceptor to achieve this. No matter which way is the best, after all, these three ways have their own characteristics and advantages and disadvantages, which need to be selected according to the specific business scenario.

Feign implements Token delivery

In a microservice architecture, Feign is often used to invoke the interface provided by other microservices. If the interface needs to check the login state, the Token carried by the current client request must be passed.By default, Feign does not carry any additional information when requesting interfaces from other services, so we have to consider how tokens are passed between microservices.

There are two main ways for Feign to deliver Token, the first using the @RequestHeader annotation of Spring MVC.Examples include the following:

@FeignClient(name = "order-center")
public interface OrderCenterService {

    @GetMapping("/orders/{id}")
    OrderDTO findById(@PathVariable Integer id,
                      @RequestHeader("X-Token") String token);
}

The method in Controller also needs this annotation to get the Token from the header and pass it to Feign.The following:

@RestController
@RequiredArgsConstructor
public class TestController {

    private final OrderCenterService orderCenterService;

    @GetMapping("/{id}")
    public OrderDTO findById(@PathVariable("id") Integer id,
                            @RequestHeader("X-Token") String token) {
        return orderCenterService.findById(id, token);
    }
}

As you can see from the example above, the advantages of using the @RequestHeader annotation are simplicity and intuition, while the disadvantages are obvious.This is possible when only one or two interfaces need to pass a Token, but if there are many remote interfaces that need to pass a Token, adding this annotation to each method will obviously add a lot of duplicate work.

Therefore, the second way to pass Token is more general, which implements Token delivery by implementing a Feign request interceptor, then obtaining the Token carried by the current client request in the interceptor and adding it to Feign's request header.Examples include the following:

package com.zj.node.contentcenter.feignclient.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * Request interceptor for Token delivery between services
 *
 * @author 01
 * @date 2019-09-08
 **/
public class TokenRelayRequestInterceptor implements RequestInterceptor {

    private static final String TOKEN_NAME = "X-Token";

    @Override
    public void apply(RequestTemplate requestTemplate) {
        // Get the current request object
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // Get Token from header
        String token = request.getHeader(TOKEN_NAME);

        // Pass token
        requestTemplate.header(TOKEN_NAME,token);
    }
}

You then need to configure the package name path for the request interceptor in the configuration file, otherwise it will not take effect.The following:

# Define feign-related configurations
feign:
  client:
    config:
      # default stands for global configuration
      default:
        requestInterceptor:
          - com.zj.node.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor

RestTemplate implements Token delivery

In addition to Feign, RestTemplate may be used in some cases to request interfaces for other services, so this section also describes how Token delivery can be achieved with RestTemplate.

RestTemplate also has two ways to deliver a Token, the first way being to use the exchange() method when requesting because it can receive a header.Examples include the following:

@RestController
@RequiredArgsConstructor
public class TestController {

    private final RestTemplate restTemplate;

    @GetMapping("/{id}")
    public OrderDTO findById(@PathVariable("id") Integer id,
                            @RequestHeader("X-Token") String token) {
        // Pass token                    
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Token", token);

        return restTemplate.exchange(
                "http://order-center/orders/{id}",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                OrderDTO.class,
                id).getBody();
    }
}

The other is to implement the ClientHttpRequestInterceptor interface, which is RestTemplate's interceptor interface, similar to Feign's interceptor, used to implement common logic.The code is as follows:

public class TokenRelayRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final String TOKEN_NAME = "X-Token";

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        // Get the current request object
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest servletRequest = attributes.getRequest();
        // Get Token from header
        String token = servletRequest.getHeader(TOKEN_NAME);

        // Deliver Token
        request.getHeaders().add(TOKEN_NAME,token);
        return execution.execute(request, body);
    }
}

Finally, you need to register the implemented interceptor with RestTemplate for it to take effect, as follows:

@Configuration
public class BeanConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(
                new TokenRelayRequestInterceptor()
        ));

        return restTemplate;
    }
}

AOP Implements User Rights Verification

In the first section, we describe how to use AOP for login state checking. In addition, some protected resources may require users to have specific permissions to access them, so we have to check the permissions before the resources can be accessed.Permission checking can also be implemented using filters, interceptors, or AOPs, as in the previous section using AOP as an example.

It's not too complex to do checking logic here, just to determine if the user is a role or not.So first define a comment that has a value that identifies which role a protected resource requires the user to be in to allow access.The code is as follows:

package com.zj.node.usercenter.auth;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Methods marked by this annotation need to check user permissions
 *
 * @author 01
 * @date 2019-09-08
 **/
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {

    /**
     * Allowed role names
     */
    String value();
}

Then define a facet to implement specific privilege checking logic.The code is as follows:

package com.zj.node.usercenter.auth;

import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * Permission Verification Face Class
 *
 * @author 01
 * @date 2019-09-08
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuthAspect {

    private static final String TOKEN_NAME = "X-Token";

    private final JwtOperator jwtOperator;

    /**
     * This method is executed before the method identified by the @CheckAuthorization annotation is executed
     */
    @Around("@annotation(com.zj.node.usercenter.auth.CheckAuthorization)")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        // Get the request object
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // Get Token from header
        String token = request.getHeader(TOKEN_NAME);

        // Verify Token is legal
        Boolean isValid = jwtOperator.validateToken(token);
        if (BooleanUtils.isFalse(isValid)) {
            log.warn("Logon state check failed, invalid Token: {}", token);
            // Throw a custom exception
            throw new SecurityException("Token Wrongful!");
        }

        Claims claims = jwtOperator.getClaimsFromToken(token);
        String role = (String) claims.get("role");
        log.info("Logon status check passed with username:{}", claims.get("userName"));

        // Verify that the user role name matches the role name defined by the protected resource
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckAuthorization annotation = signature.getMethod()
                .getAnnotation(CheckAuthorization.class);
        if (!annotation.value().equals(role)) {
            log.warn("Permission check failed!Current user role:{} User roles that allow access:{}",
                    role, annotation.value());
            // Throw a custom exception
            throw new SecurityException("Permission check failed, no access to the resource!");
        }

        log.info("Permission validation passed");
        // Set user information to request
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("userName", claims.get("userName"));
        request.setAttribute("role", claims.get("role"));

        return joinPoint.proceed();
    }
}

Just add this comment and set the role name when using it, as in the following example:

/**
 * Resources that need to be checked for login status and permissions before they can be accessed
 */
@GetMapping("/{id}")
@CheckAuthorization("admin")
public User findById(@PathVariable Integer id) {
    log.info("get request. id is {}", id);
    return userService.findById(id);
}

Posted by siri on Sun, 08 Sep 2019 09:24:46 -0700