近期需求中需要做一个系统日志,需求为记录到每次操作类型,操作的模块,IP,操作者昵称,操作者用户名,日志描述,操作时间等信息
首先,设计一下数据表
image.png
为了获取用户信息,写了一个拦截器,拦截jwt_token获取用户信息
向线程变量中封装了用户名、昵称、模块名(从配置文件注入)、IP地址四个字段
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
/**
* 用户信息拦截器
* 从Header中取出jwttoken,并获取其中的用户名设置到用户信息上下文线程变量中
*
* @author wangqichang
* @since 2019/4/24
*/
@Component
public class CustomInfoInterceptor implements HandlerInterceptor {
private ObjectMapper objectMapper = new ObjectMapper();
@Value("${spring.application.chineseName}")
private String moduleName;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authorization = request.getHeader("Authorization");
authorization = StrUtil.removePrefix(authorization, "Bearer ");
ContextInfo contextInfo = new ContextInfo();
if (StrUtil.isNotBlank(authorization)) {
//验签
Jwt decode = JwtHelper.decodeAndVerify(authorization, new MacSigner("secretKey"));
String claims = decode.getClaims();
HashMap<String, Object> hashMap = objectMapper.readValue(claims, HashMap.class);
Object userName = hashMap.get("user_name");
Object nickName = hashMap.get("nick_name");
contextInfo.setNickName((String) nickName);
contextInfo.setUserName((String) userName);
} /*else {
throw new ServiceException("当前用户未登录");
}*/
contextInfo.setModuleName(moduleName);
contextInfo.setIp(getIp());
CustomContext.setContextInfo(contextInfo);
return true;
}
/**
* @return 返回当前请求IP
*/
public static String getIp() {
if (null != RequestContextHolder.currentRequestAttributes()) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
if (null != attr) {
HttpServletRequest request = attr.getRequest();
String header = request.getHeader("x-forwarded-for");
String ip = null;
if (StrUtil.isNotBlank(header) && !"unknown".equalsIgnoreCase(header)) {
ip = header.split(",")[0];
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
}
}
return null;
}
}
mybatis拦截器如下
封装日志对象,主要为获取操作类型(增删改查)、操作表实体
package com.zh.linemanager.interceptor;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import com.zh.backend.common.context.CustomContext;
import com.zh.backend.common.dto.ContextInfo;
import com.zh.backend.common.dto.LogDto;
import com.zh.backend.common.interceptor.CustomInfoInterceptor;
import com.zh.backend.common.service.LogService;
import com.zh.backend.common.util.SqlTableUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
/**
* 日志mybatis 拦截器 功能为拦截数据库操作,获取操作类型及操作表格,并从CustomContext获取线程变量中的数据封装日志
* 比较粗糙,下次优化
* Signature 对Executor 的 update 及query 方法进行拦截,后面为参数
*
* @author wangqichang
* @see CustomInfoInterceptor
* @since 2019/5/20
*/
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class LogInterceptor implements Interceptor {
private static String LOG_TYPE = "操作日志";
/**
* 这个是日志服务,使用feign 调用
*/
@Autowired
private LogService logService;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//仅做日志,不能影响正常业务,故try住
try {
//获取第一个参数,就是Signature中args的下标
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
LogDto logDto = new LogDto();
//从自定义线程中copy出日志所需的信息,我放了用户名,ip,模块名这些
ContextInfo info = CustomContext.current();
BeanUtil.copyProperties(info, logDto);
Date date = new Date();
//获取第二个参数Object,这个对象可能是实体也可能是map,比如自己用@Param注入参数,那就是map对象
Object object = invocation.getArgs()[1];
if (!(object instanceof Map)) {
//直接记录操作的实体
logDto.setOperationEntity(object.getClass().getName());
} else {
//如果是注入的map参数,则使用jsqlphaser提取表名
String sql = mappedStatement.getBoundSql(object).getSql();
List<String> tableNames = SqlTableUtil.getTableNames(sql);
String tables = CollUtil.join(tableNames, " ");
logDto.setOperationEntity(tables);
}
String dateTime = DateUtil.formatDateTime(date);
String operationType = null;
switch (mappedStatement.getSqlCommandType()) {
case INSERT:
operationType = "新增";
break;
case UPDATE:
operationType = "更新";
break;
case DELETE:
operationType = "删除";
break;
case SELECT:
operationType = "查询";
break;
default:
operationType = "未知";
break;
}
StringBuilder logDesc = new StringBuilder().append("用户 ").append(info.getNickName()).append(" 于 ").append(dateTime).append(" 对 ").append(logDto.getModuleName()).append(" 模块进行了 ").append(operationType).append(" 操作");
logDto.setOperationType(operationType);
logDto.setCreateTime(date);
logDto.setLogType(LOG_TYPE);
logDto.setLogDesc(logDesc.toString());
logService.recordLog(logDto);
} catch (Exception e) {
log.error("日志记录出错,错误信息:{}", e.getMessage());
e.printStackTrace();
}
//放行拦截
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
提取sql表名用到的工具类
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TablesNamesFinder;
/**
* @author wangqichang
* @since 2019/5/21
*/
public class SqlTableUtil {
private static CCJSqlParserManager sqlParserManager = new CCJSqlParserManager();
private static TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
/**
* detect table names from given table
* ATTENTION : WE WILL SKIP SCALAR SUBQUERY IN PROJECTION CLAUSE
*/
public static List<String> getTableNames(String sql) throws Exception {
Statement statement = sqlParserManager.parse(new StringReader(sql));
return tablesNamesFinder.getTableList(statement);
}
}
成功记录如下
image.png
代码不易,勿忘点赞
网友评论