diff --git a/Dockerfile b/Dockerfile index eb07d50..6b9c8aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ ENV COCO_EXPIRATION_TTL=5 \ COCO_BASE_PROXY="" \ COCO_FREQUENCY_TIME=1 \ COCO_FREQUENCY_DEGREE=8 \ - COCO_USER_RATE_TIME=5 \ + COCO_USER_RATE_TIME=1 \ COCO_USER_FREQUENCY_DEGREE=10 \ COCO_USER_TOKEN_EXPIRE=1 \ COCO_USER_LEVEL=2 \ diff --git a/docker-compose.yml b/docker-compose.yml index 7e5dab1..3484799 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,31 +12,53 @@ services: ports: - "8181:8181" environment: + # coco配置 + # 请求L站 state 过期时间 分钟 COCO_EXPIRATION_TTL: "5" + # L站 oauth2认证重定向地址 COCO_REDIRECT_URI: "" + # L站 oauth2认证客户端id COCO_CLIENT_ID: "" + # L站 oauth2认证客户端secret COCO_CLIENT_SECRET: "" + # L站 oauth2认证地址 COCO_AUTHORIZATION_ENDPOINT: "" + # L站 oauth2认证token地址 COCO_TOKEN_ENDPOINT: "" + # L站 oauth2认证用户信息地址 COCO_USER_ENDPOINT: "" + # 代理节点 完整地址 COCO_BASE_API: "" + # 代理节点 COCO_BASE_PROXY: "" + # ghu 频率秒 1秒8次 COCO_FREQUENCY_TIME: "1" + # ghu频率数 COCO_FREQUENCY_DEGREE: "8" - COCO_USER_RATE_TIME: "5" + # 用户基础频率 1分钟10次 + COCO_USER_RATE_TIME: "1" + # 用户基础频率数 COCO_USER_FREQUENCY_DEGREE: "10" + # 用户token 有效期无请求接口 小时 COCO_USER_TOKEN_EXPIRE: "1" + # 允许用户的最低等级等级 COCO_USER_LEVEL: "2" + # 风控参数 + # 一个账号 一天能获取多少次Token RISK_CONTR_GET_TOKEN_NUM: "10" + # 单token 成功请求的最大数量 * 倍率 token失效 RISK_CONTR_TOKEN_MAX_REQ: "500" + # 每天用户最大请求数量 *倍率 限制 10小时 不可登陆 RISK_CONTR_USER_MAX_REQ: "2000" RISK_CONTR_USER_MAX_TIME: "10" + # 用户最大访问速率 + # 10次 该用户访问频率为 基础 * 0.5倍 RISK_CONTR_TOKEN_INVALID_NUM: "10" + # 30次 token失效 2小时不可登陆 RISK_CONTR_REJECT_TIME_NUM: "30" RISK_CONTR_REJECT_TIME: "2" + # ban 5次 5次不可登陆 RISK_CONTR_BAN_NUM: "5" - volumes: - - ./src/main/resources/redisson-config.yml:/app/conf/redisson-config.yml:rw redis: image: redis diff --git a/src/main/java/com/coco/boot/config/core/CustomizedApplicationIP.java b/src/main/java/com/coco/boot/config/core/CustomizedApplicationIP.java new file mode 100644 index 0000000..0cdf40a --- /dev/null +++ b/src/main/java/com/coco/boot/config/core/CustomizedApplicationIP.java @@ -0,0 +1,45 @@ +package com.coco.boot.config.core; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.spi.PropertyDefiner; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 自定义logbackIp获取. + * + * @author Hua + * @since 2024-03-20 10:20 + */ +@Slf4j +public class CustomizedApplicationIP extends ClassicConverter implements PropertyDefiner { + private static String ip; + + static { + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("自定义logbackIp获取异常", e); + } + } + + @Override + public String convert(ILoggingEvent event) { + return ip; + } + + /** + * 用于更新使用IP + */ + static void updateIp(String eurekaIp) { + ip = eurekaIp; + } + + @Override + public String getPropertyValue() { + return ip; + } +} diff --git a/src/main/java/com/coco/boot/config/core/CustomizedRemoteIp.java b/src/main/java/com/coco/boot/config/core/CustomizedRemoteIp.java new file mode 100644 index 0000000..1756835 --- /dev/null +++ b/src/main/java/com/coco/boot/config/core/CustomizedRemoteIp.java @@ -0,0 +1,26 @@ +package com.coco.boot.config.core; + +import ch.qos.logback.classic.pattern.ClassicConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.coco.boot.utils.IpUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * 自定义logbackIp获取. + * @author Hua + * @since 2024-03-20 10:20 + */ +public class CustomizedRemoteIp extends ClassicConverter{ + @Override + public String convert(ILoggingEvent event) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes == null) { + return "127.0.0.1"; + } + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + return IpUtils.getIpAddr(request); + } +} diff --git a/src/main/java/com/coco/boot/utils/IpUtils.java b/src/main/java/com/coco/boot/utils/IpUtils.java new file mode 100644 index 0000000..1f4fa27 --- /dev/null +++ b/src/main/java/com/coco/boot/utils/IpUtils.java @@ -0,0 +1,301 @@ +package com.coco.boot.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Optional; + +/** + * 获取IP方法 + * + * @author Hua + * @since 2024-03-20 10:20 + */ +@Slf4j +public class IpUtils { + + private static final String UNKNOWN = "unknown"; + + /** + * 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址, + * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值 + * + * @return ip + */ + public static String getIpAddr(HttpServletRequest request) { + if (request == null) { + return "unknown"; + } + String ip = request.getHeader("x_forward_for"); + if (ip != null && !ip.isEmpty() && + !UNKNOWN.equalsIgnoreCase(ip) && + ip.contains(",")) { + // 多次反向代理后会有多个ip值,第一个ip才是真实ip + ip = ip.split(",")[0]; + } + + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("x-forwarded-for"); + if (ip != null && !ip.isEmpty() && + !UNKNOWN.equalsIgnoreCase(ip) && + ip.contains(",")) { + // 多次反向代理后会有多个ip值,第一个ip才是真实ip + ip = ip.split(",")[0]; + } + } + + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + public static String getIpAddr(ServerHttpRequest request) { + if (request == null) { + return "unknown"; + } + HttpHeaders headers = request.getHeaders(); + String ip = headers.getFirst("x_forward_for"); + if (ip != null && !ip.isEmpty() && + !UNKNOWN.equalsIgnoreCase(ip) && + ip.contains(",")) { + // 多次反向代理后会有多个ip值,第一个ip才是真实ip + ip = ip.split(",")[0]; + } + + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = headers.getFirst("x-forwarded-for"); + if (ip != null && !ip.isEmpty() && + !UNKNOWN.equalsIgnoreCase(ip) && + ip.contains(",")) { + // 多次反向代理后会有多个ip值,第一个ip才是真实ip + ip = ip.split(",")[0]; + } + } + + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = headers.getFirst("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = headers.getFirst("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = headers.getFirst("HTTP_CLIENT_IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = headers.getFirst("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = headers.getFirst("X-Real-IP"); + } + if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) { + ip = Optional.ofNullable(request.getRemoteAddress()). + map(remoteAddress->remoteAddress.getAddress().getHostAddress()) + .orElse("unknown"); + } + return ip; + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) { + if (addr == null || addr.length < 2) { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) { + return true; + } + case SECTION_5: + if (b1 == SECTION_6) { + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) { + if (text.isEmpty()) { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try { + long l; + int i; + switch (elements.length) { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } catch (NumberFormatException e) { + log.error("将IPv4地址转换成字节异常", e); + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("获取IP地址异常",e); + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + log.error("获取主机名异常",e); + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) { + if (!isUnknown(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) { + return StrUtil.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } +} diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index 305674b..ec060ce 100644 --- a/src/main/resources/application-docker.yml +++ b/src/main/resources/application-docker.yml @@ -58,3 +58,9 @@ coco: #允许用户的最低等级等级 userLevel: ${COCO_USER_LEVEL:2} +# 日志配置 +log: + # 日志路径 + path: /app/logs + # 日志最大的历史 + max-history: 730 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4c71aa8..3ed4602 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -49,10 +49,10 @@ coco: frequencyTime: 1 #ghu频率数 frequencyDegree: 8 - #用户基础频率 5分钟10次 - userRateTime: 5 + #用户基础频率 1分钟10次 + userRateTime: 1 #用户基础频率数 - userFrequencyDegree: 15 + userFrequencyDegree: 10 #用户token 有效期无请求接口 小时 userTokenExpire: 1 #允许用户的最低等级等级 @@ -77,3 +77,9 @@ risk-contr: #ban级别 触发 5 次上面 reject 限制 封号 banNum: 5 +# 日志配置 +log: + # 日志路径 + path: ./logs + # 日志最大的历史 + max-history: 730 \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..feb047b --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + ${log.pattern} + + + + + ${log.path}/info.log + + + + ${log.path}/%d{yyyy-MM-dd}/info.log.gz + + ${maxHistory} + + + ${log.pattern} + + + + + ${log.path}/error.log + + + + ${log.path}/%d{yyyy-MM-dd}/error.log.gz + + ${maxHistory} + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + ${log.path}/redis.log + + + + ${log.path}/%d{yyyy-MM-dd}/redis.log.gz + + ${maxHistory} + + + ${log.pattern} + + + + + + + + + + + + + + + + + + +