SpringBoot + AOP 解决审计日志记录

1、背景介绍

软件系统开发中,信息安全至关重要,特别是对于新手开发者。随着经验的积累,开发者应从满足基本业务需求的"能用",进一步关注系统的"可靠性"和"安全性"。大型公司尤其重视信息安全,因为小漏洞可能造成巨大损失。

电商和支付系统在需求旺季易受黑客攻击,导致服务中断,影响用户体验。一些公司甚至支付赎金以求息事宁人,但这并非长久之计。关键还是要提升系统的安全防御能力。

信息安全包含多个方面,这里重点介绍审计日志的重要性。审计日志记录了谁、在什么时间、对什么数据、进行了哪些操作,对于事后问题追踪和系统审查至关重要。任何IT系统在审核时,审计日志都是必审项。

简而言之,信息安全是软件开发中不可忽视的重要方面,审计日志作为其中的关键组成部分,对于保障系统的安全性和可追溯性具有重要作用。开发者应提高安全意识,从多个维度加强系统的安全防护。

2、方案实践

实现【审计日志】这个需求,我们有一个很好的技术解决方案,就是使用 Spring 的切面编程,创建一个代理类,利用afterReturningafterThrowing方法来实现日志的记录。

具体实现步骤如下:

创建完springboot项目之后加一些依赖:

<dependency>
   <groupId>org.json</groupId>
   <artifactId>json</artifactId>
   <version>20220320</version> <!-- 请使用最新的版本号 -->
</dependency>

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

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2.1 创建审计日志表

