Spring boot uses AOP to realize the simple and flexible security authentication practice of REST interface

1 Authorized implementation

This paper will implement a relatively simple and flexible API security authentication service through AOP.

Let's look at the implementation first, then introduce and analyze the basic principles and common terms of AOP.

1. Definition notes

package com.power.demo.common;

import java.lang.annotation.*;

/*
 * Safety certification
 * */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Authorized {

    String value() default "";

}

This annotation looks like nothing but a placeholder to mark whether security authentication is required.

2. Presentation layer using annotations

@Authorized
    @RequestMapping(value = "/getinfobyid", method = RequestMethod.POST)
    @ApiOperation("By commodity Id Query product information")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", name = "authtoken", required = true, value = "authtoken", dataType =
                    "String"),
    })
    public GetGoodsByGoodsIdResponse getGoodsByGoodsId(@RequestHeader String authtoken, @RequestBody GetGoodsByGoodsIdRequest request) {

        return _goodsApiService.getGoodsByGoodsId(request);

    }

It seems that an Authorized annotation is added to a method. In fact, it can also be used on a class or mixed with a method.

3. Request authentication aspect

The following code is the key to flexible security authentication:

package com.power.demo.controller.tool;

import com.power.demo.common.AppConst;
import com.power.demo.common.Authorized;
import com.power.demo.common.BizResult;
import com.power.demo.service.contract.AuthTokenService;
import com.power.demo.util.PowerLogger;
import com.power.demo.util.SerializeUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;

/**
 * Request authentication facet to verify whether the authtoken of the custom request header is legal
 **/
@Aspect
@Component
public class AuthorizedAspect {

    @Autowired
    private AuthTokenService authTokenService;

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void requestMapping() {
    }

    @Pointcut("execution(* com.power.demo.controller.*Controller.*(..))")
    public void methodPointCut() {
    }

    /**
     * Request validity authentication injection Authorized annotation before execution of a method (first)
     */
    @Before("requestMapping() && methodPointCut()&&@annotation(authorized)")
    public void doBefore(JoinPoint joinPoint, Authorized authorized) throws Exception {

        PowerLogger.info("Start of method certification...");

        Class type = joinPoint.getSignature().getDeclaringType();

        Annotation[] annotations = type.getAnnotationsByType(Authorized.class);

        if (annotations != null && annotations.length > 0) {
            PowerLogger.info("Direct certification");
            return;
        }

        //Get current http request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String token = request.getHeader(AppConst.AUTH_TOKEN);

        BizResult<String> bizResult = authTokenService.powerCheck(token);

        System.out.println(SerializeUtil.Serialize(bizResult));

        if (bizResult.getIsOK() == true) {
            PowerLogger.info("Method certification passed");
        } else {
            throw new Exception(bizResult.getMessage());
        }
    }

    /**
     * All methods below the class require validation before execution (after)
     */
    @Before("requestMapping() && methodPointCut()")
    public void doBefore(JoinPoint joinPoint) throws Exception {

        PowerLogger.info("Class certification starts...");

        Annotation[] annotations = joinPoint.getSignature().getDeclaringType().getAnnotationsByType(Authorized.class);

        if (annotations == null || annotations.length == 0) {
            PowerLogger.info("Class does not require authentication");
            return;
        }

        //Get current http request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String token = request.getHeader(AppConst.AUTH_TOKEN);

        BizResult<String> bizResult = authTokenService.powerCheck(token);

        System.out.println(SerializeUtil.Serialize(bizResult));

        if (bizResult.getIsOK() == true) {
            PowerLogger.info("Class certification passed");
        } else {
            throw new Exception(bizResult.getMessage());
        }
    }

}

It should be noted that the overloaded processing method doBefore is defined for the Authorized processing on the class and method. The AuthTokenService is the same as the processing logic described above. If the security authentication fails, an exception will be thrown.

If we add the Authorized annotation on the class or method, we will not repeat the security authentication. Please feel free to use it.

4. Unified exception handling

As mentioned above, all API s with exceptions will return messages of uniform format to the caller. The main codes are as follows:

package com.power.demo.controller.exhandling;

