基于SpEL的SpringBoot含有动态参数的日志记录与管理的实现

一、功能介绍

在SpringBoot的项目应用中,常常需要记录日志到数据库中,例如系统访问日志(登录日志)、用户操作日志等,日志文本中需要含有动态的参数(发送http请求的请求参数),此情景下,可基于Spring Expression Language的动态日志信息记录功能。

二、需求场景

场景需求如下:

package com.tang.demo.controller;

import javax.validation.Valid;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.tang.demo.annotation.Log;
import com.tang.demo.domain.ResponseResult;

@RestController
public class TestController {

	@Log(name = "测试模块", description = "'添加一个用户,用户名为:'+#person.name")
	@RequestMapping(value = "/test", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
	public ResponseResult<Object> getModelNames(@RequestBody @Valid Person person, BindingResult bindResult) {
		if (bindResult.getErrorCount() > 0) {
			return ResponseResult.failed(-1, bindResult.getFieldError().getDefaultMessage());
		}

		return ResponseResult.success(person);
	}
}

动态拼接参数的日志最终会保存到数据库中,就像redis缓存中key的动态拼接,@Cacheable(value=“RptGroupAgent”,key="‘localAgentName’+#localAgentName") 。

接下来我们通过自定义注解来实现这个功能。

三、实现方法

我们可通过自定义注解、AOP切面、SpEL表达式等来实现,具体步骤如下:

1、定义注解@Log

package com.tang.demo.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    String  name() default "";

    String  description() default "";

}

2、AOP切面的处理

package com.tang.demo.aspect;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import com.tang.demo.annotation.Log;

@Aspect
@Component
public class LogAdvice {

	private final static Logger logger = LoggerFactory.getLogger(LogAdvice.class);

	private final static ExpressionParser spelParser = new SpelExpressionParser();

	@Pointcut("@annotation(com.tang.demo.annotation.Log)")
	public void methodAspect() {
	}

	@After("methodAspect()")
	public void after(JoinPoint joinPoint) throws IOException {
		logger.info("开始记录日志*************************");

		// 获取方法的参数名和参数值
		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
		List<String> paramNameList = Arrays.asList(methodSignature.getParameterNames());
		List<Object> paramList = Arrays.asList(joinPoint.getArgs());

		// 将方法的参数名和参数值一一对应的放入上下文中
		EvaluationContext ctx = new StandardEvaluationContext();
		for (int i = 0; i < paramNameList.size(); i++) {
			ctx.setVariable(paramNameList.get(i), paramList.get(i));
		}

		Method method = methodSignature.getMethod();
		Log myAnnotation = method.getAnnotation(Log.class);

		// 解析SpEL表达式获取结果
		String description = spelParser.parseExpression(myAnnotation.description()).getValue(ctx).toString();

		saveLog(myAnnotation.name(), description);
	}

	@AfterReturning(pointcut = "methodAspect()", returning = "returnValue")
	public void afterreturningJoinPoint(JoinPoint joinPoint, Object returnValue) {
	}

	@Around("methodAspect()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable {
		Object object = pjp.proceed();
		return object;
	}

	public static void saveLog(String name, String description) {
		logger.info(name + "----->" + description);
	}

}

3、SpEL表达式解析

在上述代码中:

Log myAnnotation = method.getAnnotation(Log.class);

// 解析SpEL表达式获取结果
String description = spelParser.parseExpression(myAnnotation.description()).getValue(ctx).toString();

的代码片段里对自定义主机@Log的description字段的文本内容进行EL表达式的解析处理,获取完整的字符串结果。

4、编写测试用的实体类

package com.tang.demo.domain;

import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Person{
	@NotNull(message = "id不能为空")
	private Integer id;
	
	@NotBlank(message = "name不能为空")
	private String name;
	
	@NotNull(message = "salary不能为空")
	private Float salary;
	
	@NotNull(message = "intrersts不能为空")
	private List<String> intrersts;
}

5、编写统一响应的实体类

package com.tang.demo.domain;

import java.io.Serializable;

public class ResponseResult<T> implements Serializable {

	private static final long serialVersionUID = -6964481356983562044L;
	public static final Integer SUCCESS_STATUS = 0;
	public static final String SUCCESS_MESSAGE = "success";

	private Integer status;
	private String message;
	private T data;

	public ResponseResult(String message, Integer code) {
		this.message = message;
		this.status = code;
	}

	public ResponseResult(String message, Integer code, T data) {
		this.message = message;
		this.status = code;
		this.data = data;
	}

	public Integer getStatus() {
		return status;
	}

	public void setStatus(Integer status) {
		this.status = status;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public T getData() {
		return data;
	}

	public void setData(T data) {
		this.data = data;
	}
	
	/

	public static <T>  ResponseResult<T> success() {
		return new ResponseResult<T>(SUCCESS_MESSAGE, SUCCESS_STATUS, null);
	}
	
	public static <T> ResponseResult<T> success(T data) {
		return new ResponseResult<T>(SUCCESS_MESSAGE, SUCCESS_STATUS, data);
	}

	public static <T> ResponseResult<T> failed(Integer errorCode, String message) {
		return new ResponseResult<T>(message, errorCode);
	}
}

6、编写测试用的Controller

package com.tang.demo.controller;

import javax.validation.Valid;

import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.tang.demo.annotation.Log;
import com.tang.demo.domain.Person;
import com.tang.demo.domain.ResponseResult;

@RestController
public class TestController {

