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

pull/1/head
YunaiV 2019-03-19 05:56:00 +08:00
parent bb612cac64
commit 532daf6299
13 changed files with 754 additions and 2 deletions

View File

@ -1,6 +1,7 @@
package cn.iocoder.mall.admin.application.config;
import cn.iocoder.common.framework.config.GlobalExceptionHandler;
import cn.iocoder.mall.admin.sdk.interceptor.AdminAccessLogInterceptor;
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@ -18,16 +19,17 @@ public class MVCConfiguration implements WebMvcConfigurer {
@Autowired
private AdminSecurityInterceptor adminSecurityInterceptor;
@Autowired
private AdminAccessLogInterceptor adminAccessLogInterceptor;
//
@Override
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口
registry.addInterceptor(adminAccessLogInterceptor).addPathPatterns("/admins/**");
registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**")
.excludePathPatterns("/admins/passport/login"); // 排除登陆接口
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 解决 swagger-ui.html 的访问,参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决

View File

@ -0,0 +1,78 @@
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();
}
}

View File

@ -39,6 +39,13 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
// 添加到 AdminSecurityContext
AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds());
AdminSecurityContextHolder.setContext(context);
// 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为:
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全
// AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号
// 因此,这里需要进行记录
if (authentication.getAdminId() != null) {
AdminAccessLogInterceptor.setAdminId(authentication.getAdminId());
}
} else {
String url = request.getRequestURI();
if (!url.equals("/admin/passport/login")) { // TODO 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问

View File

@ -0,0 +1,13 @@
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);
}

View File

@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO;
import java.util.Set;
/**
* Service
*/
public interface AdminService {
CommonResult<AdminPageBO> getAdminPage(AdminPageDTO adminPageDTO);

View File

@ -0,0 +1,132 @@
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;
}
}

View File

@ -0,0 +1,17 @@
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);
}

View File

@ -0,0 +1,11 @@
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);
}

View File

@ -0,0 +1,132 @@
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;
}
}

View File

@ -0,0 +1,56 @@
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);
}
}

View File

@ -0,0 +1,20 @@
<?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>

View File

