Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</parent>
<groupId>com.jmal</groupId>
<artifactId>jmalcloud</artifactId>
<version>2.16.5</version>
<version>2.16.6</version>
<name>jmalcloud</name>
<description>Cloud Disk</description>
<packaging>jar</packaging>
Expand Down
90 changes: 34 additions & 56 deletions src/main/java/com/jmal/clouddisk/service/impl/LogService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import com.jmal.clouddisk.model.LogOperationDTO;
import com.jmal.clouddisk.model.rbac.ConsumerDO;
import com.jmal.clouddisk.service.Constants;
import com.jmal.clouddisk.util.IPUtil;
import com.jmal.clouddisk.util.ResponseResult;
import com.jmal.clouddisk.util.ResultUtil;
import com.jmal.clouddisk.util.TimeUntils;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -26,6 +28,7 @@
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -41,8 +44,6 @@
@RequiredArgsConstructor
public class LogService {

private static final int REGION_LENGTH = 5;

private final ILogDAO logDAO;

private final UserLoginHolder userLoginHolder;
Expand Down Expand Up @@ -145,43 +146,12 @@ public LogOperation getLogOperation(HttpServletRequest request, LogOperation log
// 请求方式
logOperation.setMethod(request.getMethod());
// 客户端ip
String ip = getIpAddress(request);
String ip = IPUtil.getClientIP(request);
logOperation.setIp(ip);
setIpInfo(logOperation, ip);
return logOperation;
}

private String getIpAddress(HttpServletRequest request) {
String ip = request.getRemoteHost();
if (CharSequenceUtil.isNotBlank(ip)) {
return ip;
}
ip = request.getHeader("x-forwarded-for");
if (!CharSequenceUtil.isBlank(ip) && (ip.contains(","))) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
ip = ip.split(",")[0];
}
if (CharSequenceUtil.isBlank(ip)) {
ip = request.getHeader("X-real-ip");
}
if (CharSequenceUtil.isBlank(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (CharSequenceUtil.isBlank(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (CharSequenceUtil.isBlank(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (CharSequenceUtil.isBlank(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (CharSequenceUtil.isBlank(ip)) {
ip = request.getHeader("X-Real-IP");
}
return ip;
}

/***
* 设置IP详细信息
* @param logOperation LogOperation
Expand All @@ -199,33 +169,30 @@ private void setIpInfo(LogOperation logOperation, String ip) {
}
}

/***
* 解析IP区域信息
/**
* 解析IP区域信息, region格式: 国家|区域|省份|城市|运营商
*/
public LogOperation.IpInfo region2IpInfo(String region) {
LogOperation.IpInfo ipInfo = new LogOperation.IpInfo();
String[] r = region.split("\\|");
if (r.length != REGION_LENGTH) return ipInfo;
String country = r[0];
if (!Constants.REGION_DEFAULT.equals(country)) {
ipInfo.setCountry(country);
}
String area = r[1];
if (!Constants.REGION_DEFAULT.equals(area)) {
ipInfo.setArea(area);
}
String province = r[2];
if (!Constants.REGION_DEFAULT.equals(province)) {
ipInfo.setProvince(province);
}
String city = r[3];
if (!Constants.REGION_DEFAULT.equals(city)) {
ipInfo.setCity(city);

if (region == null || region.isEmpty()) {
return ipInfo;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了使代码更健壮,建议使用 CharSequenceUtil.isBlank(region) 来代替 region == null || region.isEmpty()isBlank 可以处理 null、空字符串以及只包含空白字符的字符串(例如 " ")。这个类中已经导入并使用了 cn.hutool.core.text.CharSequenceUtil,因此保持代码风格一致性也是一个好处。

Suggested change
if (region == null || region.isEmpty()) {
return ipInfo;
}
if (CharSequenceUtil.isBlank(region)) {
return ipInfo;
}

String operators = r[4];
if (!Constants.REGION_DEFAULT.equals(operators)) {
ipInfo.setOperators(operators);

String[] parts = IPUtil.SPLIT_PATTERN.split(region, IPUtil.REGION_LENGTH);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

此处 split 方法的 limit 参数使用不当。当 region 字符串包含的 | 分隔符超过预期(4个)时,split(region, 5) 会将第五个分隔符之后的所有内容都放入最后一个数组元素中,导致 parts.length 仍然为5,从而绕过了长度检查,但最后一个元素的内容是错误的。

例如,如果 region"a|b|c|d|e|f"parts 将是 ["a", "b", "c", "d", "e|f"],这会导致后续的IP区域信息解析错误。

建议移除 limit 参数,让 split 方法分割所有部分,然后通过数组长度检查来保证数据的正确性,这与之前的逻辑行为一致。

Suggested change
String[] parts = IPUtil.SPLIT_PATTERN.split(region, IPUtil.REGION_LENGTH);
String[] parts = IPUtil.SPLIT_PATTERN.split(region);


if (parts.length != IPUtil.REGION_LENGTH) {
return ipInfo;
}

String def = Constants.REGION_DEFAULT;

if (!def.equals(parts[0])) ipInfo.setCountry(parts[0]);
if (!def.equals(parts[1])) ipInfo.setArea(parts[1]);
if (!def.equals(parts[2])) ipInfo.setProvince(parts[2]);
if (!def.equals(parts[3])) ipInfo.setCity(parts[3]);
if (!def.equals(parts[4])) ipInfo.setOperators(parts[4]);

return ipInfo;
}

Expand Down Expand Up @@ -347,4 +314,15 @@ public ResponseResult<List<LogOperationDTO>> getFileOperationHistory(LogOperatio
public long getVisitsByUrl(String url) {
return logDAO.countByUrl(url);
}

@PreDestroy
public void destroy() {
if (ipSearcher != null) {
try {
ipSearcher.close();
} catch (IOException e) {
log.error("failed to close ip searcher", e);
}
}
}
}
59 changes: 59 additions & 0 deletions src/main/java/com/jmal/clouddisk/util/IPUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.jmal.clouddisk.util;

import jakarta.servlet.http.HttpServletRequest;

import java.util.regex.Pattern;

public final class IPUtil {

private IPUtil() {
}

public static final int REGION_LENGTH = 5;
public static final Pattern SPLIT_PATTERN = Pattern.compile("\\|");

private static final String[] IP_HEADERS = {
"CF-Connecting-IP",
"X-Real-IP",
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP"
};

/**
* 获取客户端真实IP
* 自动处理大小写不敏感的请求头
*
* @param request HTTP请求对象
* @return 客户端IP地址
*/
public static String getClientIP(HttpServletRequest request) {
for (String header : IP_HEADERS) {
String ip = request.getHeader(header);
if (isValidIp(ip)) {
return extractFirstIp(ip);
}
}
return request.getRemoteAddr();
}
Comment on lines 30 to 42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前的 getClientIP 方法在处理某些边缘情况时存在逻辑缺陷。当HTTP头中的IP地址字符串包含前导逗号(例如 X-Forwarded-For: , 1.2.3.4)时,isValidIp(ip) 会返回 true,但 extractFirstIp(ip) 会返回一个空字符串,导致此方法最终返回一个空字符串而不是正确的IP地址。

为了修复这个问题并使逻辑更健壮,建议调整为先提取第一个IP,然后再对提取出的IP进行有效性验证。这样可以正确处理上述边缘情况,并确保只有有效的IP地址被返回。

Suggested change
public static String getClientIP(HttpServletRequest request) {
for (String header : IP_HEADERS) {
String ip = request.getHeader(header);
if (isValidIp(ip)) {
return extractFirstIp(ip);
}
}
return request.getRemoteAddr();
}
public static String getClientIP(HttpServletRequest request) {
for (String header : IP_HEADERS) {
String ip = request.getHeader(header);
if (ip == null) {
continue;
}
String firstIp = extractFirstIp(ip);
if (isValidIp(firstIp)) {
return firstIp;
}
}
return request.getRemoteAddr();
}


/**
* 提取第一个IP(处理逗号分隔的多IP情况)
*/
private static String extractFirstIp(String ip) {
if (ip.contains(",")) {
int commaIndex = ip.indexOf(',');
return ip.substring(0, commaIndex).trim();
}
return ip.trim();
}

/**
* 检查IP是否有效
*/
private static boolean isValidIp(String ip) {
return ip != null
&& !ip.isEmpty()
&& !"unknown".equalsIgnoreCase(ip);
}
Comment on lines +58 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前的IP验证逻辑 (isValidIp) 过于宽松,只检查了字符串是否为非空且不等于 'unknown'。这可能允许格式不正确的字符串(例如,恶意注入的文本)被当作有效的IP地址处理和记录,存在潜在的日志注入风险。

为了增强安全性和数据准确性,建议使用 hutool 库中的 NetUtil.isIP() 方法来进行更严格的IP地址格式验证。这将确保只接受有效的IPv4或IPv6地址。

为了方便阅读,建议在文件顶部添加 import cn.hutool.core.net.NetUtil;,然后在代码中使用 NetUtil.isIP(ip)

    private static boolean isValidIp(String ip) {
        return ip != null
                && !ip.isEmpty()
                && !"unknown".equalsIgnoreCase(ip)
                && cn.hutool.core.net.NetUtil.isIP(ip);
    }

}