CREATE TABLE `tb_audit_log` (
  `id` bigint(20) NOT NULL  COMMENT '审计日志,主键ID',
  `table_name` varchar(500) DEFAULT '' COMMENT '操作的表名,多个用逗号隔开',
  `operate_desc` varchar(200) DEFAULT '' COMMENT '操作描述',
  `request_param` varchar(200) DEFAULT '' COMMENT '请求参数',
  `result` int(10) COMMENT '执行结果,0:成功,1:失败',
  `ex_msg` varchar(200) DEFAULT '' COMMENT '异常信息',
  `user_agent` text COLLATE utf8mb4_unicode_ci COMMENT '用户代理信息',
  `ip_address` varchar(32) NOT NULL DEFAULT '' COMMENT '操作时设备IP',
  `ip_address_name` varchar(32) DEFAULT '' COMMENT '操作时设备IP所在地址',
  `operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
  `operate_user_id` varchar(32) DEFAULT '' COMMENT '操作人ID',
  `operate_user_name` varchar(32) DEFAULT '' COMMENT '操作人',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审计日志表';

2.2 编写一个注解类

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
public @interface SystemAuditLog {

    /**
     * 操作了的表名
     * @return
     */
    String tableName() default "";

    /**
     * 日志描述
     * @return
     */
    String description() default "";

}

2.3 编写获取ip地址以及所在地工具类 

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.HttpURLConnection;
import org.json.JSONObject;

public class IpAddressUtil {

    /**
     * 从HttpServletRequest中获取IP地址,处理了X-Forwarded-For和Proxy-Client-IP
     *
     * @param request HttpServletRequest对象
     * @return IP地址字符串
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 根据IP地址查询所在地,使用第三方服务ip-api.com
     *
     * @param ipAddress IP地址字符串
     * @return IP所在地信息的JSON字符串
     */
    public static String getLoginAddress(String ipAddress) {
        String url = "http://ip-api.com/json/" + ipAddress;
        StringBuilder response = new StringBuilder();
        try {
            URL urlObj = new URL(url);
            HttpURLConnection con = (HttpURLConnection) urlObj.openConnection();
            con.setRequestMethod("GET");
            BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
            String inputLine;

            while ((inputLine = in.readLine()) != null) {
                response.append(inputLine);
            }
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return response.toString();
    }

    /**
     * 从第三方服务的响应中提取IP所在地信息
     *
     * @param ipApiResponse 来自ip-api.com的响应字符串
     * @return 格式化的IP所在地信息
     */
    public static String formatLocation(String ipApiResponse) {
        JSONObject jsonObject = new JSONObject(ipApiResponse);
        String city = jsonObject.optString("city", "未知城市");
        String regionName = jsonObject.optString("regionName", "未知地区");
        String country = jsonObject.optString("country", "未知国家");
        return "IP所在地: " + country + ", " + regionName + ", " + city;
    }

    // 测试IpAddressUtil工具类
    public static void main(String[] args) {
//         模拟HttpServletRequest,实际使用时将由Servlet容器提供
//         HttpServletRequest request = ...;
//         String ipAddress = getIpAddress(request);
//         String locationJson = getLoginAddress(ipAddress);
//         String location = formatLocation(locationJson);
//         System.out.println("IP地址: " + ipAddress);
//         System.out.println("IP所在地: " + location);
    }
}

2.4 编写一个代理类

import com.alibaba.fastjson.JSON;
import com.xiaobai.easyexceldemo.annotation.SystemAuditLog;
import com.xiaobai.easyexceldemo.domain.AuditLog;
import com.xiaobai.easyexceldemo.domain.BaseRequest;
import com.xiaobai.easyexceldemo.service.AuditLogService;
import com.xiaobai.easyexceldemo.utils.IpAddressUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
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 javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;

/**
 * @author: xiaobai
 * @Desc:
 * @create: 2024-08-28
 **/
@Component
@Aspect
public class SystemAuditLogAspect {

    @Autowired
    private AuditLogService systemAuditLogService;

    /**
     * 定义切入点,切入所有标注此注解的类和方法
     */
    @Pointcut("@within(com.xiaobai.easyexceldemo.annotation.SystemAuditLog)||@annotation(com.xiaobai.easyexceldemo.annotation.SystemAuditLog)")
    public void methodAspect() {
    }

    /**
     * 方法调用前拦截
     */
    @Before("methodAspect()")
    public void before() {
        System.out.println("SystemAuditLog代理    ->    调用方法执行之前......");
    }

    /**
     * 方法调用后拦截
     */
    @After("methodAspect()")
    public void after() {
        System.out.println("SystemAuditLog代理    ->    调用方法执行之后......");
    }

    /**
     * 调用方法结束拦截
     */
    @AfterReturning(value = "methodAspect()")
    public void afterReturning(JoinPoint joinPoint) throws Exception {
        System.out.println("SystemAuditLog代理    ->    调用方法结束拦截......");
        //封装数据
        AuditLog entity = warpAuditLog(joinPoint);
        entity.setResult(0);

        //插入到数据库
        systemAuditLogService.save(entity);
    }


    /**
     * 抛出异常拦截
     */
    @AfterThrowing(value = "methodAspect()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) throws Exception {
        System.out.println("SystemAuditLog代理    ->    抛出异常拦截......");
        //封装数据
        AuditLog entity = warpAuditLog(joinPoint);
        entity.setResult(1);
        //封装错误信息
        entity.setExMsg(ex.getMessage());

        //插入到数据库
        systemAuditLogService.save(entity);
    }


    /**
     * 封装插入实体
     *
     * @param joinPoint
     * @return
     * @throws Exception
     */
    private AuditLog warpAuditLog(JoinPoint joinPoint) throws Exception {
        //获取请求上下文
        HttpServletRequest request = getHttpServletRequest();
        //获取注解上的参数值
        SystemAuditLog systemAuditLog = getServiceMethodDescription(joinPoint);
        //获取请求参数
        Object requestObj = getServiceMethodParams(joinPoint);
        //封装数据
        AuditLog auditLog = new AuditLog();
        //从请求上下文对象获取相应的数据
        auditLog.setUserAgent(request.getHeader("User-Agent"));
        //获取登录时的ip地址
        auditLog.setIpAddress(IpAddressUtil.getIpAddress(request));
        //调用外部接口,获取IP所在地
        auditLog.setIpAddressName(IpAddressUtil.formatLocation(IpAddressUtil.getLoginAddress(auditLog.getIpAddress())));
        //封装操作的表和描述
        if (Objects.nonNull(systemAuditLog)) {
            auditLog.setTableName(systemAuditLog.tableName());
            auditLog.setOperateDesc(systemAuditLog.description());
        }
        //封装请求参数
        auditLog.setRequestParam(JSON.toJSONString(requestObj));
        //封装请求人
        if (Objects.nonNull(requestObj) && requestObj instanceof BaseRequest) {
            auditLog.setOperateUserId(((BaseRequest) requestObj).getLoginUserId());
            auditLog.setOperateUserName(((BaseRequest) requestObj).getLoginUserName());
        }
        auditLog.setOperateTime(new Date());
        return auditLog;
    }


    /**
     * 获取当前的request
     * 这里如果报空指针异常是因为单独使用spring获取request
     * 需要在配置文件里添加监听
     * <p>
     * 如果是spring项目,通过下面方式注入
     * <listener>
     * <listener-class>
     * org.springframework.web.context.request.RequestContextListener
     * </listener-class>
     * </listener>
     * <p>
     * 如果是springboot项目,在配置类里面,通过下面方式注入
     *
     * @return
     * @Bean public    RequestContextListener    requestContextListener(){
     * return    new    RequestContextListener();
     * }
     */
    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        return sra.getRequest();
    }

    /**
     * 获取请求对象
     *
     * @param joinPoint
     * @return
     * @throws Exception
     */
    private Object getServiceMethodParams(JoinPoint joinPoint) {
        Object[] arguments = joinPoint.getArgs();
        if (Objects.nonNull(arguments) && arguments.length > 0) {
            return arguments[0];
        }
        return null;
    }


    /**
     * 获取自定义注解里的参数
     *
     * @param joinPoint
     * @return 返回注解里面的日志描述
     * @throws Exception
     */
    private SystemAuditLog getServiceMethodDescription(JoinPoint joinPoint) throws Exception {
        //类名
        String targetName = joinPoint.getTarget().getClass().getName();
        //方法名
        String methodName = joinPoint.getSignature().getName();
        //参数
        Object[] arguments = joinPoint.getArgs();
        //通过反射获取示例对象
        Class targetClass = Class.forName(targetName);
        //通过实例对象方法数组
        Method[] methods = targetClass.getMethods();
        for (Method method : methods) {
            //判断方法名是不是一样
            if (method.getName().equals(methodName)) {
                //对比参数数组的长度
                Class[] clazzs = method.getParameterTypes();
                if (clazzs.length == arguments.length) {
                    //获取注解里的日志信息
                    return method.getAnnotation(SystemAuditLog.class);
                }
            }
        }
        return null;
    }
}

2.5 最后只需要加注解

@RestController
@RequestMapping("api")
public class LoginController {

    /**
     * 用户登录,添加审计日志注解
     * @param request
     */
    @SystemAuditLog(tableName = "tb_user", description = "用户登录")
    @PostMapping("login")
    public void login(UserLoginDTO request){
        //登录逻辑处理
    }
}

备注:相关的实体类

@Data
public class AuditLog {

    /**
     * 审计日志,主键ID
     */
    private Long id;

    /**
     * 操作的表名,多个用逗号隔开
     */
    private String tableName;

    /**
     * 操作描述
     */
    private String operateDesc;

    /**
     * 请求参数
     */
    private String requestParam;

    /**
     * 执行结果,0:成功,1:失败
     */
    private Integer result;

    /**
     * 异常信息
     */
    private String exMsg;

    /**
     * 请求代理信息
     */
    private String userAgent;

    /**
     * 操作时设备IP
     */
    private String ipAddress;

    /**
     * 操作时设备IP所在地址
     */
    private String ipAddressName;

    /**
     * 操作时间
     */
    private Date operateTime;


    /**
     * 操作人ID
     */
    private String operateUserId;

    /**
     * 操作人
     */
    private String operateUserName;
}
public class BaseRequest implements Serializable {

    /**
     * 请求token
     */
    private String token;

    /**
     * 登录人ID
     */
    private String loginUserId;

    /**
     * 登录人姓名
     */
    private String loginUserName;

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getLoginUserId() {
        return loginUserId;
    }

    public void setLoginUserId(String loginUserId) {
        this.loginUserId = loginUserId;
    }

    public String getLoginUserName() {
        return loginUserName;
    }

    public void setLoginUserName(String loginUserName) {
        this.loginUserName = loginUserName;
    }
}
@Data
public class UserLoginDTO extends BaseRequest {

    /**
     * 用户名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;
}

至此,你就完成了审计日志的核心代码编写,可以测试一下你的成果,调用一下登录的api,然后去数据库,会有你想要的结果,接下来就是根据自己的业务去修改你的代码,记录想要记录的日志。若有更好的方案,麻烦评论区告知一下,我去学习一番,共勉!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot中使用AOP自定义权限注解可以通过以下步骤实现: 1. 首先,在pom.xml文件中添加Spring Boot AOP的依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` \[1\] 2. 创建一个自定义的注解,用于标记需要进行权限控制的方法或类。例如,可以创建一个名为@CustomPermission的注解。 3. 创建一个切面类,用于定义权限控制的逻辑。在切面类中,可以使用@Before、@After、@Around等注解来定义在方法执行前、执行后或者环绕方法执行时需要执行的逻辑。在这个切面类中,可以通过获取方法的参数、注解等信息来进行权限校验和控制。 4. 在Spring Boot的配置类中,使用@EnableAspectJAutoProxy注解来启用AOP功能。 5. 在需要进行权限控制的方法或类上,添加自定义的权限注解@CustomPermission。 通过以上步骤,就可以在Spring Boot中使用AOP自定义权限注解来实现权限控制了。使用AOP可以更加灵活地对方法进行拦截和处理,同时可以通过自定义注解来标记需要进行权限控制的方法或类。\[2\]\[3\] #### 引用[.reference_title] - *1* [springboot+mybatis+aop+注解实现数据权限](https://blog.csdn.net/weixin_42935902/article/details/116758260)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [springboot+自定义注解+AOP实现权限控制(一)](https://blog.csdn.net/byteArr/article/details/103984725)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许舒雅的宝贝

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值