Compare commits
10 Commits
ceea2dab30
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
146b1d067c | ||
|
|
7cb6341b77 | ||
|
|
049fe99af3 | ||
|
|
a20b493c6d | ||
|
|
ed60374536 | ||
|
|
e305716c75 | ||
|
|
7dcffbc93e | ||
|
|
c1078242e1 | ||
|
|
ddc924cb7b | ||
|
|
1e55f9eea0 |
34
Readme.md
34
Readme.md
@@ -1,58 +1,60 @@
|
||||
# web 安全常见漏洞
|
||||
|
||||
1. sql 注入 </br>
|
||||
## 常见漏洞及解决方案
|
||||
|
||||
- sql 注入
|
||||
|
||||
解决方案:参数检测,拦截非法入参,后端使用druid 连接池的sql防火墙
|
||||
参见:com.taoyuanx.securitydemo.web
|
||||
|
||||
2.xss攻击 </br>
|
||||
- xss攻击
|
||||
|
||||
解决方案:参数检测,拦截非法入参,转义html字符,参见com.taoyuanx.securitydemo.web
|
||||
|
||||
</br>
|
||||
|
||||
3.跨站请求
|
||||
|
||||
- 跨站请求
|
||||
|
||||
解决方案:设置允许跨站的header信息
|
||||
|
||||
4.文件,图片等非站内请求
|
||||
- 文件,图片等非站内请求
|
||||
|
||||
解决方案:防盗链处理(Http Rerfer头部,nginx可设置)
|
||||
|
||||
5.接口暴力访问
|
||||
- 接口暴力访问
|
||||
|
||||
解决方案:系统限流,全局限流,单个ip限流,黑名单等,参见:com.taoyuanx.securitydemo.security.RateLimitAspect,com.taoyuanx.securitydemo.web.BlackListFilter
|
||||
|
||||
7.文件未授权访问
|
||||
- 文件未授权访问
|
||||
|
||||
解决方案:上传的文件,展示时后端返回签名的文件,访问时,走一次后端,方便做权限验证,参见,/api/upload,/api/file
|
||||
文件存储时,去除可执行权限,尽量和应用服务器进行物理隔离
|
||||
|
||||
8.文件不安全类型上传
|
||||
- 文件不安全类型上传
|
||||
|
||||
解决方案:校验文件类型,校验流信息,校验文件真实类型,参见/api/upload
|
||||
|
||||
9.密码泄露风险
|
||||
- 密码泄露风险
|
||||
|
||||
密码加密传输,参见login.html
|
||||
|
||||
10.越权访问
|
||||
- 越权访问
|
||||
|
||||
解决方案:权限控制,参见SimpleAuthHandlerIntercepter
|
||||
如果通过tomcat发布静态文件,可通过过滤器禁止非授权访问,如采用其他静态资源服务器,可严格控制后台权限,保证数据不被越权访问
|
||||
,静态资源越权访问,可通过client端js控制location
|
||||
|
||||
11.中间人攻击
|
||||
- 中间人攻击
|
||||
|
||||
解决方案:采用https协议,参数签名,返回值签名,防止参数或返回值被篡改
|
||||
|
||||
12 slowhttp 攻击
|
||||
- slowhttp 攻击
|
||||
|
||||
解决方案:限制恶意访问,有钱的买防护工具,(阿里云盾,知道创宇等),没钱的多部署几台机器,修改连接超时时间,事后分析ip,封禁
|
||||
解决方案:限制恶意访问,有钱的买防护工具,(阿里云盾,知道创宇等),没钱的多部署几台机器,修改连接超时时间,事后分析非法ip封禁,
|
||||
协议漏洞,没啥好防护的策略,没钱的事后防范吧
|
||||
|
||||
|
||||
|
||||
## esapi 介绍
|
||||
|
||||
此jar包为一个比较全面的安全库,控制较为全面,业务较为复杂的可自行扩展
|
||||
## 安全控制库 esapi
|
||||
此jar包为一个比较全面的安全库,控制较为全面,业务较为复杂的可自行扩展
|
||||
基本使用例子:com.taoyuanx.securitydemo.EsapiTest
|
||||
56
pom.xml
56
pom.xml
@@ -6,13 +6,12 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.1.7.RELEASE</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.taoyuanx</groupId>
|
||||
<artifactId>security-demo</artifactId>
|
||||
<artifactId>javaweb_security_handle</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>security-demo</name>
|
||||
<description>java security demo</description>
|
||||
<name>javaweb_security_handle</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -35,7 +34,6 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!-- boot 官方推荐模板引擎-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
@@ -65,11 +63,7 @@
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.10</version>
|
||||
</dependency>
|
||||
<!--<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.64</version>
|
||||
</dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@@ -98,48 +92,8 @@
|
||||
<version>2.2.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<profiles>
|
||||
<!--
|
||||
dev 开发环境
|
||||
test 测试环境
|
||||
prod 生产环境
|
||||
-->
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<profiles.active>dev</profiles.active>
|
||||
</properties>
|
||||
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>test</id>
|
||||
<properties>
|
||||
<profiles.active>test</profiles.active>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>prod</id>
|
||||
<properties>
|
||||
<profiles.active>prod</profiles.active>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
<build>
|
||||
<finalName>${project.artifactId}-${project.version}-${profiles.active}</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>${project.basedir}/profiles/${profiles.active}</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
server.port=9999
|
||||
|
||||
|
||||
logging.path=d://logs
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
server.port=8080
|
||||
logging.path=./logs
|
||||
@@ -1,2 +0,0 @@
|
||||
server.port=8080
|
||||
logging.path=./logs
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.taoyuanx.securitydemo.config;
|
||||
|
||||
import com.taoyuanx.securitydemo.security.ratelimit.AbstractRateLimiter;
|
||||
import com.taoyuanx.securitydemo.security.ratelimit.GuavaRateLimiter;
|
||||
import com.taoyuanx.securitydemo.security.ratelimit.RedisRateLimiter;
|
||||
import com.taoyuanx.securitydemo.utils.RSAUtil;
|
||||
import lombok.Data;
|
||||
@@ -62,13 +63,18 @@ public class GlobalConfig implements InitializingBean {
|
||||
* @param redisTemplate
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
/* @Bean
|
||||
@Autowired
|
||||
public AbstractRateLimiter rateLimiter(StringRedisTemplate redisTemplate) {
|
||||
public AbstractRateLimiter redisRateLimiter(StringRedisTemplate redisTemplate) {
|
||||
RedisRateLimiter redisRateLimiter = new RedisRateLimiter(redisTemplate);
|
||||
return redisRateLimiter;
|
||||
}
|
||||
}*/
|
||||
|
||||
@Bean
|
||||
public AbstractRateLimiter guavaRateLimiter() {
|
||||
GuavaRateLimiter guavaRateLimiter = new GuavaRateLimiter();
|
||||
return guavaRateLimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
@@ -77,10 +83,6 @@ public class GlobalConfig implements InitializingBean {
|
||||
publicKey = RSAUtil.getPublicKey(keyStore);
|
||||
privateKey = RSAUtil.getPrivateKey(keyStore, rsaP12Password);
|
||||
certificate = RSAUtil.getCertificate(keyStore);
|
||||
publickKeyBase64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCejQE9rKg8v5TuaS9tODR9QNPg\n" +
|
||||
"P9MDhtyIFmKLmQTu2an/pEW18XWXEKi04ORYfwr7BjAJ9HMWspGRg8rbSjbethud\n" +
|
||||
"QnJUDtrqHNDYzJ1HhQT7sngGvpuH9ME7ZW4yZcMDS7/i5tQSps31JmGI+ULZg1cv\n" +
|
||||
"G5A/SbSqiE5PXSfIhQIDAQAB";
|
||||
Base64.encodeBase64String(certificate.getEncoded());
|
||||
publickKeyBase64 = Base64.encodeBase64String(publicKey.getEncoded());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class BussinessController {
|
||||
@GetMapping("rateLimit")
|
||||
@ResponseBody
|
||||
@Rate(rate = {@RateLimit(type = RateLimitType.IP, limit = 1),
|
||||
@RateLimit(type = RateLimitType.SERVICE_KEY, limit = 10, key = "api/rateLimit")})
|
||||
@RateLimit(type = RateLimitType.SERVICE_KEY, limit = 10, limitKey = "api/rateLimit")})
|
||||
public String rateLimit() {
|
||||
return "hello rateLimit!";
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public class BussinessController {
|
||||
*/
|
||||
@GetMapping("rateLimit_key")
|
||||
@ResponseBody
|
||||
@RateLimit(type = RateLimitType.SERVICE_KEY, limit = 100, key = "api/rateLimit_key")
|
||||
@RateLimit(type = RateLimitType.SERVICE_KEY, limit = 100, limitKey = "api/rateLimit_key")
|
||||
public String rateLimitKey() {
|
||||
return "hello rateLimit!";
|
||||
}
|
||||
@@ -158,11 +158,6 @@ public class BussinessController {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 登录安全控制
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("loginOut")
|
||||
@ResponseBody
|
||||
public void loginOut(HttpServletResponse response, HttpServletRequest request) throws Exception {
|
||||
@@ -171,7 +166,7 @@ public class BussinessController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 黑名单测试
|
||||
* 文件上传
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@@ -206,9 +201,31 @@ public class BussinessController {
|
||||
public void upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
fileHandler.handleFile(response, request);
|
||||
}
|
||||
@RateLimit(type = RateLimitType.TOTAL_COUNT,key = "rate_count",totalCount = 100)
|
||||
|
||||
@RateLimit(type = RateLimitType.TOTAL_COUNT, limitKey = "rate_count", totalCount = 10)
|
||||
@GetMapping("rate_count")
|
||||
public void rateCount(int index) throws Exception {
|
||||
System.out.println("rate_count\t"+index);
|
||||
@ResponseBody
|
||||
public Result rateCount() throws Exception {
|
||||
System.out.println("rate_count\t");
|
||||
return ResultBuilder.success("ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* el 表达式限流测试
|
||||
*/
|
||||
@RateLimit(type = RateLimitType.SERVICE_KEY, limitKey = "#user", limit = 1)
|
||||
@GetMapping("el_rate")
|
||||
@ResponseBody
|
||||
public Result elRateCountTest(String user) throws Exception {
|
||||
System.out.println("el_rate \t");
|
||||
return ResultBuilder.success("ok");
|
||||
}
|
||||
|
||||
@RateLimit(type = RateLimitType.SERVICE_KEY, limitKey = "#fullMethodName+#user", limit = 1)
|
||||
@GetMapping("el_rate2")
|
||||
@ResponseBody
|
||||
public Result elRateCountTest2(String user) throws Exception {
|
||||
System.out.println("el_rate2 \t");
|
||||
return ResultBuilder.success("ok");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class TokenAuthHandlerIntercepter implements HandlerInterceptor {
|
||||
}
|
||||
Map<String, Object> tokenData = toeknHelper.vafy(token);
|
||||
Long tokenAccountId = toeknHelper.getAccountId(tokenData);
|
||||
if (tokenAccountId != accountId) {
|
||||
if (!tokenAccountId.equals(accountId) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,15 @@ import java.lang.annotation.*;
|
||||
public @interface RateLimit {
|
||||
/**
|
||||
* limit 每秒并发
|
||||
* key 限流的key
|
||||
* limitKey 限流的key 只有 type=RateLimitType.SERVICE_KEY 才支持el表达式
|
||||
* type 限流类型 参见:com.taoyuanx.securitydemo.security.RateLimitType
|
||||
* totalCount 次数限流
|
||||
*/
|
||||
double limit() default 100;
|
||||
|
||||
String key() default "" ;
|
||||
String limitKey() default "";
|
||||
|
||||
RateLimitType type() default RateLimitType.METHOD;
|
||||
|
||||
long totalCount() default 0;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.taoyuanx.securitydemo.security;
|
||||
|
||||
import com.taoyuanx.securitydemo.exception.LimitException;
|
||||
import com.taoyuanx.securitydemo.security.ratelimit.AbstractRateLimiter;
|
||||
import com.taoyuanx.securitydemo.utils.HelperUtil;
|
||||
import com.taoyuanx.securitydemo.utils.RequestUtil;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.Signature;
|
||||
@@ -14,9 +15,15 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* aop限流
|
||||
@@ -28,6 +35,10 @@ public class RateLimitAspect {
|
||||
|
||||
@Autowired
|
||||
AbstractRateLimiter rateLimiter;
|
||||
/**
|
||||
* el表达式解析
|
||||
*/
|
||||
private static ExpressionParser EL_PARSER = new SpelExpressionParser();
|
||||
|
||||
@Pointcut("execution(* com.taoyuanx.securitydemo.controller..*.*(..))&& (@annotation(RateLimit)||@annotation(Rate))")
|
||||
public void ratePointCut() {
|
||||
@@ -35,71 +46,116 @@ public class RateLimitAspect {
|
||||
|
||||
@Around("ratePointCut()")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
handleRateLimit(joinPoint);
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
private void handleRateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
Signature signature = joinPoint.getSignature();
|
||||
MethodSignature methodSignature = (MethodSignature) signature;
|
||||
Method currentMethod = methodSignature.getMethod();
|
||||
String className = joinPoint.getTarget().getClass().getName();
|
||||
String methodName = className + "." + methodSignature.getName();
|
||||
String fullMethodName = className + "." + methodSignature.getName();
|
||||
RateLimit rateLimit = AnnotationUtils.findAnnotation(currentMethod, RateLimit.class);
|
||||
if (rateLimit != null) {
|
||||
handleRateLimit(rateLimit, methodName);
|
||||
doHandleRateLimit(rateLimit, fullMethodName, joinPoint, methodSignature);
|
||||
} else {
|
||||
Rate rate = AnnotationUtils.findAnnotation(currentMethod, Rate.class);
|
||||
if (rate != null && rate.rate() != null) {
|
||||
for (RateLimit limit : rate.rate()) {
|
||||
handleRateLimit(limit, methodName);
|
||||
doHandleRateLimit(limit, fullMethodName, joinPoint, methodSignature);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
|
||||
private void handleRateLimit(RateLimit rateLimit, String methodName) throws Throwable {
|
||||
private void doHandleRateLimit(RateLimit rateLimit, String fullMethodName, ProceedingJoinPoint joinPoint, MethodSignature methodSignature) throws Throwable {
|
||||
RateLimitType type = rateLimit.type();
|
||||
String key = rateLimit.key();
|
||||
if (type == null) {
|
||||
key = methodName;
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("{}限流策略未定义,采用[{}]限流策略", methodName, RateLimitType.METHOD);
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case IP:
|
||||
String serviceKey = rateLimit.key();
|
||||
if (serviceKey == null || key.isEmpty()) {
|
||||
key = RequestUtil.getRemoteIp() + "_" + methodName;
|
||||
} else {
|
||||
key = RequestUtil.getRemoteIp() + "_" + serviceKey;
|
||||
}
|
||||
break;
|
||||
case METHOD:
|
||||
key = methodName;
|
||||
break;
|
||||
case SERVICE_KEY:
|
||||
break;
|
||||
case GLOBAL:
|
||||
key = "global";
|
||||
break;
|
||||
}
|
||||
}
|
||||
String limitKey = parseLimitKey(rateLimit, fullMethodName, joinPoint, methodSignature);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("采用[{}]限流策略,限流key:{}", type, key);
|
||||
LOG.debug("采用[{}]限流策略,限流key:{}", type, limitKey);
|
||||
}
|
||||
if (type.equals(RateLimitType.TOTAL_COUNT)) {
|
||||
if (!rateLimiter.tryCount(1, key, rateLimit.totalCount())) {
|
||||
if (!rateLimiter.tryCount(1, limitKey, rateLimit.totalCount())) {
|
||||
throw new LimitException("访问次数已达最大限制:" + rateLimit.totalCount() + ",请稍后再试");
|
||||
}
|
||||
} else {
|
||||
if (!rateLimiter.tryAcquire(key, rateLimit.limit())) {
|
||||
if (!rateLimiter.tryAcquire(limitKey, rateLimit.limit())) {
|
||||
throw new LimitException("请求过于频繁,请稍后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final String GLOBAL_LIMIT_KEY = "global";
|
||||
|
||||
private String parseLimitKey(RateLimit rateLimit, String fullMethodName, ProceedingJoinPoint joinPoint, MethodSignature methodSignature) {
|
||||
RateLimitType type = rateLimit.type();
|
||||
String limitKey = rateLimit.limitKey();
|
||||
boolean emptyLimitKey = HelperUtil.isEmpty(limitKey);
|
||||
switch (type) {
|
||||
case IP:
|
||||
if (emptyLimitKey) {
|
||||
/**
|
||||
* 单个方法ip限流
|
||||
*/
|
||||
return RequestUtil.getRemoteIp() + "_" + fullMethodName;
|
||||
}
|
||||
/**
|
||||
* 全局ip限流
|
||||
*/
|
||||
return RequestUtil.getRemoteIp() + "_" + limitKey;
|
||||
case METHOD:
|
||||
return fullMethodName;
|
||||
case SERVICE_KEY:
|
||||
/**
|
||||
* el表达式只有在SERVICE_KEY时才会生效
|
||||
* 允许用户自定义限流key
|
||||
*/
|
||||
limitKey = parseLimitKey(rateLimit.limitKey(), fullMethodName, joinPoint, methodSignature);
|
||||
if (HelperUtil.isEmpty(limitKey)) {
|
||||
throw new LimitException("请指定limitKey");
|
||||
}
|
||||
return limitKey;
|
||||
case GLOBAL:
|
||||
return GLOBAL_LIMIT_KEY;
|
||||
default:
|
||||
return fullMethodName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isEl(String limitKey) {
|
||||
return limitKey.startsWith("#");
|
||||
}
|
||||
|
||||
private String parseLimitKey(String limitKey, String fullMethodName, ProceedingJoinPoint joinPoint, MethodSignature methodSignature) {
|
||||
if (HelperUtil.isEmpty(limitKey)) {
|
||||
return limitKey;
|
||||
}
|
||||
if (!isEl(limitKey)) {
|
||||
return limitKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* el表达式解析
|
||||
* 并设置内置变量 方法参数
|
||||
*/
|
||||
Object[] args = joinPoint.getArgs();
|
||||
String[] argsName = methodSignature.getParameterNames();
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
context.setVariable("fullMethodName", fullMethodName);
|
||||
if (Objects.nonNull(args) && args.length > 0) {
|
||||
context.setVariable("args", args);
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
context.setVariable(argsName[i], args[i]);
|
||||
}
|
||||
}
|
||||
Expression expression = EL_PARSER.parseExpression(limitKey);
|
||||
return expression.getValue(context, String.class);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ package com.taoyuanx.securitydemo.security;
|
||||
* @date 2019/8/27
|
||||
*/
|
||||
public enum RateLimitType {
|
||||
IP(0, "IP限流"), METHOD(1, "方法名"),
|
||||
IP(0, "单个方法IP限流"),
|
||||
METHOD(1, "方法名"),
|
||||
SERVICE_KEY(3, "业务自定义key"),
|
||||
GLOBAL(4, "系统全局"),
|
||||
TOTAL_COUNT(5, "总次数限制");
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.taoyuanx.securitydemo.security.lock;
|
||||
|
||||
import com.taoyuanx.securitydemo.security.ratelimit.AbstractRateLimiter;
|
||||
import com.taoyuanx.securitydemo.utils.SpringContextUtil;
|
||||
import com.taoyuanx.securitydemo.utils.StringIntUtil;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
/**
|
||||
* @author dushitaoyuan
|
||||
* @desc redis 限流实现
|
||||
* 复制 spring-cloud-gateway,可与spring-boot,spring-cloud等分离使用
|
||||
* @date 2019/9/5
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisLock {
|
||||
|
||||
@Getter
|
||||
private String lockKey;
|
||||
@Getter
|
||||
private String lockValue;
|
||||
|
||||
|
||||
public RedisLock(String lockKey) {
|
||||
this(lockKey, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public RedisLock(String lockKey, String lockValue) {
|
||||
init();
|
||||
this.lockKey = lockKey;
|
||||
this.lockValue = lockValue;
|
||||
}
|
||||
|
||||
|
||||
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
|
||||
boolean lock = doTryLock();
|
||||
if (!lock) {
|
||||
unit.sleep(time);
|
||||
}
|
||||
return lock;
|
||||
}
|
||||
|
||||
public boolean tryLock() {
|
||||
return doTryLock();
|
||||
}
|
||||
|
||||
|
||||
public void unlock() {
|
||||
String result = redisTemplate.execute(unLockScript, Arrays.asList(lockKey), lockValue);
|
||||
if (FAILED.equals(result)) {
|
||||
log.warn("释放锁失败,lock_value 不匹配 my_lock_value:[{}]", lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doTryLock() {
|
||||
String result = redisTemplate.execute(lockScript, Arrays.asList(lockKey), lockValue, String.valueOf(LOCK_MAX_EXPIRE_SECONDS));
|
||||
if (SUCCESSS.equals(result)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static StringRedisTemplate redisTemplate;
|
||||
private static RedisScript<String> lockScript;
|
||||
private static RedisScript<String> unLockScript;
|
||||
private static String SUCCESSS = "1", FAILED = "0";
|
||||
/**
|
||||
* 最大锁定5分钟
|
||||
*/
|
||||
private static Long LOCK_MAX_EXPIRE_SECONDS = 300L;
|
||||
|
||||
private void init() {
|
||||
if (Objects.isNull(lockScript)) {
|
||||
DefaultRedisScript script = new DefaultRedisScript();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/redis_lock.lua")));
|
||||
script.setResultType(String.class);
|
||||
lockScript = script;
|
||||
}
|
||||
if (Objects.isNull(unLockScript)) {
|
||||
DefaultRedisScript script = new DefaultRedisScript();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/redis_unlock.lua")));
|
||||
script.setResultType(String.class);
|
||||
unLockScript = script;
|
||||
}
|
||||
if (Objects.isNull(redisTemplate)) {
|
||||
redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ package com.taoyuanx.securitydemo.security.ratelimit;
|
||||
* @author dushitaoyuan
|
||||
* @desc 抽象限流
|
||||
* @date 2019/9/5
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractRateLimiter {
|
||||
|
||||
@@ -16,20 +15,27 @@ public abstract class AbstractRateLimiter {
|
||||
* @param limit 限流速率
|
||||
* @return
|
||||
*/
|
||||
public boolean tryAcquire(String key, Double limit){
|
||||
return doTryAcquire(1,key,limit);
|
||||
public boolean tryAcquire(String key, Double limit) {
|
||||
return doTryAcquire(1, key, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param permits 申请令牌数量
|
||||
* @param key 限流标识
|
||||
* @param limit 速率
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean doTryAcquire(int permits, String key, Double limit);
|
||||
|
||||
/**
|
||||
* 增加资源访问次数 用户可自行持久化记录
|
||||
* @param count
|
||||
* @param key
|
||||
* @param totalCount
|
||||
* 增加资源访问次数 用户可自行持久化记录
|
||||
* @param count (申请资源数量)
|
||||
* @param key (资源key)
|
||||
* @param totalCount (资源总量)
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean tryCount(int count,String key,Long totalCount);
|
||||
public abstract boolean tryCount(int count, String key, Long totalCount);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -31,17 +31,18 @@ public class GuavaRateLimiter extends AbstractRateLimiter {
|
||||
rateHolder.clear();
|
||||
}
|
||||
RateLimiter rateLimiter = null;
|
||||
if (rateHolder.containsKey(key)) {
|
||||
rateLimiter = rateHolder.get(key);
|
||||
} else {
|
||||
rateLimiter = RateLimiter.create(limit);
|
||||
if (!rateHolder.containsKey(key)) {
|
||||
rateHolder.putIfAbsent(key, RateLimiter.create(limit));
|
||||
}
|
||||
rateHolder.putIfAbsent(key, rateLimiter);
|
||||
rateLimiter = rateHolder.get(key);
|
||||
return rateLimiter.tryAcquire(permits);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryCount(int count, String key, Long totalCount) {
|
||||
if (count > totalCount) {
|
||||
return false;
|
||||
}
|
||||
//标记后,直接返回false
|
||||
if (TOTAL_LIMIT_ZERO_FLAG.mightContain(key)) {
|
||||
return false;
|
||||
@@ -51,23 +52,16 @@ public class GuavaRateLimiter extends AbstractRateLimiter {
|
||||
countHolder.clear();
|
||||
}
|
||||
LongAdder longAdder = null;
|
||||
if (countHolder.containsKey(key)) {
|
||||
longAdder = countHolder.get(key);
|
||||
longAdder.add(-count);
|
||||
//资源总数用完后,标记
|
||||
if (longAdder.longValue() <= 0) {
|
||||
TOTAL_LIMIT_ZERO_FLAG.put(key);
|
||||
countHolder.remove(key);
|
||||
return true;
|
||||
}
|
||||
if (!countHolder.containsKey(key)) {
|
||||
countHolder.putIfAbsent(key, new LongAdder());
|
||||
}
|
||||
longAdder = countHolder.get(key);
|
||||
if (longAdder.longValue() >= totalCount) {
|
||||
TOTAL_LIMIT_ZERO_FLAG.put(key);
|
||||
countHolder.remove(key);
|
||||
return false;
|
||||
}
|
||||
if (count > totalCount) {
|
||||
return false;
|
||||
}
|
||||
longAdder = new LongAdder();
|
||||
countHolder.putIfAbsent(key, longAdder);
|
||||
countHolder.get(key).add(count);
|
||||
longAdder.add(count);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,12 @@ public class HelperUtil {
|
||||
return str == null || str.isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isNotEmpty(String str) {
|
||||
return !isEmpty(str);
|
||||
}
|
||||
|
||||
public static String getExtension(String fileName) {
|
||||
return fileName.substring(fileName.lastIndexOf(".")+1).toLowerCase();
|
||||
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
|
||||
}
|
||||
|
||||
public static InputStream getInputStream(String filePath) throws FileNotFoundException {
|
||||
|
||||
@@ -46,7 +46,7 @@ public class RequestParamFilterUtil {
|
||||
/**
|
||||
* sql注入风险检测
|
||||
*/
|
||||
private static List<String> SQL_KEY_WORDS = Splitter.on(",").splitToList("'|and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|; |or|-|+|,");
|
||||
private static List<String> SQL_KEY_WORDS = Splitter.on("|").splitToList("'|and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|; |or|-|+|,");
|
||||
public static boolean isSqlInject(String... params) {
|
||||
if (null == params) {
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
--判断资源归0标记是否存在
|
||||
--标记实现,可通过 布隆过滤和bitmap实现 注意redis支持情况
|
||||
local bit_key_offset=tonumber(ARGV[3])
|
||||
redis.log(redis.LOG_WARNING, "bit_key_offset " .. ARGV[3])
|
||||
--redis.log(redis.LOG_WARNING, "bit_key_offset " .. ARGV[3])
|
||||
local zero_flag =redis.call("GETBIT", KEYS[2],bit_key_offset)
|
||||
if zero_flag == 1 then
|
||||
return -1
|
||||
|
||||
23
src/main/resources/META-INF/redis_lock.lua
Normal file
23
src/main/resources/META-INF/redis_lock.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
-- get a lock
|
||||
-- 获取锁成功,则返回 1
|
||||
|
||||
--redis.log(redis.LOG_WARNING, "key " .. KEYS[1])
|
||||
--redis.log(redis.LOG_WARNING, "ARGV1 " .. ARGV[1])
|
||||
--redis.log(redis.LOG_WARNING, "ARGV " .. ARGV[2])
|
||||
local lock_key = KEYS[1]
|
||||
local lock_value = ARGV[1]
|
||||
local ttl = tonumber(ARGV[2])
|
||||
local result = redis.call('setnx', lock_key, lock_value)
|
||||
--redis.log(redis.LOG_WARNING, "result " .. result)
|
||||
if result == 1 then
|
||||
redis.call('expire', lock_key, ttl)
|
||||
return '1'
|
||||
else
|
||||
local value = redis.call('get', lock_key)
|
||||
-- lock_value 一致可重入
|
||||
if (value == lock_value) then
|
||||
result = '1';
|
||||
redis.call('expire', lock_key, ttl)
|
||||
end
|
||||
end
|
||||
return tostring(result)
|
||||
12
src/main/resources/META-INF/redis_unlock.lua
Normal file
12
src/main/resources/META-INF/redis_unlock.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
-- unlock
|
||||
local lock_key = KEYS[1]
|
||||
local lock_value = ARGV[1]
|
||||
--[[--redis.log(redis.LOG_WARNING, "key " .. KEYS[1])
|
||||
--redis.log(redis.LOG_WARNING, "ARGV1 " .. ARGV[1])]]
|
||||
local result = redis.call('get', lock_key)
|
||||
--redis.log(redis.LOG_WARNING, "result " .. result)
|
||||
if result == lock_value then
|
||||
redis.call('del', lock_key)
|
||||
return '1';
|
||||
end
|
||||
return '0'
|
||||
@@ -1,21 +1,10 @@
|
||||
server.port=9999
|
||||
|
||||
#数据库连接池
|
||||
#spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
|
||||
#spring.datasource.username:root
|
||||
#spring.datasource.password:root
|
||||
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
|
||||
#spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
|
||||
#spring.datasource.initialSize=10
|
||||
#spring.datasource.minIdle=10
|
||||
#spring.datasource.maxActive=100
|
||||
#spring.datasource.maxWait=60000
|
||||
#spring.datasource.timeBetweenEvictionRunsMillis=60000
|
||||
#spring.datasource.filters=wall
|
||||
logging.path=d://logs
|
||||
|
||||
spring.profiles.active=@profiles.active@
|
||||
|
||||
spring.redis.database=2
|
||||
spring.redis.host=192.168.30.211
|
||||
spring.redis.database=0
|
||||
spring.redis.host=localhost
|
||||
spring.redis.port=6379
|
||||
spring.redis.password=
|
||||
|
||||
@@ -53,4 +42,3 @@ spring.thymeleaf.suffix=.html
|
||||
logging.level.org.springframework=INFO
|
||||
|
||||
|
||||
logging.path=./logs
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<springProperty name="LOG_HOME" source="logging.path" defaultValue="./logs"/>
|
||||
<property name="LOG_PREFIX" value="seed-boot"/>
|
||||
<property name="LOG_PREFIX" value="demo"/>
|
||||
|
||||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
|
||||
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
|
||||
<springProfile name="dev">
|
||||
<root level="DEBUG">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</springProfile>
|
||||
|
||||
@@ -6,13 +6,9 @@ import org.junit.Test;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -42,31 +38,32 @@ public class BoomFilterTest {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
System.out.println("失败次数:" + count + "错误率:" + PercentUtil.percent(Double.valueOf(count), Double.valueOf(max), 2));
|
||||
System.out.println("失败次数:" + count + "错误率:" + PercentUtil.percent(Double.valueOf(count), Double.valueOf(max), 4));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testEl() {
|
||||
String el = "${m}";
|
||||
Map<String, String> args3 = new HashMap<>();
|
||||
args3.put("args1", "args3_1");
|
||||
args3.put("args2", "args3_2");
|
||||
args3.put("args3", "args3_3");
|
||||
Object[] args = new Object[]{"args1", 2, args3};
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
context.setVariable("m", "1234");
|
||||
Expression expression = parser.parseExpression(el);
|
||||
|
||||
|
||||
context.setVariable("args", args);
|
||||
Expression expression = parser.parseExpression("#args[0]");
|
||||
System.out.println(expression.getValue(context, String.class));
|
||||
|
||||
// 定义变量
|
||||
/* String name = "Tom";
|
||||
EvaluationContext context = new StandardEvaluationContext(); // 表达式的上下文,
|
||||
context.setVariable("myName", name); // 为了让表达式可以访问该对象, 先把对象放到上下文中
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
System.out.println( parser.parseExpression("#myName").getValue(context, String.class));; // Tom , 使用变量
|
||||
|
||||
*/
|
||||
expression = parser.parseExpression("#args[1]");
|
||||
System.out.println(expression.getValue(context, String.class));
|
||||
|
||||
expression = parser.parseExpression("#args[2].get('args1')");
|
||||
System.out.println(expression.getValue(context, String.class));
|
||||
|
||||
expression = parser.parseExpression("#args[2].get('args1')+#args[2].get('args2')");
|
||||
System.out.println(expression.getValue(context, String.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ public class EsapiTest {
|
||||
System.out.println(ESAPI.validator().isValidInput(
|
||||
"email", "12345", "Email",
|
||||
200, false));
|
||||
|
||||
|
||||
System.out.println(ESAPI.validator().isValidInput(
|
||||
"email", "192.168.10.1", "IPAddress",
|
||||
200, false));
|
||||
|
||||
62
src/test/java/com/taoyuanx/securitydemo/RedisLockTest.java
Normal file
62
src/test/java/com/taoyuanx/securitydemo/RedisLockTest.java
Normal file
@@ -0,0 +1,62 @@
|
||||
package com.taoyuanx.securitydemo;
|
||||
|
||||
import com.taoyuanx.securitydemo.controller.BussinessController;
|
||||
import com.taoyuanx.securitydemo.security.lock.RedisLock;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
public class RedisLockTest {
|
||||
@Test
|
||||
public void lockTestOne() throws InterruptedException {
|
||||
RedisLock redisLock = new RedisLock("lock:xxx","11");
|
||||
if (redisLock.tryLock()) {
|
||||
System.out.println("获取到锁了");
|
||||
redisLock.unlock();
|
||||
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void lockTest() throws InterruptedException {
|
||||
RedisLock redisLock = new RedisLock("lock:xxx");
|
||||
for (int i = 0; i < 100; i++) {
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
if (redisLock.tryLock(3,TimeUnit.SECONDS)) {
|
||||
count++;
|
||||
System.out.println("执行");
|
||||
redisLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("未执行");
|
||||
});
|
||||
}
|
||||
System.out.println(count);
|
||||
executorService.shutdown();
|
||||
while (!executorService.awaitTermination(3, TimeUnit.SECONDS)){
|
||||
System.out.println("休眠3秒");
|
||||
}
|
||||
System.out.println("结束");
|
||||
}
|
||||
|
||||
private ExecutorService executorService;
|
||||
private Integer count = 0;
|
||||
|
||||
@Before
|
||||
public void before() throws Exception {
|
||||
executorService = Executors.newFixedThreadPool(10);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class SecurityDemoApplicationTests {
|
||||
public void reateLimitCount() throws Exception {
|
||||
int batch=101;
|
||||
for(int i=0;i<batch;i++){
|
||||
bussinessController.rateCount(i);
|
||||
bussinessController.rateCount();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!--日志文件输出的文件名 -->
|
||||
<FileNamePattern>${LOG_HOME}/cert.log.%d{yyyy-MM-dd}.log
|
||||
<FileNamePattern>${LOG_HOME}/demo.log.%d{yyyy-MM-dd}.log
|
||||
</FileNamePattern>
|
||||
<!--日志文件保留天数 -->
|
||||
<MaxHistory>30</MaxHistory>
|
||||
|
||||
Reference in New Issue
Block a user