饿了么CPS系统中Java后端防止SQL注入与XSS攻击的实战技巧

在饿了么CPS系统中,订单查询、用户信息管理、佣金报表导出等接口若未做安全防护,极易遭受SQL注入或跨站脚本(XSS)攻击。本文从参数校验、预编译SQL、输入过滤、输出转义四个维度,结合MyBatis、Spring拦截器与工具类,提供可落地的安全编码方案。

1. 强制使用预编译语句(PreparedStatement)

杜绝字符串拼接SQL:

// ❌ 反例:MyBatis中使用${}导致注入
@Select("SELECT * FROM cps_order WHERE user_id = ${userId}")
List<Order> unsafeSelect(@Param("userId") String userId);

// ✅ 正确:使用#{}
@Select("SELECT * FROM cps_order WHERE user_id = #{userId}")
List<Order> safeSelect(@Param("userId") String userId);

动态表名/字段名需白名单校验:

public List<Order> queryByDynamicColumn(String column, String value) {
    // 白名单校验
    Set<String> allowedColumns = Set.of("user_id", "merchant_id", "order_status");
    if (!allowedColumns.contains(column)) {
        throw new IllegalArgumentException("Invalid column name");
    }
    // 使用注解+XML避免${}
    return orderMapper.selectByColumn(column, value);
}

XML中安全写法:

<select id="selectByColumn" resultType="Order">
  SELECT * FROM cps_order
  WHERE
  <choose>
    <when test="column == 'user_id'">user_id = #{value}</when>
    <when test="column == 'merchant_id'">merchant_id = #{value}</when>
    <otherwise>1=0</otherwise>
  </choose>
</select>

2. 全局参数校验(Bean Validation + 自定义注解)

对ID、手机号等字段做格式约束:

package baodanbao.com.cn.cps.security;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeStringValidator.class)
public @interface SafeString {
    String message() default "Invalid characters";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class SafeStringValidator implements ConstraintValidator<SafeString, String> {
    private static final Pattern SAFE_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\-@.]+$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null || SAFE_PATTERN.matcher(value).matches();
    }
}

DTO使用:

public class OrderQueryDTO {
    @SafeString
    private String userId;

    @Pattern(regexp = "^M\\d{8,12}$")
    private String merchantId;
}

Controller启用校验:

@PostMapping("/orders")
public List<Order> queryOrders(@Valid @RequestBody OrderQueryDTO dto) {
    return orderService.query(dto);
}

在这里插入图片描述

3. XSS输入过滤(HTML标签清除)

在进入业务逻辑前清洗富文本:

@Component
public class XssRequestWrapper extends HttpServletRequestWrapper {

    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return cleanXss(value);
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) return null;
        return Arrays.stream(values).map(this::cleanXss).toArray(String[]::new);
    }

    private String cleanXss(String value) {
        if (value == null) return null;
        return Jsoup.clean(value, Safelist.none()).trim();
    }
}

// 拦截器注册
@WebFilter("/*")
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
    }
}

4. 输出转义(模板与JSON序列化)

对返回前端的数据进行HTML实体编码:

// 自定义Jackson序列化器
public class XssStringSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null) {
            String escaped = StringEscapeUtils.escapeHtml4(value);
            gen.writeString(escaped);
        } else {
            gen.writeNull();
        }
    }
}

// 应用于敏感字段
public class UserInfo {
    @JsonSerialize(using = XssStringSerializer.class)
    private String nickname;
    
    @JsonSerialize(using = XssStringSerializer.class)
    private String remark;
}

Thymeleaf默认已转义,但需禁用th:utext

<!-- 安全 -->
<span th:text="${user.remark}"></span>

<!-- 危险!禁止使用 -->
<span th:utext="${user.remark}"></span>

5. MyBatis拦截器防御二次注入

即使使用#{},若参数本身含恶意内容仍可能影响业务逻辑。通过拦截器统一清洗:

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class SqlSafeInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object parameter = invocation.getArgs()[1];
        sanitizeParameter(parameter);
        return invocation.proceed();
    }

    private void sanitizeParameter(Object param) {
        if (param instanceof Map) {
            ((Map<?, ?>) param).values().forEach(this::sanitizeValue);
        } else {
            sanitizeValue(param);
        }
    }

    private void sanitizeValue(Object value) {
        if (value instanceof String) {
            String str = (String) value;
            // 禁止包含SQL关键字(仅辅助,不能替代预编译)
            if (str.matches(".*(?i)(union|select|insert|update|delete|drop|exec|;|--).*")) {
                throw new SecurityException("Potential SQL injection detected");
            }
        }
    }
}

6. 日志脱敏防信息泄露

记录日志时自动脱敏敏感字段:

@Aspect
@Component
public class LogSanitizeAspect {

    @Before("@annotation(org.slf4j.Logger)")
    public void sanitizeLogArgs(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof String) {
                args[i] = ((String) args[i]).replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); // 手机号
            }
        }
    }
}

本文著作权归 俱美开放平台 ,转载请注明出处!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