	@Log(name = "测试模块", description = "'添加一个用户,用户名为:'+#person.name")
	@RequestMapping(value = "/test", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
	public ResponseResult<Object> getModelNames(@RequestBody @Valid Person person, BindingResult bindResult) {
		if (bindResult.getErrorCount() > 0) {
			return ResponseResult.failed(-1, bindResult.getFieldError().getDefaultMessage());
		}

		return ResponseResult.success(person);
	}
}

7、编写启动类

package com.tang.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoLearnApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoLearnApplication.class, args);
	}

}

8、启动程序并进行测试:

curl -X POST -H ‘Content-Type: application/json’ -d ‘{“id”:1,“name”:“hello”,“salary”:100.2,“intrersts”:[“song”,“football”]}’ ‘http://127.0.0.1:8080/test’


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.0)

2020-12-03 09:54:56.856  INFO 27248 --- [           main] com.tang.demo.DemoLearnApplication       : Starting DemoLearnApplication using Java 1.8.0_131 on DESKTOP-HSUNT2S with PID 27248 (D:\Projects\JavaProject\controller-logger\target\classes started by admin in D:\Projects\JavaProject\controller-logger)
2020-12-03 09:54:56.857  INFO 27248 --- [           main] com.tang.demo.DemoLearnApplication       : No active profile set, falling back to default profiles: default
2020-12-03 09:54:57.378  INFO 27248 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-12-03 09:54:57.389  INFO 27248 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-12-03 09:54:57.389  INFO 27248 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.39]
2020-12-03 09:54:57.421  INFO 27248 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-12-03 09:54:57.421  INFO 27248 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 542 ms
2020-12-03 09:54:57.543  INFO 27248 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-12-03 09:54:57.642  INFO 27248 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-12-03 09:54:57.648  INFO 27248 --- [           main] com.tang.demo.DemoLearnApplication       : Started DemoLearnApplication in 0.97 seconds (JVM running for 1.419)
2020-12-03 09:55:02.086  INFO 27248 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-12-03 09:55:02.087  INFO 27248 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-12-03 09:55:02.087  INFO 27248 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2020-12-03 09:55:02.171  INFO 27248 --- [nio-8080-exec-1] com.tang.demo.aspect.LogAdvice           : 开始记录日志*************************
2020-12-03 09:55:02.238  INFO 27248 --- [nio-8080-exec-1] com.tang.demo.aspect.LogAdvice           : 测试模块----->添加一个用户,用户名为:hello

四、完整代码

https://github.com/tangyibo/controller-logger

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringBoot SpEL表达式注入漏洞是一种常见的安全漏洞,攻击者可以利用该漏洞在应用中执行恶意代码,从而导致应用被攻击。下面我将对该漏洞进行分析与复现。 一、漏洞分析 SpringBoot中的SpEL(Spring Expression Language)是一种基于表达式的语言,用于在运行时动态地计算值或执行逻辑。SpEL表达式可以用于访问对象的属性、调用对象的方法、进行条件判断等操作。在SpringBoot中,SpEL表达式可以用于注解中的属性值、配置文件中的属性值等场景。 在SpEL表达式中,可以使用一些特殊的语法来引用Bean对象或调用Bean对象的方法。例如,可以使用`#{beanName.methodName()}`的语法来调用Bean对象的方法。如果在SpEL表达式中使用了未经过滤的用户输入,就会存在SpEL表达式注入漏洞。 攻击者可以通过构造恶意的SpEL表达式,将其注入到应用中,从而执行恶意代码。例如,可以将`#{T(java.lang.Runtime).getRuntime().exec('calc')}`注入到应用中,从而在受害者机器上执行计算器程序。 二、漏洞复现 下面我将通过一个简单的漏洞复现来说明SpEL表达式注入漏洞的危害性。 1. 创建一个SpringBoot应用 首先,我们需要创建一个SpringBoot应用。可以使用Spring Initializr来快速创建一个基本的SpringBoot应用。 2. 添加注解 在创建好的SpringBoot应用中,我们可以添加一个Controller类,并在其中添加一个RequestMapping注解。在RequestMapping注解中,我们可以使用SpEL表达式来引用Bean对象或调用Bean对象的方法。 ```java @RestController public class MyController { @RequestMapping("/test") public String test() { return "hello world"; } @RequestMapping(value = "/{name}", method = RequestMethod.GET) public String sayHello(@PathVariable("name") String name) { return "Hello " + name + "!"; } @RequestMapping(value = "/spel", method = RequestMethod.GET) public String spel() { String expression = "#{T(java.lang.Runtime).getRuntime().exec('calc')}"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(expression); return exp.getValue().toString(); } } ``` 在上述代码中,我们在/spel接口中使用了SpEL表达式来调用Runtime.getRuntime().exec方法,从而执行计算器程序。 3. 启动应用 启动应用后,访问/spel接口,可以看到计算器程序被成功执行。 4. 防范措施 为了防范SpEL表达式注入漏洞,我们可以采取以下措施: 1. 对用户输入进行过滤和验证,避免未经过滤的用户输入被注入到SpEL表达式中。 2. 尽量避免在注解或配置文件中使用SpEL表达式。 3. 对于必须使用SpEL表达式的场景,可以对SpEL表达式进行白名单过滤,只允许特定的SpEL表达式被执行。 4. 在应用中禁用动态表达式功能,避免SpEL表达式被执行。 5. 总结 SpEL表达式注入漏洞是一种常见的安全漏洞,攻击者可以利用该漏洞在应用中执行恶意代码。为了防范该漏洞,我们需要对用户输入进行过滤和验证,避免未经过滤的用户输入被注入到SpEL表达式中。同时,尽量避免在注解或配置文件中使用SpEL表达式,对于必须使用SpEL表达式的场景,可以对SpEL表达式进行白名单过滤,只允许特定的SpEL表达式被执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值