JavaSpring

一、简介

任何一个软件系统,都不可避免的会碰到【信息安全】这个词,尤其是对于刚入行的新手。
实际上,随着工作阅历的增加,越来越能感觉到,实现业务方提的需求,只是完成了软件系统研发中的【能用】要求;服务是否【可靠】可能需要从架构层和运维方面去着手解决;至于是否【安全】、更多的需要从【信息安全】这个角度来思考,尤其是当软件系统面对外界的恶意干扰和攻击时,是否依然能保障用户正常使用,对于大公司,这个可能是头等大事,因为可能一个很小很小的漏洞,一不小心可能会给公司带来几千万的损失!
最常见的就是电商系统和支付系统,尤其是需求旺季的时候,经常有黑客专门攻击这些电商系统,导致大量服务宕机,影响用户正常下单。
像这样的攻击案例每天都有,有的公司甚至直接向黑客气妥,给钱消灾!
但是这种做法肯定不是长久之计,最重要的还是主动提升系统的【安全】防御系数。
由于信息安全所涉及的要求内容众多,这里介绍其中关于【审计日志】的要求和具体应用。
【审计日志】,简单的说就是系统需要记录谁,在什么时间,对什么数据,做了什么样的更改!这个日志数据是极其珍贵的,后面如果因业务操作上出了问题,可以很方便进行操作回查。
同时,任何一个 IT 系统,如果要过审,这项任务基本上也是必审项!

二、实践

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

创建审计日志表

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

编写一个注解类

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target({ElementType.TYPE,ElementType.METHOD})
  3. @Documented
  4. public @interface SystemAuditLog {
  5. /**
  6. * 操作了的表名
  7. * @return
  8. */
  9. String tableName() default "";
  10. /**
  11. * 日志描述
  12. * @return
  13. */
  14. String description() default "";
  15. }

编写一个代理类

  1. @Component
  2. @Aspect
  3. public class SystemAuditLogAspect {
  4. @Autowired
  5. private SystemAuditLogService systemAuditLogService;
  6. /**
  7. * 定义切入点,切入所有标注此注解的类和方法
  8. */
  9. @Pointcut("@within(com.example.demo.core.annotation.SystemAuditLog)|| @annotation(com.example.demo.core.annotation.SystemAuditLog)")
  10. public void methodAspect() {
  11. }
  12. /**
  13. * 方法调用前拦截
  14. */
  15. @Before("methodAspect()")
  16. public void before(){
  17. System.out.println("SystemAuditLog代理 -> 调用方法执行之前......");
  18. }
  19. /**
  20. * 方法调用后拦截
  21. */
  22. @After("methodAspect()")
  23. public void after(){
  24. System.out.println("SystemAuditLog代理 -> 调用方法执行之后......");
  25. }
  26. /**
  27. * 调用方法结束拦截
  28. */
  29. @AfterReturning(value = "methodAspect()")
  30. public void afterReturning(JoinPoint joinPoint) throws Exception {
  31. System.out.println("SystemAuditLog代理 -> 调用方法结束拦截......");
  32. //封装数据
  33. AuditLog entity = warpAuditLog(joinPoint);
  34. entity.setResult(0);
  35. //插入到数据库
  36. systemAuditLogService.add(entity);
  37. }
  38. /**
  39. * 抛出异常拦截
  40. */
  41. @AfterThrowing(value="methodAspect()", throwing="ex")
  42. public void afterThrowing(JoinPoint joinPoint, Exception ex) throws Exception {
  43. System.out.println("SystemAuditLog代理 -> 抛出异常拦截......");
  44. //封装数据
  45. AuditLog entity = warpAuditLog(joinPoint);
  46. entity.setResult(1);
  47. //封装错误信息
  48. entity.setExMsg(ex.getMessage());
  49. //插入到数据库
  50. systemAuditLogService.add(entity);
  51. }
  52. /**
  53. * 封装插入实体
  54. * @param joinPoint
  55. * @return
  56. * @throws Exception
  57. */
  58. private AuditLog warpAuditLog(JoinPoint joinPoint) throws Exception {
  59. //获取请求上下文
  60. HttpServletRequest request = getHttpServletRequest();
  61. //获取注解上的参数值
  62. SystemAuditLog systemAuditLog = getServiceMethodDescription(joinPoint);
  63. //获取请求参数
  64. Object requestObj = getServiceMethodParams(joinPoint);
  65. //封装数据
  66. AuditLog auditLog = new AuditLog();
  67. auditLog.setId(SnowflakeIdWorker.getInstance().nextId());
  68. //从请求上下文对象获取相应的数据
  69. if(Objects.nonNull(request)){
  70. auditLog.setUserAgent(request.getHeader("User-Agent"));
  71. //获取登录时的ip地址
  72. auditLog.setIpAddress(IpAddressUtil.getIpAddress(request));
  73. //调用外部接口,获取IP所在地
  74. auditLog.setIpAddressName(IpAddressUtil.getLoginAddress(auditLog.getIpAddress()));
  75. }
  76. //封装操作的表和描述
  77. if(Objects.nonNull(systemAuditLog)){
  78. auditLog.setTableName(systemAuditLog.tableName());
  79. auditLog.setOperateDesc(systemAuditLog.description());
  80. }
  81. //封装请求参数
  82. auditLog.setRequestParam(JSON.toJSONString(requestObj));
  83. //封装请求人
  84. if(Objects.nonNull(requestObj) && requestObj instanceof BaseRequest){
  85. auditLog.setOperateUserId(((BaseRequest) requestObj).getLoginUserId());
  86. auditLog.setOperateUserName(((BaseRequest) requestObj).getLoginUserName());
  87. }
  88. auditLog.setOperateTime(new Date());
  89. return auditLog;
  90. }
  91. /**
  92. * 获取当前的request
  93. * 这里如果报空指针异常是因为单独使用spring获取request
  94. * 需要在配置文件里添加监听
  95. *
  96. * 如果是spring项目,通过下面方式注入
  97. * <listener>
  98. * <listener-class>
  99. * org.springframework.web.context.request.RequestContextListener
  100. * </listener-class>
  101. * </listener>
  102. *
  103. * 如果是springboot项目,在配置类里面,通过下面方式注入
  104. * @Bean
  105. * public RequestContextListener requestContextListener(){
  106. * return new RequestContextListener();
  107. * }
  108. * @return
  109. */
  110. private HttpServletRequest getHttpServletRequest(){
  111. RequestAttributes ra = RequestContextHolder.getRequestAttributes();
  112. ServletRequestAttributes sra = (ServletRequestAttributes)ra;
  113. HttpServletRequest request = sra.getRequest();
  114. return request;
  115. }
  116. /**
  117. * 获取请求对象
  118. * @param joinPoint
  119. * @return
  120. * @throws Exception
  121. */
  122. private Object getServiceMethodParams(JoinPoint joinPoint) {
  123. Object[] arguments = joinPoint.getArgs();
  124. if(Objects.nonNull(arguments) && arguments.length > 0){
  125. return arguments[0];
  126. }
  127. return null;
  128. }
  129. /**
  130. * 获取自定义注解里的参数
  131. * @param joinPoint
  132. * @return 返回注解里面的日志描述
  133. * @throws Exception
  134. */
  135. private SystemAuditLog getServiceMethodDescription(JoinPoint joinPoint) throws Exception {
  136. //类名
  137. String targetName = joinPoint.getTarget().getClass().getName();
  138. //方法名
  139. String methodName = joinPoint.getSignature().getName();
  140. //参数
  141. Object[] arguments = joinPoint.getArgs();
  142. //通过反射获取示例对象
  143. Class targetClass = Class.forName(targetName);
  144. //通过实例对象方法数组
  145. Method[] methods = targetClass.getMethods();
  146. for(Method method : methods) {
  147. //判断方法名是不是一样
  148. if(method.getName().equals(methodName)) {
  149. //对比参数数组的长度
  150. Class[] clazzs = method.getParameterTypes();
  151. if(clazzs.length == arguments.length) {
  152. //获取注解里的日志信息
  153. return method.getAnnotation(SystemAuditLog.class);
  154. }
  155. }
  156. }
  157. return null;
  158. }
  159. }