import com.power.demo.common.ErrorInfo;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * Global unified exception handling enhancement
 **/
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * API Unified exception handling
     **/
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ErrorInfo<Exception> jsonApiErrorHandler(HttpServletRequest request, Exception e) {
        ErrorInfo<Exception> errorInfo = new ErrorInfo<>();
        try {
            System.out.println("Unified exception handling...");
            e.printStackTrace();

            Throwable innerEx = e.getCause();
            while (innerEx != null) {
                //innerEx.printStackTrace();
                if (innerEx.getCause() == null) {
                    break;
                }
                innerEx = innerEx.getCause();
            }

            if (innerEx == null) {
                errorInfo.setMessage(e.getMessage());
                errorInfo.setError(e.toString());
            } else {
                errorInfo.setMessage(innerEx.getMessage());
                errorInfo.setError(innerEx.toString());
            }

            errorInfo.setData(e);
            errorInfo.setTimestamp(new Date());
            errorInfo.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//500 error
            errorInfo.setUrl(request.getRequestURL().toString());
            errorInfo.setPath(request.getServletPath());

        } catch (Exception ex) {
            ex.printStackTrace();

            errorInfo.setMessage(ex.getMessage());
            errorInfo.setError(ex.toString());
        }

        return errorInfo;
    }

}

The API call results that fail the authentication are as follows:

The whole stack of exceptions can be very convenient to help us find out the problem.

According to the theoretical analysis and practice, we find that Filter is prior to Interceptor, prior to user-defined Authorized method authentication, prior to Authorized class authentication.

Here, we find that through AOP framework AspectJ, one @ Aspect annotation and several dozens of lines of business code, we can easily intercept the REST API.

So why does @ Pointcut exist? Since there is @ Before, is there @ After?

In fact, the above simple security authentication function mainly uses the AOP feature of Spring.

Next, I will briefly introduce the common concepts of AOP (mainly refer to the actual combat of Spring) to deepen our understanding. There are many concepts of AOP and it's boring. The experienced old man can ignore this section here.

2 AOP

1. Overview

AOP (Aspect Oriented Programming) can handle many things, such as logging, performance statistics, security control, transaction processing, exception handling, etc.

AOP can be regarded as a more advanced "reuse" technology, which is the complement and perfection of OOP (Object Oriented Programming). The concept of AOP is to extract the same code scattered in each business logic code into an independent module through horizontal cutting. The same logic of repeated code is extracted horizontally, and these repeated codes are woven into the target object method by using dynamic agent technology to achieve the same function as before. In this way, we only care about business code when we write business logic.

OOP introduces the concepts of encapsulation, inheritance and polymorphism to establish an object hierarchy, which is used to simulate a set of public behaviors. However, OOP allows developers to define vertical relationships, but it is not suitable to define horizontal relationships, such as logging. Log code is often distributed horizontally in all object levels, and has nothing to do with the core functions of its corresponding objects. This is also true for other types of code, such as security, exception handling and transparent persistence. This kind of irrelevant code scattered everywhere is called crosscutting Cutting), in OOP design, it leads to a lot of code repetition, which is not conducive to the reuse of each module.

AOP technology, on the contrary, uses a technology called "crosscutting" to dissect the inner part of packed objects, encapsulate the common behaviors that affect multiple classes into a reusable module, and name it "Aspect", that is, Aspect.

The so-called "aspect" is simply the logic or responsibility that has nothing to do with the business but is called by the business modules together. It is convenient to reduce the repeated code of the system, reduce the coupling degree between the modules, and is conducive to the operability and maintainability in the future.

Using "crosscutting" technology, AOP divides the software system into two parts: core concerns and crosscutting concerns.

The main process of business processing is the core concern, and the little related part is the crosscutting concern. One of the characteristics of crosscutting concerns is that they often occur in many places of core concerns, and they are basically similar everywhere, such as permission authentication, log, transaction. The role of AOP is to separate various concerns in the system and separate the core concerns from the crosscutting concerns.

2. AOP terminology


