提交 532daf62 authored 作者: YunaiV's avatar YunaiV

后端:增加管理员访问日志

上级 bb612cac
package cn.iocoder.mall.admin.application.config; package cn.iocoder.mall.admin.application.config;
import cn.iocoder.common.framework.config.GlobalExceptionHandler; import cn.iocoder.common.framework.config.GlobalExceptionHandler;
import cn.iocoder.mall.admin.sdk.interceptor.AdminAccessLogInterceptor;
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor; import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -18,16 +19,17 @@ public class MVCConfiguration implements WebMvcConfigurer { ...@@ -18,16 +19,17 @@ public class MVCConfiguration implements WebMvcConfigurer {
@Autowired @Autowired
private AdminSecurityInterceptor adminSecurityInterceptor; private AdminSecurityInterceptor adminSecurityInterceptor;
@Autowired
private AdminAccessLogInterceptor adminAccessLogInterceptor;
// //
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口 // registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口
registry.addInterceptor(adminAccessLogInterceptor).addPathPatterns("/admins/**");
registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**") registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**")
.excludePathPatterns("/admins/passport/login"); // 排除登陆接口 .excludePathPatterns("/admins/passport/login"); // 排除登陆接口
} }
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 解决 swagger-ui.html 的访问,参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决 // 解决 swagger-ui.html 的访问,参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决
......
package cn.iocoder.mall.admin.sdk.interceptor;
import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.mall.admin.api.AdminAccessLogService;
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
/**
* 访问日志拦截器
*/
@Component
public class AdminAccessLogInterceptor extends HandlerInterceptorAdapter {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 开始时间
*/
private static final ThreadLocal<Date> START_TIME = new ThreadLocal<>();
/**
* 管理员编号
*/
private static final ThreadLocal<Integer> ADMIN_ID = new ThreadLocal<>();
@Reference
private AdminAccessLogService adminAccessLogService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 记录当前时间
START_TIME.set(new Date());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
AdminAccessLogAddDTO accessLog = new AdminAccessLogAddDTO();
try {
accessLog.setAdminId(ADMIN_ID.get());
if (accessLog.getAdminId() == null) {
accessLog.setAdminId(AdminAccessLogAddDTO.ADMIN_ID_NULL);
}
accessLog.setUri(request.getRequestURI()); // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
accessLog.setQueryString(HttpUtil.buildQueryString(request));
accessLog.setMethod(request.getMethod());
accessLog.setUserAgent(HttpUtil.getUserAgent(request));
accessLog.setIp(HttpUtil.getIp(request));
accessLog.setStartTime(START_TIME.get());
accessLog.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime()));// 默认响应时间设为0
adminAccessLogService.addAdminAccessLog(accessLog);
// TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
} catch (Throwable th) {
logger.error("[afterCompletion][插入管理员访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
} finally {
clear();
}
}
public static void setAdminId(Integer adminId) {
ADMIN_ID.set(adminId);
}
public static void clear() {
START_TIME.remove();
ADMIN_ID.remove();
}
}
...@@ -39,6 +39,13 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter { ...@@ -39,6 +39,13 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
// 添加到 AdminSecurityContext // 添加到 AdminSecurityContext
AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds()); AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds());
AdminSecurityContextHolder.setContext(context); AdminSecurityContextHolder.setContext(context);
// 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为:
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全
// AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号
// 因此,这里需要进行记录
if (authentication.getAdminId() != null) {
AdminAccessLogInterceptor.setAdminId(authentication.getAdminId());
}
} else { } else {
String url = request.getRequestURI(); String url = request.getRequestURI();
if (!url.equals("/admin/passport/login")) { // TODO 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问 if (!url.equals("/admin/passport/login")) { // TODO 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问
......
package cn.iocoder.mall.admin.api;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
/**
* 管理员访问日志 Service 接口
*/
public interface AdminAccessLogService {
CommonResult<Boolean> addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO);
}
...@@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO; ...@@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO;
import java.util.Set; import java.util.Set;
/**
* 管理员 Service 接口
*/
public interface AdminService { public interface AdminService {
CommonResult<AdminPageBO> getAdminPage(AdminPageDTO adminPageDTO); CommonResult<AdminPageBO> getAdminPage(AdminPageDTO adminPageDTO);
......
package cn.iocoder.mall.admin.api.dto;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* 管理员访问日志添加 DTO
*/
public class AdminAccessLogAddDTO {
/**
* 管理员编号 - 空
*/
public static final Integer ADMIN_ID_NULL = 0;
/**
* 管理员编号.
*
* 当管理员为空时,该值为0
*/
@NotNull(message = "管理员编号不能为空")
private Integer adminId;
/**
* 访问地址
*/
@NotNull(message = "访问地址不能为空")
private String uri;
/**
* 参数
*/
@NotNull(message = "请求参数不能为空")
private String queryString;
/**
* http 方法
*/
@NotNull(message = "http 请求方法不能为空")
private String method;
/**
* User Agent
*/
@NotNull(message = "User-Agent 不能为空")
private String userAgent;
/**
* ip
*/
@NotNull(message = "ip 不能为空")
private String ip;
/**
* 请求时间
*/
@NotNull(message = "请求时间不能为空")
private Date startTime;
/**
* 响应时长 -- 毫秒级
*/
@NotNull(message = "响应时长不能为空")
private Integer responseTime;
public Integer getAdminId() {
return adminId;
}
public AdminAccessLogAddDTO setAdminId(Integer adminId) {
this.adminId = adminId;
return this;
}
public String getUri() {
return uri;
}
public AdminAccessLogAddDTO setUri(String uri) {
this.uri = uri;
return this;
}
public String getQueryString() {
return queryString;
}
public AdminAccessLogAddDTO setQueryString(String queryString) {
this.queryString = queryString;
return this;
}
public String getMethod() {
return method;
}
public AdminAccessLogAddDTO setMethod(String method) {
this.method = method;
return this;
}
public String getUserAgent() {
return userAgent;
}
public AdminAccessLogAddDTO setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public String getIp() {
return ip;
}
public AdminAccessLogAddDTO setIp(String ip) {
this.ip = ip;
return this;
}
public Date getStartTime() {
return startTime;
}
public AdminAccessLogAddDTO setStartTime(Date startTime) {
this.startTime = startTime;
return this;
}
public Integer getResponseTime() {
return responseTime;
}
public AdminAccessLogAddDTO setResponseTime(Integer responseTime) {
this.responseTime = responseTime;
return this;
}
}
\ No newline at end of file
package cn.iocoder.mall.admin.convert;
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AdminAccessLogConvert {
AdminAccessLogConvert INSTANCE = Mappers.getMapper(AdminAccessLogConvert.class);
@Mappings({})
AdminAccessLogDO convert(AdminAccessLogAddDTO adminAccessLogAddDTO);
}
\ No newline at end of file
package cn.iocoder.mall.admin.dao;
import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO;
import org.springframework.stereotype.Repository;
@Repository
public interface AdminAccessLogMapper {
void insert(AdminAccessLogDO entity);
}
package cn.iocoder.mall.admin.dataobject;
import cn.iocoder.common.framework.dataobject.BaseDO;
import java.util.Date;
/**
* 管理员访问日志 DO
*/
public class AdminAccessLogDO extends BaseDO {
/**
* 编号
*/
private Integer id;
/**
* 管理员编号.
*
* 当管理员为空时,该值为0
*/
private Integer adminId;
/**
* 访问地址
*/
private String uri;
/**
* 参数
*/
private String queryString;
/**
* http 方法
*/
private String method;
/**
* userAgent
*/
private String userAgent;
/**
* ip
*/
private String ip;
/**
* 请求时间
*/
private Date startTime;
/**
* 响应时长 -- 毫秒级
*/
private Integer responseTime;
public Integer getId() {
return id;
}
public AdminAccessLogDO setId(Integer id) {
this.id = id;
return this;
}
public Integer getAdminId() {
return adminId;
}
public AdminAccessLogDO setAdminId(Integer adminId) {
this.adminId = adminId;
return this;
}
public String getUri() {
return uri;
}
public AdminAccessLogDO setUri(String uri) {
this.uri = uri;
return this;
}
public String getQueryString() {
return queryString;
}
public AdminAccessLogDO setQueryString(String queryString) {
this.queryString = queryString;
return this;
}
public String getMethod() {
return method;
}
public AdminAccessLogDO setMethod(String method) {
this.method = method;
return this;
}
public String getUserAgent() {
return userAgent;
}
public AdminAccessLogDO setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public String getIp() {
return ip;
}
public AdminAccessLogDO setIp(String ip) {
this.ip = ip;
return this;
}
public Date getStartTime() {
return startTime;
}
public AdminAccessLogDO setStartTime(Date startTime) {
this.startTime = startTime;
return this;
}
public Integer getResponseTime() {
return responseTime;
}
public AdminAccessLogDO setResponseTime(Integer responseTime) {
this.responseTime = responseTime;
return this;
}
}
\ No newline at end of file
package cn.iocoder.mall.admin.service;
import cn.iocoder.common.framework.util.StringUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.admin.api.AdminAccessLogService;
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
import cn.iocoder.mall.admin.convert.AdminAccessLogConvert;
import cn.iocoder.mall.admin.dao.AdminAccessLogMapper;
import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
public class AdminAccessLogServiceImpl implements AdminAccessLogService {
/**
* 请求参数最大长度。
*/
private static final Integer QUERY_STRING_MAX_LENGTH = 4096;
/**
* 请求地址最大长度。
*/
private static final Integer URI_MAX_LENGTH = 4096;
/**
* User-Agent 最大长度。
*/
private static final Integer USER_AGENT_MAX_LENGTH = 1024;
@Autowired
private AdminAccessLogMapper adminAccessLogMapper;
@Override
public CommonResult<Boolean> addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO) {
// 创建 AdminAccessLogDO
AdminAccessLogDO accessLog = AdminAccessLogConvert.INSTANCE.convert(adminAccessLogAddDTO);
accessLog.setCreateTime(new Date());
// 截取最大长度
if (accessLog.getUri().length() > URI_MAX_LENGTH) {
accessLog.setUri(StringUtil.substring(accessLog.getUri(), URI_MAX_LENGTH));
}
if (accessLog.getQueryString().length() > QUERY_STRING_MAX_LENGTH) {
accessLog.setQueryString(StringUtil.substring(accessLog.getQueryString(), QUERY_STRING_MAX_LENGTH));
}
if (accessLog.getUserAgent().length() > USER_AGENT_MAX_LENGTH) {
accessLog.setUserAgent(StringUtil.substring(accessLog.getUserAgent(), USER_AGENT_MAX_LENGTH));
}
// 插入
adminAccessLogMapper.insert(accessLog);
// 返回成功
return CommonResult.success(true);
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.mall.admin.dao.AdminAccessLogMapper">
<!--<sql id="FIELDS">-->
<!--id, username, nickname, password, status,-->
<!--create_time-->
<!--</sql>-->
<insert id="insert" parameterType="AdminAccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO admin_access_log (
admin_id, uri, query_string, method, user_agent,
ip, start_time, response_time, create_time
) VALUES (
#{adminId}, #{uri}, #{queryString}, #{method}, #{userAgent},
#{ip}, #{startTime}, #{responseTime}, #{createTime}
)
</insert>
</mapper>
\ No newline at end of file
...@@ -27,4 +27,8 @@ public class StringUtil { ...@@ -27,4 +27,8 @@ public class StringUtil {
return array; return array;
} }
public static String substring(String str, int start) {
return org.apache.commons.lang3.StringUtils.substring(str, start);
}
} }
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论