在对应的接口或者方法上添加审计日志注解

  1. @RestController
  2. @RequestMapping("api")
  3. public class LoginController {
  4. /**
  5. * 用户登录,添加审计日志注解
  6. * @param request
  7. */
  8. @SystemAuditLog(tableName = "tb_user", description = "用户登录")
  9. @PostMapping("login")
  10. public void login(UserLoginDTO request){
  11. //登录逻辑处理
  12. }
  13. }

相关的实体类

  1. @Data
  2. public class AuditLog {
  3. /**
  4. * 审计日志,主键ID
  5. */
  6. private Long id;
  7. /**
  8. * 操作的表名,多个用逗号隔开
  9. */
  10. private String tableName;
  11. /**
  12. * 操作描述
  13. */
  14. private String operateDesc;
  15. /**
  16. * 请求参数
  17. */
  18. private String requestParam;
  19. /**
  20. * 执行结果,0:成功,1:失败
  21. */
  22. private Integer result;
  23. /**
  24. * 异常信息
  25. */
  26. private String exMsg;
  27. /**
  28. * 请求代理信息
  29. */
  30. private String userAgent;
  31. /**
  32. * 操作时设备IP
  33. */
  34. private String ipAddress;
  35. /**
  36. * 操作时设备IP所在地址
  37. */
  38. private String ipAddressName;
  39. /**
  40. * 操作时间
  41. */
  42. private Date operateTime;
  43. /**
  44. * 操作人ID
  45. */
  46. private String operateUserId;
  47. /**
  48. * 操作人
  49. */
  50. private String operateUserName;
  51. }
  1. public class BaseRequest implements Serializable {
  2. /**
  3. * 请求token
  4. */
  5. private String token;
  6. /**
  7. * 登录人ID
  8. */
  9. private String loginUserId;
  10. /**
  11. * 登录人姓名
  12. */
  13. private String loginUserName;
  14. public String getToken() {
  15. return token;
  16. }
  17. public void setToken(String token) {
  18. this.token = token;
  19. }
  20. public String getLoginUserId() {
  21. return loginUserId;
  22. }
  23. public void setLoginUserId(String loginUserId) {
  24. this.loginUserId = loginUserId;
  25. }
  26. public String getLoginUserName() {
  27. return loginUserName;
  28. }
  29. public void setLoginUserName(String loginUserName) {
  30. this.loginUserName = loginUserName;
  31. }
  32. }
  1. @Data
  2. public class UserLoginDTO extends BaseRequest {
  3. /**
  4. * 用户名
  5. */
  6. private String userName;
  7. /**
  8. * 密码
  9. */
  10. private String password;
  11. }

三、小结

整个程序的实现过程,主要使用了 Spring AOP 特性,对特定方法进行前、后拦截,从而实现业务方的需求。