Target: target class, which needs to be proxied, such as UserService
Advice: Notice, the function to be enhanced or added, defines the "what" and "when" of the aspect. The modes include Before, After, After returning, After throwing and Around
Join Point: connection point, all "points" (timing) that can be inserted into the tangent during application execution
Pointcut: tangent point. In actual operation, select the connection point to insert the tangent plane, that is, define which points have been enhanced. The tangent defines where the tangent is. We usually use explicit class and method names, or use regular expressions to define matching class and method names to specify these pointcuts.
Aspect: facet, which modularizes cross cutting concerns into special classes called facets. Facets are a combination of notification and pointcuts. The notification and pointcut together define the full content of the pointcut: what it is, when and where to complete its functions
Introduction: introduction, which allows us to add new methods or properties to existing classes
Weaving: weaving, the process of applying the facet to the target object and creating a new proxy object. The facet is woven into the target object at the specified connection point. There are multiple points that can be woven into the target object's life cycle: compile time, class load time, and run time
The following reference from the online picture, you can more intuitively understand these AOP terms and flow process.

3. AOP implementation
(1) Dynamic agent can dynamically generate implementation objects for one or more interfaces during runtime by using dynamic agent. When implementing interface methods in the generated objects, enhanced code can be added to realize AOP:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Dynamic agent class
 */
public class DynamicProxy implements InvocationHandler {

    /**
     * Target class requiring proxy
     */
    private Object target;

    /**
     * Write method is fixed, aop is special: bind delegate object and return a proxy class
     *
     * @param target
     * @return
     */
    public Object bind(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    /**
     * Call InvocationHandler interface definition method
     *
     * @param proxy  Refers to the represented object.
     * @param method Method to call
     * @param args   Parameters required for method call
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        // Before cut
        System.out.println("[Dynamic agent]Before cut");

        // Conduct business
        result = method.invoke(target, args);

        // Perform after slice
        System.out.println("[Dynamic agent]Perform after cut");

        return result;
    }

}

The disadvantage is that only the interface can be proxied. At the same time, because the dynamic proxy is implemented by reflection, sometimes the overhead of reflection calls may be considered, otherwise it is easy to cause performance problems.

(2) Bytecode generation

Dynamic bytecode generation technology refers to the dynamic generation of a subclass object of a specified class (note that it is for the class) at runtime, and the coverage of a specific method. When the method is covered, enhanced code can be added to achieve AOP.
The most commonly used tool is CGLib:

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * Using cglib dynamic proxy
 * <p>
 * JDK When the dynamic proxy in is used, there must be a business interface, and cglib is for the class
 */
public class CglibProxy implements MethodInterceptor {

    private Object target;

    /**
     * Create proxy object
     *
     * @param target
     * @return
     */
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // Callback method
        enhancer.setCallback(this);
        // Create proxy object
        return enhancer.create();
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object result = null;
        System.out.println("[cglib]Before cut");

        result = methodProxy.invokeSuper(proxy, args);

        System.out.println("[cglib]Perform after cut");

        return result;
    }

}

(3) Custom classloader

When all objects of the class need to be enhanced, dynamic proxy and bytecode generation essentially need to construct proxy objects dynamically, that is, the ultimate enhanced objects are generated by the AOP framework, not by the developer new.

The solution is to implement a custom class loader and enhance it when a class is loaded.

JBoss implements AOP functions in this way.

This way is just hearsay at present, I have not practiced in the actual project.

(4) Code generation

Using the tool to generate new code based on the existing code, any crosscutting code can be added to realize AOP.

(5) Language extension

AspectJ is a common Java language extension to implement AOP in this way.

Comparison: according to the log, the execution order of the above processes is: filter, interceptor, AOP method authentication, AOP class authentication.

Attachment: record API log

Finally, by recording API logs and adding API time-consuming statistics when recording logs (in fact, AOP is the standard way to record logs during the development of. NET applications), we can deepen the understanding of several core concepts of AOP

package com.power.demo.controller.tool;

import com.power.demo.apientity.BaseApiRequest;
import com.power.demo.apientity.BaseApiResponse;
import com.power.demo.util.DateTimeUtil;
import com.power.demo.util.SerializeUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

/**
 * Service log, mainly recording interface log and time consumption
 **/
@Aspect
@Component
public class SvcLogAspect {

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void requestMapping() {
    }

    @Pointcut("execution(* com.power.demo.controller.*Controller.*(..))")
    public void methodPointCut() {
    }

