Record of user behavior log in SSO microservice Engineering

Posted by alcapone on Tue, 07 Dec 2021 11:52:55 +0100

System requirements analysis

Business description

When the user accesses our resource data in the SSO resource project, the user's behavior log information is obtained, and then passed to the SSO system project to store the log information in the database

Business architecture analysis

Design of log storage in system service  

In this design, the system service is responsible for writing the user behavior logs obtained by other services to the database.

Pojo logic implementation

Define a Log object to store user behavior Log information in memory, for example:

package com.jt.system.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * Encapsulate user behavior logs based on this object?
 * Who performed what operation at what time, accessed what method, passed what parameters, and how long the access was
 */
@Data
@TableName("tb_logs")
public class Log implements Serializable {
    private static final long serialVersionUID = 3054471551801044482L;
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String operation;
    private String method;
    private String params;
    private Long time;
    private String ip;
    @TableField("createdTime")
    private Date createdTime;
    private Integer status;
    private String error;
}

Dao logic implementation

Step 1: create a user behavior log data layer object to handle data persistence logic, such as

package com.jt.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.Log;
import org.apache.ibatis.annotations.Mapper;

/**
 * User behavior log data layer object
 */
@Mapper
public interface LogMapper extends BaseMapper<Log> {
}

Step 2: define unit test and unit test the data layer method

package com.jt;

import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;

@SpringBootTest
public class LogMapperTests {
    @Autowired
    private LogMapper logMapper;
    @Test
    void testInsert(){
        //Build a user behavior log object (store some user behavior logs based on this object, and use false data first)
        Log log=new Log();
        log.setUsername("cgb2107");
        log.setIp("192.168.100.200");
        log.setOperation("Query resources");
        log.setMethod("pkg.ResourceController.doSelect");
        log.setParams("");
        log.setStatus(1);
        log.setTime(100L);
        log.setCreatedTime(new Date());
        //Persist logs to database
        logMapper.insert(log);
    }
}

Service logic implementation

Step 1: define the log business interface, for example:

package com.jt.system.service;
import com.jt.system.pojo.Log;
/**
 * User behavior log business logic interface definition
 */
public interface LogService {
    /**
     * Save user behavior log
     * @param log
     */
    void insertLog(Log log);
    //.....
}

Step 2: define the log business interface implementation class, for example:

package com.jt.system.service.impl;

import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogMapper logMapper;
    /**
     * @Async The method described will be executed asynchronously (not by the web service thread),
     * It is to be executed by the threads in the spring's own thread pool) but the @ Async annotation
     * There is a prerequisite for the application. Asynchronous execution needs to be started on the startup class (add @ EnableAsync annotation description)
     * Advantage: web Service (such as tomcat) threads will not be blocked for a long time
     */
    @Async
    @Override
    public void insertLog(Log log) {
        logMapper.insert(log);
    }
}

Controller logic implementation

Step 1: define the LogController object to implement log control logic, for example:

package com.jt.system.controller;

import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/log")
public class LogController {
    @Autowired
    private LogService logService;
    @PostMapping
    public void doInsertLog(@RequestBody Log log){
        logService.insertLog(log);
    }
}

Add to the startup class

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync          //Start asynchronous operation
@SpringBootApplication
public class SystemApplication {
    public static void main(String[] args) {
        SpringApplication.run(SystemApplication.class,args);
    }

}

Step 2: start the service and conduct access test based on postman, for example:

Design of behavior log operation in resource service  

Business description

Without modifying the code implementation of the target business method, the user behavior log is obtained when accessing the target method

Pojo logical object definition

Define a log object to encapsulate the obtained user behavior log, for example:

package com.jt.resource.pojo;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * Encapsulate user behavior logs based on this object?
 * Who performed what operation at what time, accessed what method, passed what parameters, and how long the access was
 */
@Data
public class Log implements Serializable {
    private static final long serialVersionUID = 3054471551801044482L;
    private Long id;
    private String username;
    private String operation;
    private String method;
    private String params;
    private Long time;
    private String ip;
    private Date createdTime;
    private Integer status;
    private String error;
}

Pointcut annotation definition

Build a user-defined annotation named RequiredLog, and use this annotation description as the entry point to define the entry point method. For example:

package com.jt.resource.annotation;

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

/**
 * Define the RequiredLog annotation, and use this annotation to
 * The method of logging is described
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
    String value() default "";
}

Obtain and record logs in AOP mode

Define a log aspect, and realize the acquisition and recording of user behavior log based on the notification method in this aspect

package com.jt.resource.aspect;

import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Date;

/**
 * @Aspect The type of annotation description is a section type, which can be defined in this class:
 * 1)The entry point (the location where the extension logic is entered ~ for example, the location of permission control, logging, and transaction processing) is
 * @Aspect The classes described are usually defined with @ Pointcut annotation. The methods described with pointcuts are Pointcut methods
 *
 * 2)Notification method (the logic to be executed before and after the execution of the target method corresponding to the pointcut needs to be written to such a method), in
 * @Aspect In the class described by, @ before, @ after, @ around, @ afterreturning, @ afterthrowing
 * Such annotations are described
 * a: @Before Execution before pointcut method execution
 * b: @After Execute after the pointcut method is executed (it will execute whether the pointcut method is executed successfully or not)
 * c: @Aroud Pointcut methods can be executed before and after execution (most importantly)
 * d: @AfterReturning The pointcut method executes after successful execution
 * e: @AfterThrowing An exception occurred during the execution of the pointcut method
 */
