使用Spring MVC拦截器自定义注解实现审计日志收集

× 文章目录
  1. 1. 场景
    1. 1.1. 需求
    2. 1.2. 实现思路
  2. 2. 实现细节
    1. 2.1. 方法拦截器HandlerInterceptor
    2. 2.2. 自定义注解
    3. 2.3. Controller使用示例
    4. 2.4. AuditLogHandlerInterceptor主要代码示例
  3. 3. 扩展自定义注解实现权限控制
    1. 3.1. 自定义注解
    2. 3.2. 在方法上配置权限
    3. 3.3. 权限逻辑

前言 Spring WebMvc框架中的Interceptor,与Servlet API中的Filter十分类似,用于对Web请求进行预处理/后处理。通常情况下这些预处理/后处理逻辑是通用的,可以被应用于所有或多个Web请求,例如:

  • 记录Web请求相关日志,可以用于做一些信息监控、统计、分析
  • 检查Web请求访问权限,例如发现用户没有登录后,重定向到登录页面
  • 打开/关闭数据库连接——预处理时打开,后处理关闭,可以避免在所有业务方法中都编写类似代码,也不会忘记关闭数据库连接

场景

需求

使用手动埋点,收集用户审计日志,包括操作人,对应的Url,操作的模块,调用的方法(url),方法的描述。

实现思路

手动在每个Controller方法中,逐一埋点审计日志,这样的方式比较low,而且无法做到组件式通用。因此,采用Spring MVC的HandlerInterceptor(拦截器)+自定义注解实现。

实现细节

方法拦截器HandlerInterceptor

在HandlerInterceptor中有三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
public interface HandlerInterceptor {
// 在执行目标方法之前执行
boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler)throws Exception;
// 执行目标方法之后执行
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception;
// 在请求已经返回之后执行
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception;
}

在以上注释中已经写明执行顺序:

  • preHandle():预处理回调方法,若方法返回值为true,请求继续(调用下一个拦截器或处理器方法);若方法返回值为false,请求处理流程中断,不会继续调用其他的拦截器或处理器方法,此时需要通过response产生响应;
  • postHandle():后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时可以通过modelAndView对模型数据进行处理或对视图进行处理
  • afterCompletion():整个请求处理完毕回调方法,即在视图渲染完毕时调用
1
2
3
4
5
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println(handler.getClass());
return true;
}

所有请求都是从DispatcherServlet来调用请求url对应的方法的,因此我们可以获取到URL对应的Controller方法。

自定义注解

定义一个@interface类,AuditLog注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
String url() default "";
String module() default "";
String operation() default "";
String description() default "";
String objType() default "";
}

@Target注解是标注这个类它可以标注的位置,常用的元素类型(ElementType)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
// TYPE类型可以声明在类上或枚举上或者是注解上
TYPE,
/** Field declaration (includes enum constants) */
// FIELD声明在字段上
FIELD,
/** Method declaration */
// 声明在方法上
METHOD,
/** Formal parameter declaration */
// 声明在形参列表中
PARAMETER,
/** Constructor declaration */
// 声明在构造方法上
CONSTRUCTOR,
/** Local variable declaration */
// 声明在局部变量上
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}

@Retention注解表示的是本注解(标注这个注解的注解保留时期)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
// 源代码时期
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
// 字节码时期, 编译之后
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
// 运行时期, 也就是一直保留, 通常也都用这个
RUNTIME
}

@Documented是否生成文档的标注, 也就是生成接口文档是, 是否生成注解文档。

Controller使用示例

1
2
3
4
5
6
7
@ResponseBody
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
@AuditLog(module = AuditLogModule.CHANNEL, operation = AuditLogOperate.DELETE, description = "deleteBuild",
objType = AuditLogObjType.CHANNELBUILD)
public ResponseEntity deleteBuild(HttpServletRequest request, @PathVariable(value = "id") long id) {
//其它代码省略
}

AuditLogHandlerInterceptor主要代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
LOGGER.error("auditlog create");
Method method = handlerMethod.getMethod();
AuditLog auditLog = method.getAnnotation(AuditLog.class);
if (auditLog == null) {
return;
}
try {
AuditLogInfo auditLogInfo = createAuditLog(auditLog, request, response);
if (auditLogInfo != null) {
AuditLogHandler.createAuditLog(auditLogInfo);
}
} catch (Exception e) {
LOGGER.error("auditlog create error", e);
}
}
}

扩展自定义注解实现权限控制

自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Access {
String[] value() default {};
String[] authorities() default {};
String[] roles() default {};
}

在方法上配置权限

1
2
3
4
5
6
7
8
9
10
@RestController
public class HelloController {
@RequestMapping(value = "/admin", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET)
// 配置注解权限, 允许身份为admin, 或者说允许权限为admin的人访问
@Access(authorities = {"admin"})
public String hello() {
return "Hello, admin";
}
}

权限逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 自定义一个权限拦截器, 继承HandlerInterceptorAdapter类
public class AuthenticationInterceptor extends HandlerInterceptorAdapter {
// 在调用方法之前执行拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 将handler强转为HandlerMethod, 前面已经证实这个handler就是HandlerMethod
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 从方法处理器中获取出要调用的方法
Method method = handlerMethod.getMethod();
// 获取出方法上的Access注解
Access access = method.getAnnotation(Access.class);
if (access == null) {
// 如果注解为null, 说明不需要拦截, 直接放过
return true;
}
if (access.authorities().length > 0) {
// 如果权限配置不为空, 则取出配置值
String[] authorities = access.authorities();
Set<String> authSet = new HashSet<>();
for (String authority : authorities) {
// 将权限加入一个set集合中
authSet.add(authority);
}
String role = request.getParameter("role");
if (StringUtils.isNotBlank(role)) {
if (authSet.contains(role)) {
// 校验通过返回true, 否则拦截请求
return true;
}
}
}
// 拦截之后应该返回公共结果, 这里没做处理
return false;
}
}
如果您觉得文章不错,可以打赏我喝一杯咖啡!