Commit 25684165 authored by gaozhaochen's avatar gaozhaochen

fix: 安全漏洞修复

- 修复XSS储存型漏洞
- 修复验证码可进行爆破漏洞,连续错误3次及以上锁定账户
- 修复短信轰炸漏洞,验证码有效期5分钟
parent 5387fe3a
......@@ -17,7 +17,7 @@ public class ValidateCodeUtil {
/**
* 过期时间/s
*/
private final static Long EXPIRE_TIME_SECOND = 120L;
private final static Long EXPIRE_TIME_SECOND = 300L;
/**
* 从redis取出验证码并验证
......
......@@ -2,11 +2,11 @@ package cn.sh.stc.sict.cloud.common.gateway.filter;
import cn.hutool.core.util.StrUtil;
import cn.sh.stc.sict.cloud.common.core.constant.Constant;
import cn.sh.stc.sict.cloud.common.core.constant.RedisCacheConstant;
import cn.sh.stc.sict.cloud.common.core.constant.SecurityConstants;
import cn.sh.stc.sict.cloud.common.core.constant.enums.LoginTypeEnum;
import cn.sh.stc.sict.cloud.common.core.exception.ValidateCodeException;
import cn.sh.stc.sict.cloud.common.core.util.R;
import cn.sh.stc.sict.cloud.common.core.util.SmsSendUtil;
import cn.sh.stc.sict.cloud.common.core.util.ValidateCodeUtil;
import cn.sh.stc.sict.cloud.common.core.util.WebUtils;
import cn.sh.stc.sict.cloud.common.gateway.config.FilterIgnorePropertiesConfig;
......@@ -24,6 +24,12 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @Description 验证码处理
* @Author
......@@ -33,6 +39,17 @@ import reactor.core.publisher.Mono;
@Component
@AllArgsConstructor
public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory {
// 失败次数阈值
private static final int MAX_ATTEMPTS_TIER1 = 3;
private static final int MAX_ATTEMPTS_TIER2 = 4;
private static final int MAX_ATTEMPTS_TIER3 = 5;
// 对应的锁定时间
private static final long LOCK_TIME_TIER1_MINUTES = 10;
private static final long LOCK_TIME_TIER2_MINUTES = 30;
private static final long LOCK_TIME_TIER3_HOURS = 2;
private final ObjectMapper objectMapper;
private final FilterIgnorePropertiesConfig filterIgnorePropertiesConfig;
private final StringRedisTemplate redisTemplate;
......@@ -43,9 +60,7 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory {
ServerHttpRequest request = exchange.getRequest();
// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getURI().getPath()
, SecurityConstants.OAUTH_TOKEN_URL, SecurityConstants.SMS_TOKEN_URL
, SecurityConstants.SOCIAL_TOKEN_URL)) {
if (!StrUtil.containsAnyIgnoreCase(request.getURI().getPath(), SecurityConstants.OAUTH_TOKEN_URL, SecurityConstants.SMS_TOKEN_URL, SecurityConstants.SOCIAL_TOKEN_URL)) {
return chain.filter(exchange);
}
......@@ -69,9 +84,7 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory {
String phone = mobile.split("@")[2];
String code = request.getQueryParams().getFirst("code");
log.error("mobile = {}, code = {}", phone, code);
if(!ValidateCodeUtil.validateCode(redisTemplate, phone, code, Constant.BYTE_NO)){
throw new ValidateCodeException("验证码不合法");
}
validCode(redisTemplate, phone, code);
return chain.filter(exchange);
} else {
return chain.filter(exchange);
......@@ -100,10 +113,7 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory {
response.setStatusCode(HttpStatus.PRECONDITION_REQUIRED);
response.getHeaders().set("Content-type", "application/json; charset=utf-8");
try {
return response.writeWith(Mono.just(response.bufferFactory()
.wrap(objectMapper.writeValueAsBytes(
R.builder().msg(e.getMessage())
.code(Constant.BYTE_NO).build()))));
return response.writeWith(Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(R.builder().msg(e.getMessage()).code(Constant.BYTE_NO).build()))));
} catch (JsonProcessingException e1) {
log.error("对象输出异常", e1);
}
......@@ -114,20 +124,154 @@ public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory {
}
/**
* 检验验证码
* 连续输入错误3次,锁定10分钟
* 连续输入错误4次,锁定30分钟
* 连续输入错误5次,锁定2小时
*
* @param redisTemplate
*/
private void validCode(StringRedisTemplate redisTemplate, String phone, String code) throws ValidateCodeException {
// 是否锁定
if (isLocked(phone)) {
throw new ValidateCodeException(getLockoutMessage(phone));
}
// 登录成功,清除失败记录
if (ValidateCodeUtil.validateCode(redisTemplate, phone, code, Constant.BYTE_NO)) {
loginSucceeded(phone);
} else {
loginFailed(phone);
if (isLocked(phone)) {
throw new ValidateCodeException(getLockoutMessage(phone));
} else {
throw new ValidateCodeException("验证码不合法");
}
}
}
/**
* 当用户登录成功时调用此方法。
*
* @param phone 手机号
*/
public void loginSucceeded(String phone) {
String lockKey = getLoginFailLockKey(phone);
String countKey = getLoginFailCountKey(phone);
redisTemplate.delete(Arrays.asList(countKey, lockKey));
}
/**
* 当用户登录失败时调用此方法。
*
* @param phone 用户名
*/
public void loginFailed(String phone) {
String countKey = getLoginFailCountKey(phone);
String lockKey = getLoginFailLockKey(phone);
// 原子递增失败次数
Long attempts = redisTemplate.opsForValue().increment(countKey);
// 如果是第一次失败,记录失败次数,设置24H过期
if (attempts != null && attempts == 1) {
redisTemplate.expire(countKey, 24, TimeUnit.HOURS);
}
if (attempts != null) {
// 根据失败次数设置不同的锁定时间
if (attempts >= MAX_ATTEMPTS_TIER3) {
// 第5次及以上失败,锁定2小时
redisTemplate.opsForValue().set(lockKey, "locked", Duration.ofHours(LOCK_TIME_TIER3_HOURS));
} else if (attempts >= MAX_ATTEMPTS_TIER2) {
// 第4次失败,锁定30分钟
redisTemplate.opsForValue().set(lockKey, "locked", Duration.ofMinutes(LOCK_TIME_TIER2_MINUTES));
} else if (attempts >= MAX_ATTEMPTS_TIER1) {
// 第3次失败,锁定10分钟
redisTemplate.opsForValue().set(lockKey, "locked", Duration.ofMinutes(LOCK_TIME_TIER1_MINUTES));
}
}
}
/**
* 检查用户是否已被锁定。
*
* @param phone 手机号
* @return 如果被锁定,返回 true;否则返回 false。
*/
public boolean isLocked(String phone) {
String lockKey = getLoginFailLockKey(phone);
return redisTemplate.hasKey(lockKey);
}
/**
* 获取锁定解除时间的提示信息。
*
* @param phone 手机号
* @return 格式化的提示信息,例如:"您的账号已被锁定,请于 2025-08-11 10:30:00 后再试。"
*/
public String getLockoutMessage(String phone) {
String key = getLoginFailLockKey(phone);
// 获取key的剩余过期时间(TTL),单位为秒
long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl <= 0) {
return "";
}
// 计算确切的解锁时间点
LocalDateTime unlockTime = LocalDateTime.now().plusSeconds(ttl);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return String.format("您的账号因多次登录失败已被锁定,请于 %s 后再试。", unlockTime.format(formatter));
}
/**
* 获取登录失败锁定的key
* 超时过期
*
* @param phone 手机号
* @return
*/
private static String getLoginFailLockKey(String phone) {
return RedisCacheConstant.SICT_PHONE_CODE_KEY + ":" + Constant.BYTE_NO + ":" + phone + ":" + "login" + ":" + "fail" + ":" + "lock";
}
/**
* 获取登录失败次数的key,24H内有效
*
* @param phone 手机号
* @return
*/
private static String getLoginFailCountKey(String phone) {
return RedisCacheConstant.SICT_PHONE_CODE_KEY + ":" + Constant.BYTE_NO + ":" + phone + ":" + "login" + ":" + "fail" + ":" + "count";
}
/**
* /**
* 检查code
*
* @param request
*/
@SneakyThrows
private void checkCode(ServerHttpRequest request) {
// String code = request.getQueryParams().getFirst("code");
//
// if (StrUtil.isBlank(code)) {
// System.out.println("DEBUG: Code is blank");
// throw new ValidateCodeException("验证码不能为空");
// }
//
// String randomStr = request.getQueryParams().getFirst("randomStr");
// if (StrUtil.isBlank(randomStr)) {
// System.out.println("DEBUG: randomStr is blank, trying mobile");
// randomStr = request.getQueryParams().getFirst("mobile");
// }
// String key = CommonConstants.DEFAULT_CODE_KEY + randomStr;
// if (StrUtil.isBlank(randomStr)) {
// randomStr = request.getQueryParams().getFirst("mobile");
// }
......
......@@ -38,14 +38,13 @@ public class EncryptionUtil {
/**
* 获取解密返回流
*/
public static InputStream getDecodeInputStream(EncryptionRequest request) {
public static byte[] getDecodeInputStream(EncryptionRequest request) {
String encryptionKey = request.getEncryptionKey();
// rsa解密 获取aes秘钥
byte[] decryptKey = RSA.decrypt(encryptionKey, KeyType.PrivateKey);
// aes进行解密内容 获取输入流(方便整合springMVC)
AES aes = SecureUtil.aes(decryptKey);
byte[] decryptData = aes.decrypt(request.getEncryptionData());
return IoUtil.toStream(decryptData);
return aes.decrypt(request.getEncryptionData());
}
......
package cn.sh.stc.sict.theme.config;
import cn.hutool.core.io.IoUtil;
import cn.hutool.http.HtmlUtil;
import cn.sh.stc.sict.theme.common.crypto.EncryptionRequest;
import cn.sh.stc.sict.theme.common.crypto.EncryptionUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
......@@ -15,6 +17,10 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author gao
......@@ -50,7 +56,17 @@ public class DecodeRequestAdvice implements RequestBodyAdvice {
// 解密后的输入流
@Override
public InputStream getBody() {
return EncryptionUtil.getDecodeInputStream(encryptionRequest);
byte[] decodeReqBytes = EncryptionUtil.getDecodeInputStream(encryptionRequest);
// 解密请求体
String rbContentStr = new String(decodeReqBytes, StandardCharsets.UTF_8);
// XSS过滤
try {
Object rbContentObj = objectMapper.readValue(rbContentStr, Object.class);
return IoUtil.toStream(objectMapper.writeValueAsBytes(cleanJsonValue(rbContentObj)));
} catch (Exception e) {
// ignore
}
return IoUtil.toStream(decodeReqBytes);
}
};
}
......@@ -65,5 +81,35 @@ public class DecodeRequestAdvice implements RequestBodyAdvice {
return o;
}
}
/**
* XSS过滤
*
* @param value Map, List, String, 其他基础类型
* @return 清理后的值
*/
private Object cleanJsonValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof String) {
return HtmlUtil.escape((String) value);
} else if (value instanceof Map) {
// 如果是 Map,遍歷其所有 entry
Map<String, Object> map = (Map<String, Object>) value;
map.forEach((key, val) -> {
// 遞迴地清理它的值
map.put(key, cleanJsonValue(val));
});
return map;
} else if (value instanceof List) {
// 如果是 List,遍歷其所有元素
List<?> list = (List<?>) value;
// 建立一個新的 List 來存放清理後的元素
return list.stream()
.map(this::cleanJsonValue) // 對每個元素進行遞迴清理
.collect(Collectors.toList());
} else {
return value;
}
}
}
\ No newline at end of file
......@@ -3,26 +3,21 @@ package cn.sh.stc.sict.theme.hphy.controller.mp;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import cn.sh.stc.sict.cloud.common.core.constant.Constant;
import cn.sh.stc.sict.cloud.common.core.util.R;
import cn.sh.stc.sict.cloud.common.core.util.ValidateCodeUtil;
import cn.sh.stc.sict.theme.hpgp.model.HpgpDepartmentRank;
import cn.sh.stc.sict.theme.hpgp.service.HpgpDepartmentRankService;
import cn.sh.stc.sict.theme.hphy.service.HpDeptInfoService;
import cn.sh.stc.sict.theme.hphy.wd.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import cn.sh.stc.sict.theme.hphy.wd.NumSourceInfo;
import cn.sh.stc.sict.theme.hphy.wd.WanDaHttpUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author F_xh
......@@ -58,12 +53,13 @@ public class WDController {
return new R().error("手机号错误!");
}
// 是否重复发送
if(ValidateCodeUtil.hasPhoneKey(redisTemplate, phone, type)){
if (ValidateCodeUtil.hasPhoneKey(redisTemplate, phone, type)) {
return new R().success("短信已发送,请等待!");
}
String appName = "dx";
String code = RandomUtil.randomNumbers(6);
String msg = "您的验证码是:" + code;
WanDaHttpUtil.sendMsg(phone, msg, appName);
ValidateCodeUtil.saveCode(redisTemplate, phone, code, type);
return new R().success("短信已发送,请等待!");
......@@ -81,7 +77,7 @@ public class WDController {
// }
numSourceInfo.setOrderNumType(null);
List<NumSourceInfo> list = WanDaHttpUtil.getOrderNumInfo(numSourceInfo);
if(CollUtil.isEmpty(list)){
if (CollUtil.isEmpty(list)) {
return new R();
}
......@@ -114,7 +110,7 @@ public class WDController {
@ApiOperation("获取测压亭列表")
@GetMapping("/pressure/pavilion")
public R getPressureStationList(){
public R getPressureStationList() {
return new R();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment