feat:V3.1.1整合remi-comm到remi-cloud中

This commit is contained in:
dianjiu
2024-09-11 17:41:17 +08:00
parent e111c59a69
commit 28d62f6cf4
245 changed files with 20975 additions and 154 deletions

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>remi-comm-safe</artifactId>
<name>remi-comm-safe</name>
<description>remi-comm-safe</description>
<parent>
<groupId>ai.remi.top.cloud</groupId>
<artifactId>remi-comm</artifactId>
<version>3.1.1</version>
</parent>
<!-- 打包上传到私服, 需要和配置文件中的server节点配合使用 -->
<distributionManagement>
<repository>
<id>maven-releases</id>
<name>release</name>
<url>https://repository.njydsz.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>maven-snapshots</id>
<name>snapshot</name>
<url>https://repository.njydsz.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<dependencies>
<!--公共依赖-->
<dependency>
<groupId>ai.remi.top.cloud</groupId>
<artifactId>remi-comm-core</artifactId>
</dependency>
<dependency>
<groupId>ai.remi.top.cloud</groupId>
<artifactId>remi-comm-redis</artifactId>
</dependency>
<dependency>
<groupId>ai.remi.top.cloud</groupId>
<artifactId>remi-comm-util</artifactId>
</dependency>
<dependency>
<groupId>ai.remi.top.cloud</groupId>
<artifactId>remi-comm-exception</artifactId>
</dependency>
<!--spring-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,47 @@
package ai.remi.comm.safe.annotation;
import ai.remi.comm.safe.constant.SafeConstants;
import ai.remi.comm.safe.enums.LimitType;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;
/**
* 基于Redis的防刷限流注解
*
* @author DianJiu 【公众号 点九开源】
* @email dianjiuxyz@gmail.com
* @date 2022-07-19
*/
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface RedisLimit {
/**
* key
*/
String key() default SafeConstants.REDIS_LIMIT_KEY;
/**
* Key的前缀
*/
String prefix() default "";
/**
* 一定时间内最多访问次数
*/
int count();
/**
* 给定的时间范围 单位(秒)
*/
int period();
/**
* 限流的类型(用户自定义key或者请求ip)
*/
LimitType limitType() default LimitType.CUSTOMER;
}

View File

@@ -0,0 +1,26 @@
package ai.remi.comm.safe.annotation;
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
*
* @author DianJiu 【公众号 点九开源】
* @email dianjiuxyz@gmail.com
* @date 2022-07-19
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍候再试";
}

View File

@@ -0,0 +1,26 @@
package ai.remi.comm.safe.annotation;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;
/**
* 基于Sentinel的防刷限流注解
*
* @author DianJiu 【公众号 点九开源】
* @email dianjiuxyz@gmail.com
* @date 2022-07-19
*/
@Inherited
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface SentinelLimit {
String resourceName();
int limitCount() default 5;
}

View File

@@ -0,0 +1,21 @@
package ai.remi.comm.safe.annotation;
import ai.remi.comm.safe.aspect.XssValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Constraint(validatedBy = {XssValidator.class})
public @interface Xss {
String message() default "不允许任何脚本运行";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,146 @@
package ai.remi.comm.safe.aspect;
import com.google.common.collect.ImmutableList;
import ai.remi.comm.exception.custom.BusinessException;
import ai.remi.comm.safe.annotation.RedisLimit;
import ai.remi.comm.safe.enums.LimitType;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
@Aspect
@Component
@Order(10)
public class RedisLimitAspect {
private static final Logger logger = LoggerFactory.getLogger(RedisLimitAspect.class);
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Resource
private DefaultRedisScript<Number> redisLuaScript;
@Autowired
public RedisLimitAspect(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Pointcut(value = "@annotation(ai.remi.comm.safe.annotation.RedisLimit)")
public void redisLimit() {
}
@Around("redisLimit()")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取连接点所在类名
String classname = joinPoint.getTarget().getClass().getSimpleName();
// 获取连接点所在方法名称
String methodName = method.getName();
String functionName = String.format("【%s.%s()】", classname, methodName);
RedisLimit redisLimit = method.getAnnotation(RedisLimit.class);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
LimitType limitType = redisLimit.limitType();
String key = redisLimit.key();
if (StringUtils.isEmpty(key)) {
if (limitType == LimitType.IP) {
key = getIpAddr(request);
} else {
key = redisLimit.key();
}
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(redisLimit.prefix(), key));
//执行到这里开始走进来的方法体(必须声明)
Object returnValue;
try {
String luaScript = buildLuaScript();
redisLuaScript = new DefaultRedisScript<>(luaScript, Number.class);
//调用lua脚本,获取返回结果,这里即为请求的次数
Number number = limitRedisTemplate.execute(
redisLuaScript,
keys,
redisLimit.count(),
redisLimit.period()
);
if (number == null || number.intValue() == 0) {
throw new BusinessException("111111", "访问频率过快,被限流了");
}
logger.info("第{}次访问 [{}] 接口!", number, functionName);
returnValue = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new BusinessException(((BusinessException) throwable).getCode(), (throwable).getMessage());
}
return returnValue;
}
/**
* 限流脚本
*/
private String buildLuaScript() {
return "local limit = tonumber(ARGV[1])" +
"local current = tonumber(redis.call('get', key) or '0')" +
"if current + 1 > limit then" +
" return 0" +
"else" +
" current = redis.call('incr',KEYS[1])" +
" redis.call('expire',KEYS[1],ARGV[2])" +
" return current" +
"end;";
}
/**
* 获取请求的IP方法
*
* @param request
* @return
*/
private static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}

View File

@@ -0,0 +1,98 @@
package ai.remi.comm.safe.aspect;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import ai.remi.comm.safe.annotation.SentinelLimit;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Aspect
@Component
@Order(10)
public class SentinelLimitAspect {
private static void initFlowRule(String resourceName,int limitCount) {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//设置受保护的资源
rule.setResource(resourceName);
//设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源阈值
rule.setCount(limitCount);
rules.add(rule);
//加载配置好的规则
FlowRuleManager.loadRules(rules);
}
@Pointcut(value = "@annotation(ai.remi.comm.safe.annotation.SentinelLimit)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object around(ProceedingJoinPoint joinPoint) {
//1、获取当前的调用方法
Method currentMethod = getCurrentMethod(joinPoint);
if (Objects.isNull(currentMethod)) {
return null;
}
//2、从方法注解定义上获取限流的类型
String resourceName = currentMethod.getAnnotation(SentinelLimit.class).resourceName();
if(StringUtils.isEmpty(resourceName)){
throw new RuntimeException("资源名称为空");
}
int limitCount = currentMethod.getAnnotation(SentinelLimit.class).limitCount();
initFlowRule(resourceName,limitCount);
Entry entry = null;
Object result = null;
try {
entry = SphU.entry(resourceName);
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
System.out.println("blocked");
return "被限流了";
} catch (Exception e) {
Tracer.traceEntry(e, entry);
} finally {
if (entry != null) {
entry.exit();
}
}
return result;
}
private Method getCurrentMethod(JoinPoint joinPoint) {
Method[] methods = joinPoint.getTarget().getClass().getMethods();
Method target = null;
for (Method method : methods) {
if (method.getName().equals(joinPoint.getSignature().getName())) {
target = method;
break;
}
}
return target;
}
}

View File

@@ -0,0 +1,27 @@
package ai.remi.comm.safe.aspect;
import ai.remi.comm.safe.annotation.Xss;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class XssValidator implements ConstraintValidator<Xss, String> {
private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />";
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isBlank(value)) {
return true;
}
return !containsHtml(value);
}
public static boolean containsHtml(String value) {
Pattern pattern = Pattern.compile(HTML_PATTERN);
Matcher matcher = pattern.matcher(value);
return matcher.matches();
}
}

View File

@@ -0,0 +1,24 @@
package ai.remi.comm.safe.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
@Configuration
public class RedisLimiterHelper {
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory factory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(factory);
return template;
}
}

View File

@@ -0,0 +1,18 @@
package ai.remi.comm.safe.constant;
/**
* 缓存的key 常量
*/
public class SafeConstants {
/**
* 防重提交 redis key
*/
public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
/**
* 限流 redis key
*/
public static final String REDIS_LIMIT_KEY = "redis_limit:";
}

View File

@@ -0,0 +1,18 @@
package ai.remi.comm.safe.enums;
/**
* 限制的类型
*/
public enum LimitType {
/**
* 自定义key
*/
CUSTOMER,
/**
* 根据请求者IP
*/
IP;
}

View File

@@ -0,0 +1,61 @@
package ai.remi.comm.safe.filter;
import ai.remi.comm.core.enums.HttpMethod;
import ai.remi.comm.util.string.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 防止XSS攻击的过滤器
*
*/
public class XssFilter implements Filter {
/**
* 排除链接
*/
public List<String> excludes = new ArrayList<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String tempExcludes = filterConfig.getInitParameter("excludes");
if (StringUtils.isNotEmpty(tempExcludes)) {
String[] url = tempExcludes.split(",");
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (handleExcludeURL(req, resp)) {
chain.doFilter(request, response);
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}
private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
String url = request.getServletPath();
String method = request.getMethod();
// GET DELETE 不过滤
if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) {
return true;
}
return StringUtils.matches(url, excludes);
}
@Override
public void destroy() {
}
}

View File

@@ -0,0 +1,95 @@
package ai.remi.comm.safe.filter;
import ai.remi.comm.util.html.EscapeUtils;
import ai.remi.comm.util.string.StringUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* XSS过滤处理
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* @param request
*/
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapesValues = new String[length];
for (int i = 0; i < length; i++) {
// 防xss攻击和过滤前后空格
escapesValues[i] = EscapeUtils.clean(values[i]).trim();
}
return escapesValues;
}
return super.getParameterValues(name);
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 非json类型,直接返回
if (!isJsonRequest()) {
return super.getInputStream();
}
// 为空,直接返回
String json = IOUtils.toString(super.getInputStream(), "utf-8");
if (StringUtils.isEmpty(json)) {
return super.getInputStream();
}
// xss过滤
json = EscapeUtils.clean(json).trim();
byte[] jsonBytes = json.getBytes("utf-8");
final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public int available() throws IOException {
return jsonBytes.length;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bis.read();
}
};
}
/**
* 是否是Json请求
*
* @param request
*/
public boolean isJsonRequest() {
String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
}