    @Around("requestMapping() && methodPointCut()")
    public Object around(ProceedingJoinPoint pjd) throws Throwable {

        System.out.println("Spring AOP Record service log by");

        Object response = null;//Define return information

        BaseApiRequest baseApiRequest = null;//Request base class

        int index = 0;

        Signature curSignature = pjd.getSignature();

        String className = curSignature.getClass().getName();//Class name

        String methodName = curSignature.getName(); //Method name

        Logger logger = LoggerFactory.getLogger(className);//journal

        StopWatch watch = DateTimeUtil.StartNew();//Used to count call time

        // Get method parameters
        Object[] reqParamArr = pjd.getArgs();
        StringBuffer sb = new StringBuffer();
        //Get request parameter set and merge for traversal splicing
        for (Object reqParam : reqParamArr) {
            if (reqParam == null) {
                index++;
                continue;
            }
            try {
                sb.append(SerializeUtil.Serialize(reqParam));

                //Get request entity inherited from BaseApiRequest
                if (baseApiRequest == null && reqParam instanceof BaseApiRequest) {
                    index++;
                    baseApiRequest = (BaseApiRequest) reqParam;
                }

            } catch (Exception e) {
                sb.append(reqParam.toString());
            }
            sb.append(",");
        }

        String strParam = sb.toString();
        if (strParam.length() > 0) {
            strParam = strParam.substring(0, strParam.length() - 1);
        }

        //Record request
        logger.info(String.format("[%s]Class[%s]Method, request parameters:%s", className, methodName, strParam));

        response = pjd.proceed(); // Execute service method

        watch.stop();

        //Record response
        logger.info(String.format("[%s]Class[%s]Method, response parameters:%s", className, methodName, SerializeUtil.Serialize(response)));

        // Get execution completion time
        logger.info(String.format("Interface[%s]Total time(millisecond): %s", methodName, watch.getTotalTimeMillis()));

        //Standard request response model

        if (baseApiRequest == null) {

            return response;
        }

        if ((response != null && response instanceof BaseApiResponse) == false) {

            return response;
        }

        System.out.println("Spring AOP Method record standard request-Response model service log");

        Object request = reqParamArr[index];

        BaseApiResponse bizResp = (BaseApiResponse) response;
        //Log
        String msg = String.format("Request:%s======answer:%s======Total time(millisecond): %s", SerializeUtil.Serialize(request),
                SerializeUtil.Serialize(response), watch.getTotalTimeMillis());

        if (bizResp.getIsOK() == true) {
            logger.info(msg);
        } else {
            logger.error(msg);//Log errors
        }

        return response;
    }

}

In the standard request response model, we will define the request base class and response base class. The example in this article gives BaseApiRequest and BaseApiResponse. When collecting logs, you can distinguish special processing from error logs.

Notice the @ Around notification in the above code. The parameter type is ProceedingJoinPoint, while in the first example, @ Before pre notification, the parameter type is JoinPoint.

Here are five modes of AspectJ notification and enhancement:

@Before pre notification, implement the enhancement before the execution of the target method, request the parameter JoinPoint, which is used to connect the connection details of the current connection point, generally including the method name and parameter value. The method body is executed before the method execution, and the method parameters and execution results cannot be changed.
@After post notification requests the parameter JoinPoint. After the target method is executed, it will be notified whether there is an exception or not. In post notification, the execution result of the target method cannot be accessed (because of the possibility of an exception), and the execution result of the method cannot be changed.
@AfterReturning returns the notification, implements the enhancement after the target method is executed, and requests the parameter JoinPoint, which can access the method execution result (due to normal execution) and the method connection details, but cannot change the method execution result. (note the difference between post notice)
@AfterThrowing Exception notification is implemented after the method throws an Exception. The parameter joinpoint is requested. The throwing property represents the Exception thrown during the execution of the method body. Its value must be consistent with the value of Exception in the method.
@Around circular notification, request parameter ProceedingJoinPoint, circular notification is similar to the whole process of dynamic agent. Parameters of ProceedingJoinPoint type can determine whether to execute the target method, and circular notification must have a return value, which is the return value of the target method.

Reference source: https://www.cnblogs.com/jeffwongishandsome

Tags: Java Spring Programming REST

Posted on Fri, 26 Jun 2020 02:55:37 -0400 by NME