基于JavaParser与ASM的RuoYi框架白盒漏洞检测工具设计与实现
1. 项目缘起:为什么我们需要一个专属的RuoYi漏洞检测工具?
如果你是一名Java后端开发者,或者正在负责一个基于Spring Boot的Web项目安全,那么“RuoYi”这个名字你一定不陌生。它作为一个国内广泛使用的开源权限管理系统,以其清晰的架构、丰富的功能和活跃的社区,成为了许多团队快速搭建后台管理系统的首选脚手架。我自己的团队在过去几年里,也基于RuoYi-Vue版本衍生开发了不下五个内部系统。用起来确实顺手,开发效率提升明显。
但用得越多,心里那根安全的弦就绷得越紧。RuoYi作为一个功能完备的框架,集成了用户管理、角色权限、菜单管理、定时任务、数据监控等大量模块。代码量庞大,依赖复杂,这意味着潜在的安全攻击面也相应扩大。我们经历过几次小范围的安全扫描,结果让人警醒:一些常见的SQL注入风险点、未授权访问接口、甚至是框架历史版本中已知但未被修复的漏洞,都可能因为我们的疏忽或对框架机制理解不深而潜伏在系统中。更现实的问题是,市面上通用的Web漏洞扫描器(如AWVS、Nessus)虽然强大,但它们往往是“黑盒”的,针对的是泛化的Web应用漏洞,对RuoYi这种特定框架的代码结构、权限模型、数据流特点缺乏深度理解,扫描结果误报率高,且很难覆盖框架层面的设计缺陷和配置不当引发的安全问题。
于是,一个想法自然产生:能不能针对RuoYi框架,开发一个“白盒”或“灰盒”的专用漏洞检测工具?这个工具应该能理解RuoYi的目录结构、注解使用习惯(如 @PreAuthorize )、MyBatis的XML编写模式、Shiro或Spring Security的配置方式。它不需要像商业扫描器那样面面俱到,但要精准打击在RuoYi项目中最可能出现的几类高危漏洞。这不仅是提升现有项目安全性的有效手段,更能让我们在开发新功能时,建立起一套自动化的安全编码检查机制,将部分安全左移,防患于未然。
这个工具的目标用户很明确:就是像我一样的RuoYi项目开发者、架构师和运维安全人员。它不是一个要替代专业安全产品的庞然大物,而是一个能集成到CI/CD流水线、或者由开发者在提交代码前手动运行的“安全哨兵”。接下来,我就把自己从零开始构建这个工具的思路、核心技术和踩过的坑,完整地分享出来。
2. 工具整体设计与核心思路拆解
构建一个针对特定框架的检测工具,关键在于“理解”和“检测”两个环节。我们的工具命名为“RuoYi-Scanner”(以下简称Scanner),其核心架构围绕静态代码分析(SAST)和轻量级动态探针相结合的方式展开。
2.1 技术栈选型与理由
首先明确技术栈,这决定了工具的可用性和扩展性。
- 主语言:Java 8+ 。这是最自然的选择。我们的目标是检测Java项目,用Java来写工具,可以无缝使用Java Parser、ASM等字节码操作库,直接分析AST(抽象语法树)和字节码,避免跨语言调用的复杂性。同时,工具本身也可以打包成JAR,方便在任意有JRE的环境下运行。
- 静态分析引擎:JavaParser + ASM 。
- JavaParser :用于解析源代码(
.java文件)。它能将代码转换成一颗丰富的AST,我们可以方便地遍历这棵树,寻找特定的模式。例如,检查Controller中是否使用了@RequestMapping但未配合@PreAuthorize注解;分析MyBatis Mapper接口与XML中#{}和${}的使用是否正确,防止SQL注入。 - ASM :用于分析编译后的字节码(
.class文件)。有些信息在源码中可能不直观(如某些动态代理的实现、运行时依赖),或者我们只想对最终的交付物(JAR/WAR)进行扫描。ASM可以让我们在不执行代码的情况下,分析类、方法、字段及指令流,非常适合检测序列化漏洞、某些硬编码密钥等。
- JavaParser :用于解析源代码(
- 动态探针(可选):基于Java Agent的插桩 。对于某些需要在运行时才能判断的漏洞,如某些权限绕过逻辑,纯静态分析可能力有不逮。我们可以设计一个轻量级的Java Agent,在应用启动时植入一些探针,收集关键方法(如权限校验方法)的调用路径和参数,进行轻量级的动态验证。这部分属于进阶功能,初期可以聚焦静态分析。
- 依赖管理:Maven/Gradle 。工具本身作为一个Maven项目来管理,便于集成第三方库和打包发布。
- 报告输出:HTML + JSON 。HTML报告便于人工阅读,直观展示漏洞位置、等级、修复建议。JSON报告则便于集成到CI/CD系统(如Jenkins、GitLab CI)中,进行自动化质量门禁判断。
2.2 核心检测模型设计
Scanner的核心是一系列“检测规则”。每条规则都针对RuoYi中一类特定的安全隐患。我们将其设计为可插拔的,方便后续扩展。每条规则主要包含以下几个部分:
- 漏洞类型 :如SQL注入、未授权访问、硬编码密码、不安全的反序列化、XSS等。
- 检测目标 :明确是针对源码(
.java)、资源文件(.xml,.yml)还是字节码(.class)。 - 检测逻辑 :用代码描述如何发现该漏洞。这通常是一个“探测器”(Detector)类。
- 严重等级 :高、中、低,用于在报告中区分优先级。
- 修复建议 :提供具体的代码修改示例或配置调整方案。
工具的工作流程可以概括为:
- 输入 :指定一个RuoYi项目的根目录或打包好的JAR文件路径。
- 解析 :
- 如果是源码目录,使用JavaParser遍历所有
.java文件,构建AST森林;同时解析pom.xml/build.gradle、application.yml等配置文件。 - 如果是JAR文件,先解压(或直接读取),使用ASM框架遍历其中的
.class文件进行分析。
- 如果是源码目录,使用JavaParser遍历所有
- 应用规则 :将解析出的所有代码元素(类、方法、注解、表达式等)依次喂给所有已启用的检测规则。
- 收集结果 :每条规则独立判断,如果发现匹配的漏洞模式,则生成一个“发现项”(Finding),包含漏洞详情、位置、等级等。
- 生成报告 :将所有发现项汇总,按照严重等级排序,生成HTML和JSON格式的报告。
注意 :在设计之初就要考虑性能。一个中型RuoYi项目源码可能有上千个文件,遍历和解析需要时间。我们需要对文件解析过程做缓存(例如,同一个文件的AST只解析一次),并且让规则检测尽量是只读的、无状态的,便于并行处理。
3. 核心漏洞检测规则详解与实现
这里,我挑选几个在RuoYi项目中最具代表性、也最高频的漏洞类型,详细讲解其检测规则的实现思路和代码关键点。
3.1 SQL注入检测:聚焦MyBatis动态SQL
RuoYi默认使用MyBatis作为ORM框架。SQL注入的风险主要来源于MyBatis的XML映射文件中,使用了不安全的动态SQL拼接。
风险模式 :
- 在
<select>,<update>,<insert>,<delete>等标签的SQL语句中,使用${xxx}进行变量拼接。${}是直接文本替换,而#{}是预编译参数占位符。 - 在
<if>,<choose>,<foreach>等动态SQL标签内,虽然使用了#{},但拼接的字段名或表名来自用户输入。
检测规则实现(源码分析) : 我们的探测器需要扫描所有 *Mapper.xml 文件。使用一个XML解析器(如DOM4J或更轻量的Jsoup)加载文件,然后进行XPath或CSS选择器查询。
// 伪代码示例:检测 ${} 的使用
public class MyBatisSQLInjectionDetector implements Detector {
@Override
public List<Finding> detect(ProjectContext context) {
List<Finding> findings = new ArrayList<>();
// 获取项目中所有的 MyBatis Mapper XML 文件
List<File> xmlFiles = context.getFilesWithExtension(".xml");
for (File xmlFile : xmlFiles) {
if (!xmlFile.getName().endsWith("Mapper.xml")) continue;
Document doc = Jsoup.parse(xmlFile, "UTF-8");
// 查找所有包含 ${ 的文本节点(简化处理,实际需更精确匹配SQL语句部分)
Elements sqlTexts = doc.select("select, insert, update, delete, sql");
for (Element sqlElem : sqlTexts) {
String sql = sqlElem.text();
// 简单的正则匹配,实际应使用更严谨的语法分析避免误报
Pattern pattern = Pattern.compile("\\$\\{[^}]+\\}");
Matcher matcher = pattern.matcher(sql);
while (matcher.find()) {
String riskPart = matcher.group();
Finding finding = new Finding();
finding.setType("SQL_INJECTION");
finding.setSeverity(Severity.HIGH);
finding.setFile(xmlFile.getAbsolutePath());
// 需要更精准的行号定位,这里简化为文件路径
finding.setDescription("发现不安全的MyBatis SQL拼接: " + riskPart + "。应使用 #{} 预编译参数。");
finding.setRecommendation("将 `" + riskPart + "` 改为对应的 `#{...}` 形式。");
findings.add(finding);
}
}
}
return findings;
}
}
实操心得 :
- 简单的正则匹配
${会带来大量误报,比如在XML注释里。更可靠的方法是解析出SQL语句主体(去除注释),或者利用MyBatis自身的语法树解析器(如MyBatisX插件的思路)进行精准定位。 - 除了
${},还要检查<bind>标签的使用,确保其value属性不是直接拼接用户输入。 - 对于
ORDER BY、GROUP BY等动态排序场景,如果字段名来自前端,即使使用#{}也是不安全的,因为#{}会给参数加引号。这类场景需要提醒开发者进行字段名白名单校验。我们的规则可以尝试识别ORDER BY #{xxx}这种模式,并给出警告。
3.2 未授权访问检测:剖析Spring Security注解
RuoYi-Vue版本通常集成Spring Security。权限控制的核心是 @PreAuthorize 和 @PostAuthorize 注解。未授权访问往往因为注解缺失或配置错误。
风险模式 :
- Controller中的公开接口(使用
@RequestMapping,@GetMapping,@PostMapping等注解的方法)没有添加@PreAuthorize注解,或者注解中的SpEL表达式为空/过于宽松(如permitAll)。 - 在Security配置类(
SecurityConfig)中,configure(HttpSecurity http)方法里配置的URL匹配规则过于宽泛,将本应受保护的路径放行。
检测规则实现(AST分析) : 这里就需要用到JavaParser来解析Controller层的源码。
// 伪代码示例:检测缺失的 @PreAuthorize 注解
public class UnauthorizedAccessDetector implements Detector {
@Override
public List<Finding> detect(ProjectContext context) {
List<Finding> findings = new ArrayList<>();
// 获取所有Java源文件
List<CompilationUnit> compilationUnits = context.getJavaCompilationUnits();
for (CompilationUnit cu : compilationUnits) {
// 只关心Controller层的类
if (!cu.getClassByName().filter(c -> c.isAnnotationPresent("RestController") || c.isAnnotationPresent("Controller")).isPresent()) {
continue;
}
cu.accept(new VoidVisitorAdapter<Void>() {
@Override
public void visit(MethodDeclaration md, Void arg) {
super.visit(md, arg);
// 检查方法上是否有RequestMapping或其变体注解
boolean isMappingMethod = md.getAnnotations().stream()
.anyMatch(a -> a.getNameAsString().endsWith("Mapping"));
if (isMappingMethod) {
// 检查是否有权限注解
boolean hasSecurityAnnotation = md.getAnnotations().stream()
.anyMatch(a -> {
String name = a.getNameAsString();
return name.equals("PreAuthorize") || name.equals("PostAuthorize") || name.equals("Secured");
});
if (!hasSecurityAnnotation) {
// 检查该方法是否在配置中被忽略(例如,登录接口/login)
// 这里需要一个已知的公开接口白名单列表
if (!isInPublicWhitelist(md)) {
Finding finding = new Finding();
finding.setType("UNAUTHORIZED_ACCESS");
finding.setSeverity(Severity.HIGH);
finding.setFile(cu.getStorage().get().getPath().toAbsolutePath().toString());
finding.setLine(md.getRange().map(r -> r.begin.line).orElse(-1));
finding.setDescription("Controller方法 `" + md.getNameAsString() + "` 缺少权限控制注解(@PreAuthorize等)。");
finding.setRecommendation("根据业务需求,添加合适的 `@PreAuthorize(\"hasPermission('...')\")` 或 `@PreAuthorize(\"hasRole('...')\")` 注解。");
findings.add(finding);
}
} else {
// 如果有注解,进一步检查表达式内容是否过于宽松(如 `permitAll`)
checkSecurityAnnotationExpression(md, findings);
}
}
}
}, null);
}
return findings;
}
private boolean isInPublicWhitelist(MethodDeclaration md) {
// 实现逻辑:判断方法名和所属类是否在预定义的公开接口白名单内
// 例如:AuthController 中的 login, logout, captchaImage 等方法
// 可以通过配置文件加载白名单规则
return false;
}
private void checkSecurityAnnotationExpression(MethodDeclaration md, List<Finding> findings) {
// 解析 @PreAuthorize 注解的值,检查是否为 `permitAll` 或空字符串等不安全配置
// 略...
}
}
实操心得 :
- 白名单机制至关重要 。像登录、注销、获取验证码等接口本来就是公开的,不能误报。我们需要维护一个可配置的白名单列表,支持按类名+方法名、URL路径等多种方式匹配。
- 除了检查注解,还要结合
SecurityConfig的配置。有时开发者会在配置类里用http.authorizeRequests().antMatchers(...).permitAll()来放行一批接口,而在Controller上就没加注解。我们的工具需要能解析Security配置类,将配置的放行规则与Controller接口进行关联分析,这需要更复杂的跨文件数据流分析。 - 对于使用Shiro的老版本RuoYi,检测逻辑类似,但关注的注解变为
@RequiresPermissions,@RequiresRoles等。
3.3 敏感信息硬编码检测
在配置文件 application.yml 或 application.properties 中明文存储数据库密码、Redis密码、加密密钥等,是极其危险的做法。虽然RuoYi官方推荐使用 jasypt 进行加密,但实践中仍有很多项目直接硬编码。
检测规则实现(配置文件分析) : 这个规则相对直接,主要分析配置文件。
public class HardcodedSecretDetector implements Detector {
// 定义敏感关键词模式
private static final List<Pattern> SENSITIVE_PATTERNS = Arrays.asList(
Pattern.compile("(?i).*password.*"),
Pattern.compile("(?i).*secret.*"),
Pattern.compile("(?i).*key.*"),
Pattern.compile("(?i).*token.*"),
Pattern.compile("(?i).*jwt.*")
);
// 定义可能的安全值模式(如加密后的格式、环境变量引用)
private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("^ENC\\(.*\\)$"); // jasypt格式
private static final Pattern ENV_VAR_PATTERN = Pattern.compile("^\\$\\{.*\\}$|^\\$[A-Z_]+$");
@Override
public List<Finding> detect(ProjectContext context) {
List<Finding> findings = new ArrayList<>();
// 扫描yml和properties文件
List<File> configFiles = context.getFilesByName("application.yml", "application.properties", "bootstrap.yml");
for (File file : configFiles) {
List<String> lines = Files.readAllLines(file.toPath());
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();
if (line.isEmpty() || line.startsWith("#")) continue;
for (Pattern keyPattern : SENSITIVE_PATTERNS) {
if (keyPattern.matcher(line).find()) {
// 找到可能包含敏感信息的行,检查其值
String[] parts = line.split("[:=]", 2);
if (parts.length == 2) {
String value = parts[1].trim();
// 如果值不是加密格式,也不是环境变量引用,且不是空值,则报告
if (!isSafeValue(value)) {
Finding finding = new Finding();
finding.setType("HARDCODED_SECRET");
finding.setSeverity(Severity.CRITICAL); // 硬编码密码通常是严重级别
finding.setFile(file.getAbsolutePath());
finding.setLine(i + 1);
finding.setDescription("发现疑似硬编码的敏感信息: " + line);
finding.setRecommendation("请勿在配置文件中明文存储密码或密钥。建议使用Jasypt加密,或使用环境变量、配置中心管理。例如:`password: ENC(加密后的字符串)` 或 `password: ${DB_PASSWORD}`。");
findings.add(finding);
}
}
}
}
}
}
return findings;
}
private boolean isSafeValue(String value) {
if (value.isEmpty() || "null".equalsIgnoreCase(value)) {
return true; // 空值或null不算硬编码
}
// 检查是否为加密格式或环境变量
return ENCRYPTED_PATTERN.matcher(value).matches() || ENV_VAR_PATTERN.matcher(value).matches();
}
}
实操心得 :
- 误报控制:像
spring.datasource.url里可能包含“password”这个词,但它是URL的一部分。我们的正则需要更精细,最好能匹配到password:或.password=这样的键名模式。 - 进阶检测:除了配置文件,还要扫描源代码中是否在字符串字面量里硬编码了密钥、API Token等。这需要结合AST分析,查找
String类型的字面量赋值,并匹配敏感数据模式(如Base64编码特征、JWT格式等)。 - 建议整合:在报告中,可以附上RuoYi官方推荐的
jasypt集成步骤链接,提供一键修复的指导。
4. 工具工程化:从脚本到可交付产品
写几个独立的检测类不难,但要让它成为一个真正好用、可维护的工具,还需要做大量的工程化工作。
4.1 项目结构与模块划分
一个清晰的项目结构有助于长期维护和扩展。
ruoyi-scanner/
├── pom.xml
├── src/main/java/com/ruoyi/scanner/
│ ├── core/
│ │ ├── ScannerEngine.java // 扫描引擎,调度所有探测器
│ │ ├── ProjectContext.java // 项目上下文,封装被扫描项目的信息
│ │ └── Finding.java // 漏洞发现项实体类
│ ├── detector/ // 所有检测规则实现
│ │ ├── Detector.java // 检测器接口
│ │ ├── impl/
│ │ │ ├── MyBatisSQLInjectionDetector.java
│ │ │ ├── UnauthorizedAccessDetector.java
│ │ │ └── HardcodedSecretDetector.java
│ │ └── ...
│ ├── parser/ // 各种解析器
│ │ ├── JavaSourceParser.java // 基于JavaParser
│ │ ├── BytecodeAnalyzer.java // 基于ASM
│ │ └── ConfigFileParser.java
│ ├── reporter/ // 报告生成器
│ │ ├── HtmlReporter.java
│ │ └── JsonReporter.java
│ └── cli/ // 命令行入口
│ └── Main.java
├── src/main/resources/
│ ├── whitelist.yml // 公开接口白名单配置
│ └── template/
│ └── report.html.vm // HTML报告模板(使用Velocity或Freemarker)
└── README.md
4.2 命令行接口(CLI)设计
工具最终要通过命令行调用。一个友好的CLI可以大大提升易用性。
// Main.java 示例
public class Main {
public static void main(String[] args) {
Options options = new Options();
options.addOption("p", "project", true, "RuoYi项目根目录路径 (必填)");
options.addOption("o", "output", true, "报告输出目录,默认 ./scanner-report");
options.addOption("f", "format", true, "报告格式,支持 html,json,all,默认 all");
options.addOption("r", "rules", true, "指定启用的规则集,默认启用全部");
options.addOption("h", "help", false, "显示帮助信息");
CommandLineParser parser = new DefaultParser();
try {
CommandLine cmd = parser.parse(options, args);
if (cmd.hasOption("h")) {
printHelp(options);
return;
}
if (!cmd.hasOption("p")) {
System.err.println("错误:必须指定项目路径 (-p)");
printHelp(options);
System.exit(1);
}
String projectPath = cmd.getOptionValue("p");
String outputDir = cmd.getOptionValue("o", "./scanner-report");
String format = cmd.getOptionValue("f", "all");
// 初始化扫描引擎
ScannerEngine engine = new ScannerEngine();
// 加载项目
ProjectContext context = ProjectContext.load(projectPath);
// 执行扫描
List<Finding> findings = engine.scan(context);
// 生成报告
ReportGenerator.generate(findings, outputDir, format);
System.out.println("扫描完成!报告已生成至: " + new File(outputDir).getAbsolutePath());
System.out.println("共发现 " + findings.size() + " 个问题。");
} catch (ParseException e) {
System.err.println("参数解析失败: " + e.getMessage());
printHelp(options);
System.exit(1);
} catch (Exception e) {
System.err.println("扫描过程发生错误: ");
e.printStackTrace();
System.exit(1);
}
}
}
使用方式:
java -jar ruoyi-scanner.jar -p /path/to/your/ruoyi-project -o ./my-report
4.3 报告生成与可读性优化
报告是工具价值的直接体现。HTML报告应该清晰、美观、可交互。
HTML报告核心要素 :
- 概览仪表盘 :显示扫描统计信息(总文件数、检测规则数、发现问题按等级分布饼图)。
- 问题列表 :表格形式列出所有发现,列包括:严重等级(用红/黄/蓝图标)、漏洞类型、文件位置、行号(可点击跳转)、问题描述、修复建议。支持按等级、类型、文件排序。
- 详情面板 :点击列表中的条目,可以展开查看更详细的信息,例如出错的代码片段(高亮显示风险行)、完整的修复建议示例代码。
- 导出功能 :提供一键导出为PDF或JSON。
我们可以使用模板引擎(如Velocity)来生成这样的HTML。JSON报告则更简单,将 List<Finding> 序列化即可,便于Jenkins等CI工具通过插件解析并判断构建是否通过(例如,存在CRITICAL级别漏洞则失败)。
5. 集成与进阶:让工具融入开发流程
工具做出来不是终点,用起来才是。
5.1 集成到Maven/Gradle构建流程
我们可以将Scanner打包成一个Maven插件,让开发者只需在 pom.xml 中简单配置,就能在 mvn verify 阶段自动执行安全扫描。
<!-- 在项目的pom.xml中 -->
<build>
<plugins>
<plugin>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-scanner-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals>
<goal>scan</goal>
</goals>
<phase>verify</phase> <!-- 在集成测试阶段执行 -->
</execution>
</executions>
<configuration>
<outputDirectory>${project.build.directory}/security-report</outputDirectory>
<failOnHighSeverity>true</failOnHighSeverity> <!-- 发现高危漏洞则构建失败 -->
</configuration>
</plugin>
</plugins>
</build>
这样,每次代码提交或合并请求触发CI构建时,安全扫描就会自动执行,并将报告作为构建产物的一部分。
5.2 进阶检测规则设想
基础规则覆盖常见漏洞后,可以考虑更复杂的场景:
- 依赖组件漏洞检测 :解析
pom.xml,获取所有依赖库及其版本,与已知的漏洞库(如NVD、国产化组件漏洞库)进行比对,报告存在已知高危漏洞的组件。这可以集成OWASP Dependency-Check的能力。 - 不安全的反序列化 :使用ASM分析字节码,查找实现了
Serializable接口且重写了readObject/readResolve等方法的类,检查其中是否直接调用了ObjectInputStream.readObject()而没有进行输入验证。同时,检查是否使用了已知不安全的反序列化库(如commons-collections的老版本)。 - 日志敏感信息泄露 :扫描代码中对日志框架(SLF4J、Log4j2)的调用,检查是否有将用户密码、身份证号、手机号等敏感信息直接打印到日志中。这需要结合数据流分析,判断传入日志方法的参数是否来源于用户输入或数据库敏感字段。
5.3 实际使用中的注意事项与调优
- 性能调优 :首次扫描一个大型项目可能会比较慢(几十秒到几分钟)。可以通过缓存解析结果(如将AST序列化到临时文件)、并行执行独立检测规则来优化。对于CI/CD场景,可以考虑增量扫描,只分析变更的文件。
- 误报处理 :安全工具难免误报。除了不断优化规则逻辑,提供一个“忽略列表”(
.scanner-ignore)功能非常必要。允许开发者在项目根目录放置一个配置文件,列出需要被本工具忽略的特定问题(通过问题ID或文件路径正则),避免反复报告已知的、可接受的风险。 - 规则更新 :安全威胁在变化,规则也需要更新。可以考虑让工具支持从远程规则库(一个简单的HTTP服务或Git仓库)定期拉取最新的检测规则,实现规则的动态更新。
- 与IDE集成 :开发一个IntelliJ IDEA或Eclipse插件,让开发者在编码时就能实时看到安全警告,体验最好。这需要将检测引擎封装成语言服务器(Language Server)或直接利用IDE的AST。
构建这样一个工具的过程,本身就是对RuoYi框架和安全开发实践的一次深度复盘。它强迫你去仔细阅读框架源码,理解每一处可能被滥用的设计,思考如何在自动化与准确性之间取得平衡。最终产出的不仅是一个工具,更是一份针对RuoYi项目的安全开发最佳实践清单。当你和你的团队开始习惯在代码提交前运行一遍这个扫描器,那些曾经容易被忽略的安全隐患,就会在萌芽阶段被有效遏制。
更多推荐


所有评论(0)