修复漏洞
This commit is contained in:
1 parent
710e4cfb90
commit
bb0b8bd7ef
14 files changed
+777
-161
No files matched your search
+57
@@ -0,0 +1,57 @@
|
||||
package com.bsd.pjcloud.common.core.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/**
|
||||
* 弱口令校验:禁止密码与手机号/用户名相同等低强度规则。
|
||||
*/
|
||||
public final class PasswordWeakCheckUtil {
|
||||
|
||||
private PasswordWeakCheckUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rawPassword 明文密码
|
||||
* @param phone 手机号(可空)
|
||||
* @param username 用户名(可空)
|
||||
* @return 不通过时的中文原因;通过返回 null
|
||||
*/
|
||||
public static String validate(String rawPassword, String phone, String username) {
|
||||
if (StrUtil.isBlank(rawPassword)) {
|
||||
return "密码不能为空";
|
||||
}
|
||||
String pwd = StrUtil.trim(rawPassword);
|
||||
String phoneTrim = StrUtil.trim(phone);
|
||||
String userTrim = StrUtil.trim(username);
|
||||
if (StrUtil.isNotBlank(phoneTrim) && pwd.equals(phoneTrim)) {
|
||||
return "密码不能与手机号相同";
|
||||
}
|
||||
if (StrUtil.isNotBlank(userTrim) && pwd.equalsIgnoreCase(userTrim)) {
|
||||
return "密码不能与用户名相同";
|
||||
}
|
||||
if (pwd.length() < 8) {
|
||||
return "密码长度不能少于8位";
|
||||
}
|
||||
boolean hasLetter = false;
|
||||
boolean hasDigit = false;
|
||||
for (int i = 0; i < pwd.length(); i++) {
|
||||
char c = pwd.charAt(i);
|
||||
if (Character.isLetter(c)) {
|
||||
hasLetter = true;
|
||||
} else if (Character.isDigit(c)) {
|
||||
hasDigit = true;
|
||||
}
|
||||
}
|
||||
if (!hasLetter || !hasDigit) {
|
||||
return "密码必须同时包含字母和数字";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void assertStrong(String rawPassword, String phone, String username) {
|
||||
String err = validate(rawPassword, phone, username);
|
||||
if (err != null) {
|
||||
throw new IllegalArgumentException(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25
|
||||
sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
|
||||
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7
|
||||
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u89C1\u7CFB\u7EDF\u53C2\u6570USER_DEFAULT_PASSWORD
|
||||
sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
|
||||
sys.user.phone.existing={0} \u624b\u673a\u53f7\u5df2\u5b58\u5728
|
||||
sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
|
||||
|
||||
+84
-53
@@ -35,76 +35,107 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author pjcloud
|
||||
* @date 2019-06-24
|
||||
* <p>
|
||||
* 解决Mybatis Plus Order By SQL注入问题
|
||||
* 解决Mybatis Plus Order By SQL注入问题:ascs/descs 仅允许简单列名白名单,禁止表达式/函数注入。
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
/**
|
||||
* 排序字段白名单:仅允许标识符(可选表别名.列名),禁止空格、括号、逗号、注释等。
|
||||
* 例:create_time、user_id、u.createTime
|
||||
*/
|
||||
private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile(
|
||||
"^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$");
|
||||
|
||||
/**
|
||||
* 判断Controller是否包含page 参数
|
||||
*
|
||||
* @param parameter 参数
|
||||
* @return 是否过滤
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType().equals(Page.class);
|
||||
}
|
||||
/** 单次请求最多允许的排序列数量,防止恶意堆叠。 */
|
||||
private static final int MAX_ORDER_COLUMNS = 5;
|
||||
|
||||
/**
|
||||
* @param parameter 入参集合
|
||||
* @param mavContainer model 和 view
|
||||
* @param webRequest web相关
|
||||
* @param binderFactory 入参解析
|
||||
* @return 检查后新的page对象
|
||||
* <p>
|
||||
* page 只支持查询 GET .如需解析POST获取请求报文体处理
|
||||
*/
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
|
||||
/**
|
||||
* 判断Controller是否包含page 参数
|
||||
*
|
||||
* @param parameter 参数
|
||||
* @return 是否过滤
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType().equals(Page.class);
|
||||
}
|
||||
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
/**
|
||||
* @param parameter 入参集合
|
||||
* @param mavContainer model 和 view
|
||||
* @param webRequest web相关
|
||||
* @param binderFactory 入参解析
|
||||
* @return 检查后新的page对象
|
||||
* <p>
|
||||
* page 只支持查询 GET .如需解析POST获取请求报文体处理
|
||||
*/
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
|
||||
|
||||
String ascs = request.getParameter("ascs");
|
||||
String descs = request.getParameter("descs");
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
|
||||
String current = request.getParameter("current");
|
||||
String size = request.getParameter("size");
|
||||
String ascs = request.getParameter("ascs");
|
||||
String descs = request.getParameter("descs");
|
||||
|
||||
Page page = new Page();
|
||||
if (StrUtil.isNotBlank(current)) {
|
||||
// 如果current page 小于零 视为不合法数据
|
||||
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
|
||||
throw new CheckedException("current page error");
|
||||
}
|
||||
page.setCurrent(Long.parseLong(current));
|
||||
}
|
||||
String current = request.getParameter("current");
|
||||
String size = request.getParameter("size");
|
||||
|
||||
if (StrUtil.isNotBlank(size)) {
|
||||
page.setSize(Long.parseLong(size));
|
||||
}
|
||||
Page page = new Page();
|
||||
if (StrUtil.isNotBlank(current)) {
|
||||
// 如果current page 小于零 视为不合法数据
|
||||
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
|
||||
throw new CheckedException("current page error");
|
||||
}
|
||||
page.setCurrent(Long.parseLong(current));
|
||||
}
|
||||
|
||||
List<OrderItem> orderItemList = new ArrayList<>();
|
||||
Optional.ofNullable(ascs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.filter(asc -> !SqlInjectionUtils.check(asc))
|
||||
.map(OrderItem::asc)
|
||||
.collect(Collectors.toList())));
|
||||
Optional.ofNullable(descs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.filter(desc -> !SqlInjectionUtils.check(desc))
|
||||
.map(OrderItem::desc)
|
||||
.collect(Collectors.toList())));
|
||||
page.addOrder(orderItemList);
|
||||
if (StrUtil.isNotBlank(size)) {
|
||||
page.setSize(Long.parseLong(size));
|
||||
}
|
||||
|
||||
List<OrderItem> orderItemList = new ArrayList<>();
|
||||
Optional.ofNullable(ascs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(col -> toSafeOrderColumn(col, true))
|
||||
.collect(Collectors.toList())));
|
||||
Optional.ofNullable(descs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(col -> toSafeOrderColumn(col, false))
|
||||
.collect(Collectors.toList())));
|
||||
if (orderItemList.size() > MAX_ORDER_COLUMNS) {
|
||||
throw new CheckedException("排序字段数量超限");
|
||||
}
|
||||
page.addOrder(orderItemList);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端排序字段转为安全的 OrderItem:必须通过标识符白名单,且通过 SqlInjectionUtils 黑名单兜底。
|
||||
*/
|
||||
private static OrderItem toSafeOrderColumn(String column, boolean asc) {
|
||||
if (!SAFE_ORDER_COLUMN.matcher(column).matches()) {
|
||||
log.warn("拒绝非法排序字段: {}", column);
|
||||
throw new CheckedException("排序字段不合法");
|
||||
}
|
||||
if (SqlInjectionUtils.check(column)) {
|
||||
log.warn("排序字段命中注入检测: {}", column);
|
||||
throw new CheckedException("排序字段不合法");
|
||||
}
|
||||
return asc ? OrderItem.asc(column) : OrderItem.desc(column);
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
}
|
||||
+131
-95
@@ -18,8 +18,10 @@
|
||||
package com.bsd.pjcloud.common.sentinel.handle;
|
||||
|
||||
import com.alibaba.csp.sentinel.Tracer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.common.core.exception.ServiceException;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -40,111 +42,145 @@ import java.util.List;
|
||||
@RestControllerAdvice
|
||||
public class GlobalBizExceptionHandler {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 全局异常.
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(Exception e) {
|
||||
/**
|
||||
* 业务可控异常:允许把明确的业务文案返回前端。
|
||||
*/
|
||||
@ExceptionHandler({CheckedException.class, ServiceException.class, IllegalArgumentException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBizException(RuntimeException e) {
|
||||
log.warn("业务异常 ex={}", e.getMessage());
|
||||
return R.failed(e.getMessage());
|
||||
}
|
||||
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
/**
|
||||
* SQLException:禁止把完整 SQL / 表结构回显给前端。
|
||||
*/
|
||||
@ExceptionHandler({SQLException.class})
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleSQLException(SQLException exception) {
|
||||
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
/**
|
||||
* 全局异常:默认不回显底层异常原文(MyBatis 常把 SQL、mapper 路径塞进 message)。
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(Exception e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
Tracer.trace(e);
|
||||
if (isDataAccessRelated(e)) {
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
return R.failed("系统繁忙,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLException Exception
|
||||
*
|
||||
* @param exception 数据库调用异常
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({SQLException.class})
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleSQLException(SQLException exception) {
|
||||
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
/**
|
||||
* 识别 Spring DAO / MyBatis / JDBC 相关异常(本模块不直接依赖 mybatis,用类名与特征串判断)。
|
||||
*/
|
||||
private static boolean isDataAccessRelated(Throwable e) {
|
||||
Throwable t = e;
|
||||
while (t != null) {
|
||||
if (t instanceof SQLException) {
|
||||
return true;
|
||||
}
|
||||
String name = t.getClass().getName();
|
||||
if (name.startsWith("org.springframework.dao.")
|
||||
|| name.startsWith("org.springframework.jdbc.")
|
||||
|| name.contains("MyBatis")
|
||||
|| name.contains("PersistenceException")
|
||||
|| name.contains("BadSqlGrammar")) {
|
||||
return true;
|
||||
}
|
||||
String msg = t.getMessage();
|
||||
if (msg != null && (msg.contains("### Error") || msg.contains("### SQL")
|
||||
|| msg.contains("SQL:") || msg.contains("Cause: java.sql")
|
||||
|| msg.contains(" bad SQL grammar"))) {
|
||||
return true;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ExceptionHandler(FeignException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(FeignException e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
@ExceptionHandler(FeignException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(FeignException e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
|
||||
if (e.responseBody().isPresent()) {
|
||||
try {
|
||||
return objectMapper.readValue(e.responseBody().get().array(), R.class);
|
||||
} catch (Exception parseEx) {
|
||||
// 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
|
||||
log.warn("Feign 错误响应非 JSON,status={}", e.status());
|
||||
return R.failed("下游服务异常,请稍后重试");
|
||||
}
|
||||
}
|
||||
if (e.responseBody().isPresent()) {
|
||||
try {
|
||||
return objectMapper.readValue(e.responseBody().get().array(), R.class);
|
||||
} catch (Exception parseEx) {
|
||||
// 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
|
||||
log.warn("Feign 错误响应非 JSON,status={}", e.status());
|
||||
return R.failed("下游服务异常,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
return R.failed("服务调用失败,请稍后重试");
|
||||
}
|
||||
return R.failed("服务调用失败,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* AccessDeniedException
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public R handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("拒绝授权异常信息 ex={}", e.getMessage());
|
||||
return R.failed("权限不足,不允许访问");
|
||||
}
|
||||
/**
|
||||
* AccessDeniedException
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public R handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("拒绝授权异常信息 ex={}", e.getMessage());
|
||||
return R.failed("权限不足,不允许访问");
|
||||
}
|
||||
|
||||
/**
|
||||
* validation Exception
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({BindException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBodyValidException(BindException exception) {
|
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
|
||||
// 插入log 的逻辑
|
||||
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
|
||||
}
|
||||
/**
|
||||
* validation Exception
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({BindException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBodyValidException(BindException exception) {
|
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
|
||||
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
|
||||
*
|
||||
* @return R
|
||||
*/
|
||||
@DeleteMapping("/error")
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noHandlerFoundException() {
|
||||
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
|
||||
}
|
||||
/**
|
||||
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
|
||||
*
|
||||
* @return R
|
||||
*/
|
||||
@DeleteMapping("/error")
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noHandlerFoundException() {
|
||||
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持和低版本请求路径不存在的行为一致
|
||||
* <p>
|
||||
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
|
||||
* 3.2.0] 404 Not Found behavior #38733</a>
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({NoResourceFoundException.class})
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noResourceFoundException(NoResourceFoundException exception) {
|
||||
log.debug("请求路径 404 {}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
/**
|
||||
* 保持和低版本请求路径不存在的行为一致
|
||||
* <p>
|
||||
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
|
||||
* 3.2.0] 404 Not Found behavior #38733</a>
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({NoResourceFoundException.class})
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noResourceFoundException(NoResourceFoundException exception) {
|
||||
log.debug("请求路径 404 {}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user