@Aspect
@Component
public class LogAspect {
    /**
     * @Pointcut Annotation is used to define the pointcut. The content in this annotation is the pointcut expression
     * @annotation It is a pointcut expression in annotation mode. The expression in this mode is a fine-grained pointcut expression,
     * Because it can be accurate to the method, for example, when we use the RequiredLog annotation to describe the method, it describes the method
     * This is an entry point method
     */
    @Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
    public void doLog(){
        //There is no need to write anything in this method, which is only responsible for carrying the @ Pointcut annotation
    }

    /**
     * @Around The method described by the annotation is a surround notification method in Aspect
     * Internal control can be used to call the target method
     * @param joinPoint Connection point object, which encapsulates the pointcut method information you want to execute, and can be based on
     *           This object makes reflection calls to pointcut methods
     * @return The return value of the pointcut method in the target execution chain
     * @throws Throwable
     */
    @Around("doLog()")
    public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
        int status=1;//state
        String error=null;//error message
        long time=0l;//Execution duration
        long t1=System.currentTimeMillis();
        try {
            //Manually call the target execution chain (this execution chain contains pointcut methods to target methods)
           Object result = joinPoint.proceed();
           long t2=System.currentTimeMillis();
           time=t2-t1;
           return result;
        }catch (Throwable e){
           long t3=System.currentTimeMillis();
           time=t3-t1;
           status=0;
           error=e.getMessage();
           throw e;
        }finally {
           saveLog(joinPoint,time,status,error);
        }
    }
    //Store user behavior logs
    private void saveLog(ProceedingJoinPoint joinPoint,long time,
                         int status,String error)throws Throwable{
        //1. Get user behavior log
        //1.1 get the target object type (the type of the class where the pointcut method is located)
        Class<?> targetClass = joinPoint.getTarget().getClass();
        //1.2. Method of obtaining target
        //1.2.1 obtain method signature (including method information,...)
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        //1.2.2 get method object
        Method targetMethod=
                targetClass.getDeclaredMethod(signature.getName(),signature.getParameterTypes());
        //1.3 get the RequiredLog annotation content on the method
        //1.3.1 obtain annotation on target method
        RequiredLog requiredLog=targetMethod.getAnnotation(RequiredLog.class);
        //1.3.2 get the content in the annotation (this content is the operation name defined by us)
        String operation=requiredLog.value();
        //1.4 get the target method name (class name + method name)
        String targetMethodName=targetClass.getName()+"."+targetMethod.getName();
        //1.5 get the parameters passed in during the execution of the target method
        String params=new ObjectMapper().writeValueAsString(joinPoint.getArgs());
        //1.6 obtain the login user name (refer to the official Security code)
        String username=(String)
                SecurityContextHolder.getContext()
                        .getAuthentication()
                        .getPrincipal();
        //1.7 obtain the ip address (obtain the request object from the current thread, and then obtain the ip address based on the request)
        //String ip="192.168.1.100";
        ServletRequestAttributes requestAttributes =
        (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String ip=requestAttributes.getRequest().getRemoteAddr();
        //2. Encapsulate the user behavior Log into the Log object
        Log logInfo=new Log();
        logInfo.setIp(ip);//Subsequent acquisition
        logInfo.setUsername(username);
        logInfo.setOperation(operation);
        logInfo.setMethod(targetMethodName);
        logInfo.setParams(params);
        logInfo.setTime(time);
        logInfo.setStatus(status);
        logInfo.setError(error);
        logInfo.setCreatedTime(new Date());
        System.out.println("logInfo="+logInfo);
    }

}

Start the service for access testing

Start the engineering services of Nacos, SSO system, SSO auth, SSO resource, SSO gateway and SSO UI in turn, and then execute the login. After the login is successful, query my resources and detect the log output.

Application principle analysis of AOP technology in service

AOP is a design idea. Its function is "icing on the cake". It is to add some extended functions, such as log recording, permission control, transaction control, asynchronous task execution, etc. on the basis of trying not to modify the original target method. Its application principle is shown in the figure:

Note: after defining the AOP Aspect in the project, when the system starts, it will load and analyze the classes described by the @ Aspect annotation, create a proxy object based on the description of the pointcut as the target type object, and create an execution chain inside the proxy object, which contains interceptors (encapsulating the pointcut information), notifications (Around,...), When we request the target object resources, we will call the resources directly in the order of the execution chain


Pass logs to system services in Feign mode

Step 1: ensure that the openfeign dependency is added to the SSO resource project, for example:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Step 2: ensure that the @ EnableFeignClients annotation is added to the start of the SSO resource project, for example:

package com.jt;

@EnableFeignClients
@SpringBootApplication
public class ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

Step 3: define the log remote service call interface, for example:

package com.jt.resource.service;
import com.jt.resource.pojo.Log;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value = "sso-system",contextId = "remoteLogService")
public interface RemoteLogService {
     @PostMapping("/log")
     public void insertLog(@RequestBody Log log);
}

Step 4: inject the RemoteLogService object into the LogAspect class, and pass the log object to the SSO system service through this object

@Autowired
private RemoteLogService remoteLogService;
....

Step 5: start the service in turn for access test.

Topics: Microservices