diff --git a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java index 19806637b..802debaa8 100644 --- a/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java +++ b/admin/admin-application/src/main/java/cn/iocoder/mall/admin/application/config/MVCConfiguration.java @@ -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 解决 diff --git a/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminAccessLogInterceptor.java b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminAccessLogInterceptor.java new file mode 100644 index 000000000..7cd08dda8 --- /dev/null +++ b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminAccessLogInterceptor.java @@ -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 START_TIME = new ThreadLocal<>(); + /** + * 管理员编号 + */ + private static final ThreadLocal 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(); + } + +} diff --git a/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java index bfc580613..876071167 100644 --- a/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java +++ b/admin/admin-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java @@ -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 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问 diff --git a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminAccessLogService.java b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminAccessLogService.java new file mode 100644 index 000000000..0b22c1819 --- /dev/null +++ b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminAccessLogService.java @@ -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 addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO); + +} diff --git a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java index 78e5e6752..76903ffb6 100644 --- a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java +++ b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/AdminService.java @@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO; import java.util.Set; +/** + * 管理员 Service 接口 + */ public interface AdminService { CommonResult getAdminPage(AdminPageDTO adminPageDTO); diff --git a/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/dto/AdminAccessLogAddDTO.java b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/dto/AdminAccessLogAddDTO.java new file mode 100644 index 000000000..fe259c718 --- /dev/null +++ b/admin/admin-service-api/src/main/java/cn/iocoder/mall/admin/api/dto/AdminAccessLogAddDTO.java @@ -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; + } + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/convert/AdminAccessLogConvert.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/convert/AdminAccessLogConvert.java new file mode 100644 index 000000000..a7816e6c7 --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/convert/AdminAccessLogConvert.java @@ -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); + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dao/AdminAccessLogMapper.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dao/AdminAccessLogMapper.java new file mode 100644 index 000000000..58cd8f525 --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dao/AdminAccessLogMapper.java @@ -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); + +} diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dataobject/AdminAccessLogDO.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dataobject/AdminAccessLogDO.java new file mode 100644 index 000000000..93221788c --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/dataobject/AdminAccessLogDO.java @@ -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; + } + +} \ No newline at end of file diff --git a/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/AdminAccessLogServiceImpl.java b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/AdminAccessLogServiceImpl.java new file mode 100644 index 000000000..c2f5d7160 --- /dev/null +++ b/admin/admin-service-impl/src/main/java/cn/iocoder/mall/admin/service/AdminAccessLogServiceImpl.java @@ -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 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 diff --git a/admin/admin-service-impl/src/main/resources/mapper/AdminAccessLogMapper.xml b/admin/admin-service-impl/src/main/resources/mapper/AdminAccessLogMapper.xml new file mode 100644 index 000000000..4afa93f7c --- /dev/null +++ b/admin/admin-service-impl/src/main/resources/mapper/AdminAccessLogMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + 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} + ) + + + \ No newline at end of file diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java index 418db24aa..c71bb9323 100644 --- a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java +++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/HttpUtil.java @@ -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. + *

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. +// *

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 request.getCharacterEncoding + * returns null, 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 : ""; + } + + /** + * 根据request拼接queryString + * + * @return queryString + */ + @SuppressWarnings("unchecked") + public static String buildQueryString(HttpServletRequest request) { + Enumeration 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. + *

+ * For example, for a request to URL + *

+ * http://www.somehost.com/myapp/my/url.jsp, + *

+ * for an application deployed to /mayapp (the application's context path), this method would return + *

+ * /my/url.jsp. + * + * 该方法,是从 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. + *

As the value returned by request.getRequestURI() is not + * decoded by the servlet container, this method will decode it. + *

The URI that the web container resolves should 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. WARNING - 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. WARNING - 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. + *

As the value returned by request.getContextPath() is not + * 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". + *

The default implementation uses URLDecoder.decode(input, enc). + * + * @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. + *

The default implementation checks the request's + * {@link ServletRequest#getCharacterEncoding() character encoding}, and if that + * null, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}. + * + * @param request current HTTP request + * @return the encoding for the request (never null) + * @see javax.servlet.ServletRequest#getCharacterEncoding() + */ + protected static String determineEncoding(HttpServletRequest request) { + String enc = request.getCharacterEncoding(); + if (enc == null) { + enc = DEFAULT_CHARACTER_ENCODING; + } + return enc; + } + } \ No newline at end of file diff --git a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java index 8e98169ed..c81be713f 100644 --- a/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java +++ b/common/common-framework/src/main/java/cn/iocoder/common/framework/util/StringUtil.java @@ -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); + } + } \ No newline at end of file