@ -1,11 +1,49 @@
package cn.iocoder.common.framework.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Enumeration;
public class HttpUtil {
private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
/**
* Standard Servlet 2.3+ spec request attributes for include URI and paths.
* <p>If included via a RequestDispatcher, the current resource will see the
* originating request. Its own URI and paths are exposed as request attributes.
*/
public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
// public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
// public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
// public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";
//
// /**
// * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
// * <p>If forwarded to via a RequestDispatcher, the current resource will see its
// * own URI and paths. The originating URI and paths are exposed as request attributes.
// */
// public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
// public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
// public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
// public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
// public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
/**
* Default character encoding to use when <code>request.getCharacterEncoding</code>
* returns <code>null</code>, according to the Servlet spec.
*
* @see javax.servlet.ServletRequest#getCharacterEncoding
*/
public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
public static String obtainAccess(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (!StringUtils.hasText(authorization)) {
@ -39,4 +77,243 @@ public class HttpUtil {
return request.getRemoteAddr();
}
/**
* @param request
* @return ua
*/
public static String getUserAgent(HttpServletRequest request) {
String ua = request.getHeader("User-Agent");
return ua != null ? ua : "";
}
/**
* requestqueryString
*
* @return queryString
*/
@SuppressWarnings("unchecked")
public static String buildQueryString(HttpServletRequest request) {
Enumeration<String> es = request.getParameterNames();
if (!es.hasMoreElements()) {
return "";
}
String parameterName, parameterValue;
StringBuilder params = new StringBuilder();
while (es.hasMoreElements()) {
parameterName = es.nextElement();
parameterValue = request.getParameter(parameterName);
params.append(parameterName).append("=").append(parameterValue).append("&");
}
return params.deleteCharAt(params.length() - 1).toString();
}
/**
* Return the path within the web application for the given request.
* Detects include request URL if called within a RequestDispatcher include.
* <p/>
* For example, for a request to URL
* <p/>
* <code>http://www.somehost.com/myapp/my/url.jsp</code>,
* <p/>
* for an application deployed to <code>/mayapp</code> (the application's context path), this method would return
* <p/>
* <code>/my/url.jsp</code>.
*
* Shiro add by
*
* @param request current HTTP request
* @return the path within the web application
*/
public static String getPathWithinApplication(HttpServletRequest request) {
String contextPath = getContextPath(request);
String requestUri = getRequestUri(request);
if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
// Normal case: URI contains context path.
String path = requestUri.substring(contextPath.length());
return (StringUtils.hasText(path) ? path : "/");
} else {
// Special case: rather unusual.
return requestUri;
}
}
/**
* Return the request URI for the given request, detecting an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i>
* decoded by the servlet container, this method will decode it.
* <p>The URI that the web container resolves <i>should</i> be correct, but some
* containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
* in the URI. This method cuts off such incorrect appendices.
*
* @param request current HTTP request
* @return the request URI
*/
public static String getRequestUri(HttpServletRequest request) {
String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
if (uri == null) {
uri = request.getRequestURI();
}
return normalize(decodeAndCleanUriString(request, uri));
}
/**
* Normalize a relative URI path that may have relative values ("/./",
* "/../", and so on ) it it. <strong>WARNING</strong> - This method is
* useful only for normalizing application-generated paths. It does not
* try to perform security checks for malicious input.
* Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
* Tomcat trunk, r939305
*
* @param path Relative path to be normalized
* @return normalized path
*/
public static String normalize(String path) {
return normalize(path, true);
}
/**
* Normalize a relative URI path that may have relative values ("/./",
* "/../", and so on ) it it. <strong>WARNING</strong> - This method is
* useful only for normalizing application-generated paths. It does not
* try to perform security checks for malicious input.
* Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
* Tomcat trunk, r939305
*
* @param path Relative path to be normalized
* @param replaceBackSlash Should '\\' be replaced with '/'
* @return normalized path
*/
private static String normalize(String path, boolean replaceBackSlash) {
if (path == null)
return null;
// Create a place for the normalized path
String normalized = path;
if (replaceBackSlash && normalized.indexOf('\\') >= 0)
normalized = normalized.replace('\\', '/');
if (normalized.equals("/."))
return "/";
// Add a leading "/" if necessary
if (!normalized.startsWith("/"))
normalized = "/" + normalized;
// Resolve occurrences of "//" in the normalized path
while (true) {
int index = normalized.indexOf("//");
if (index < 0)
break;
normalized = normalized.substring(0, index) +
normalized.substring(index + 1);
}
// Resolve occurrences of "/./" in the normalized path
while (true) {
int index = normalized.indexOf("/./");
if (index < 0)
break;
normalized = normalized.substring(0, index) +
normalized.substring(index + 2);
}
// Resolve occurrences of "/../" in the normalized path
while (true) {
int index = normalized.indexOf("/../");
if (index < 0)
break;
if (index == 0)
return (null); // Trying to go outside our context
int index2 = normalized.lastIndexOf('/', index - 1);
normalized = normalized.substring(0, index2) +
normalized.substring(index + 3);
}
// Return the normalized path that we have completed
return (normalized);
}
/**
* Decode the supplied URI string and strips any extraneous portion after a ';'.
*
* @param request the incoming HttpServletRequest
* @param uri the application's URI string
* @return the supplied URI string stripped of any extraneous portion after a ';'.
*/
private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
uri = decodeRequestString(request, uri);
int semicolonIndex = uri.indexOf(';');
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
}
/**
* Return the context path for the given request, detecting an include request
* URL if called within a RequestDispatcher include.
* <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
* decoded by the servlet container, this method will decode it.
*
* @param request current HTTP request
* @return the context path
*/
public static String getContextPath(HttpServletRequest request) {
String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE);
if (contextPath == null) {
contextPath = request.getContextPath();
}
if ("/".equals(contextPath)) {
// Invalid case, but happens for includes on Jetty: silently adapt it.
contextPath = "";
}
return decodeRequestString(request, contextPath);
}
/**
* Decode the given source string with a URLDecoder. The encoding will be taken
* from the request, falling back to the default "ISO-8859-1".
* <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
*
* @param request current HTTP request
* @param source the String to decode
* @return the decoded String
* @see #DEFAULT_CHARACTER_ENCODING
* @see javax.servlet.ServletRequest#getCharacterEncoding
* @see java.net.URLDecoder#decode(String, String)
* @see java.net.URLDecoder#decode(String)
*/
@SuppressWarnings({"deprecation"})
public static String decodeRequestString(HttpServletRequest request, String source) {
String enc = determineEncoding(request);
try {
return URLDecoder.decode(source, enc);
} catch (UnsupportedEncodingException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
"': falling back to platform default encoding; exception message: " + ex.getMessage());
}
return URLDecoder.decode(source);
}
}
/**
* Determine the encoding for the given request.
* Can be overridden in subclasses.
* <p>The default implementation checks the request's
* {@link ServletRequest#getCharacterEncoding() character encoding}, and if that
* <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}.
*
* @param request current HTTP request
* @return the encoding for the request (never <code>null</code>)
* @see javax.servlet.ServletRequest#getCharacterEncoding()
*/
protected static String determineEncoding(HttpServletRequest request) {
String enc = request.getCharacterEncoding();
if (enc == null) {
enc = DEFAULT_CHARACTER_ENCODING;
}
return enc;
}
}

View File

@ -27,4 +27,8 @@ public class StringUtil {
return array;
}
public static String substring(String str, int start) {
return org.apache.commons.lang3.StringUtils.substring(str, start);
}
}