为什么个人站的代码也需要「体检」
很多人觉得代码质量工具是大厂才玩的东西,个人站长写的几个 PHP 文件、一个 WordPress 主题,能跑就行了。但实际情况是:个人站的代码量一点都不小——主题 functions.php 动辄上千行,自研的插件、采集脚本、定时任务脚本零散分布在服务器各处,改了一年之后自己都记不清哪个函数是干嘛的。等到某天网站突然报 500,或者被扫出个 SQL 注入漏洞,回头翻代码才发现一堆「当时图快」埋下的雷。
静态代码分析就是解决这个问题的:不用运行代码,工具直接读源码,自动找出潜在 bug、安全漏洞、代码异味(code smell)和重复代码。对个人站长来说,它的价值有三个:
- 防安全漏洞:SQL 拼接、未过滤的输出、硬编码密码这类问题,静态分析一扫就出来,比被黑客发现强一万倍。
- 防低级 bug:空指针、未定义变量、永远为真的判断条件——这些在 PHP 这种弱类型语言里特别常见。
- 让代码能长期维护:个人站是要跑很多年的,代码可读性直接决定你三年后还改不改得动。
而 SonarQube 是目前开源生态里最完整的代码质量平台:支持 30 多种语言、有 Web 界面、有「质量门禁」(Quality Gate)概念,能在 CI 里当卡点。这篇文章我从零把它跑起来,讲清楚扫描结果怎么看,最后接到 Git 钩子上,让每次提交代码都自动体检。
先搞懂:静态分析和「测试」不是一回事
很多新手会把 SonarQube 和单元测试搞混,这里必须分开:
- 单元测试验证「代码运行结果对不对」。它需要你写测试用例,跑起来才有结论,覆盖的是你想到的场景。
- 静态分析验证「代码写法有没有问题」。它不运行代码,纯靠语法树和数据流分析,覆盖的是所有代码,包括你没想到的场景。
两者的关系是互补的:测试保证功能正确,静态分析保证代码健康。SonarQube 属于后者。它不需要你写任何测试,装好就能对现有代码库开扫。
SonarQube 扫描后会给代码打几个维度的分:
- Reliability(可靠性):有没有会导致运行出错的 bug。
- Security(安全性):有没有可被利用的漏洞,以及需要复查的「安全热点」。
- Maintainability(可维护性):代码异味数量,换算成「修复需要多少天」。
- Coverage(覆盖率):需要你上报测试覆盖数据才有,没有测试就是 0。
- Duplications(重复率):复制粘贴的代码占比。
用 Docker 部署 SonarQube
SonarQube 官方要求生产环境至少 2G 内存,而且它对 Linux 内核参数有硬性要求(因为内部用了 Elasticsearch)。先做系统准备:
# SonarQube 内置 Elasticsearch 需要这些内核参数,否则启动会失败
sudo sysctl -w vm.max_map_count=524288
sudo sysctl -w fs.file-max=131072
# 持久化到配置,防止重启丢失
echo "vm.max_map_count=524288" | sudo tee -a /etc/sysctl.conf
echo "fs.file-max=131072" | sudo tee -a /etc/sysctl.conf
然后建一个 PostgreSQL 作为 SonarQube 的数据库。SonarQube 内嵌的 H2 数据库只能用于试用,正式用必须换 PostgreSQL(它的新版本已经不再免费支持 MySQL):
version: "3.8"
services:
sonar-db:
image: postgres:15-alpine
container_name: sonar-db
restart: unless-stopped
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: change_me_strong_password
POSTGRES_DB: sonar
volumes:
- ./pgdata:/var/lib/postgresql/data
# 数据库不需要对外暴露,只在容器网络内可见
expose:
- "5432"
sonarqube:
image: sonarqube:10.6-community
container_name: sonarqube
restart: unless-stopped
depends_on:
- sonar-db
environment:
SONAR_JDBC_URL: jdbc:postgresql://sonar-db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: change_me_strong_password
# JVM 堆别给太大,小机器容易 OOM;2G 以内够个人用
SONAR_SEARCH_JAVAOPTS: "-Xms512m -Xmx512m"
ports:
# 只绑回环,通过 Nginx 反代或 SSH 隧道访问,绝不裸奔公网
- "127.0.0.1:9000:9000"
volumes:
- ./sonar_data:/opt/sonarqube/data
- ./sonar_extensions:/opt/sonarqube/extensions
- ./sonar_logs:/opt/sonarqube/logs
ulimits:
nofile:
soft: 65536
hard: 65536
docker compose up -d
docker logs -f sonarqube
# 等到日志出现 "SonarQube is operational" 才算起来
curl -s http://127.0.0.1:9000/api/system/status
启动后浏览器访问(本地的话用 SSH 隧道把 9000 端口转发出来):
ssh -L 9000:127.0.0.1:9000 you@your-server
# 然后本机浏览器开 http://localhost:9000
默认账号密码是 admin / admin,第一次登录会强制改密码。三件事必须马上做:9000 端口不要暴露公网、改掉默认密码、把「强制认证」打开(Administration → Security → Force user authentication),否则任何人都能看到你的代码,甚至改配置。
用 SonarScanner 扫第一份代码
SonarQube 服务器只负责存结果和给界面,真正干活的是 SonarScanner(扫描器),它跑在你的代码所在机器上,把源码分析完再把报告发给服务器。
先在服务器后台建项目拿一个 token:
- Projects → Create Project → Manually,起个项目名(比如
my-site)。 - 提示生成 token 时复制保存好(只显示一次),比如
sqa_xxxxxxxxxxxx。
装扫描器(可以装在开发机,也可以装在服务器上扫部署目录):
# 下载 scanner 命令行工具
curl -L https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-6.2.1.4610-linux-x64.zip -o /tmp/scanner.zip
sudo unzip /tmp/scanner.zip -d /opt/
sudo ln -s /opt/sonar-scanner-6.2.1.4610-linux-x64/bin/sonar-scanner /usr/local/bin/sonar-scanner
sonar-scanner --version
在项目根目录放一个 sonar-project.properties:
sonar.projectKey=my-site
sonar.projectName=My Personal Site
sonar.projectVersion=1.0
# 源码目录
sonar.sources=.
# 排除第三方库、缓存、上传目录这些不该扫的
sonar.exclusions=vendor/**,node_modules/**,uploads/**,cache/**,wp-includes/**,wp-admin/**
# 语言与编码(中文项目必须显式指定,否则中文注释会报错)
sonar.sourceEncoding=UTF-8
# 服务器地址与 token
sonar.host.url=http://127.0.0.1:9000
sonar.token=sqa_xxxxxxxxxxxx
# PHP 项目建议关掉「重复代码」对短文件的误报
sonar.cpd.minimumTokens=100
执行扫描:
cd /var/www/my-site
sonar-scanner
# 扫完会打印一个链接,打开就是结果页面
sonar.token 直接写文件里不安全,更好的做法是用环境变量:
export SONAR_TOKEN=sqa_xxxxxxxxxxxx
sonar-scanner -Dsonar.token=$SONAR_TOKEN
读懂扫描结果:三个核心指标
第一次扫出来的数字通常很难看,别慌,先学会看重点。
Bugs(可靠性)
SonarQube 判定「一定会出问题」的代码。常见的有:变量用了没定义、条件永远为真/假、除法可能除零、资源打开后没关闭。这类问题优先级最高,看到就改。
Vulnerabilities 和 Security Hotspots
这两个要分清:
- Vulnerability(漏洞):确定的、可被利用的问题,比如硬编码的数据库密码、拼接进 SQL 的未过滤参数、危险的 eval 调用。
- Security Hotspot(安全热点):需要「人工复查」的可疑点,比如用了 MD5 做哈希、关闭了 SSL 校验。不一定错,但必须有人看一眼确认。
Code Smells(代码异味)
不影响运行,但影响可维护性:函数太长、嵌套太深、重复代码、命名不规范、注释掉的死代码。SonarQube 会把这些换算成「技术债务」,比如「预计 3 天修复」。个人项目不用追求清零,但应该控制增长趋势。
在界面上点 Issues 标签,可以按严重程度(Blocker / Critical / Major / Minor / Info)过滤。我的建议是:先把 Blocker 和 Critical 全部处理掉,Major 挑顺手改,Minor 可以放着。
质量门禁:让 CI 自动卡住有问题的代码
SonarQube 最实用的功能是 Quality Gate(质量门禁)——一条预设的「通过线」。扫描后如果代码不达标,CI 直接失败,不让它合并上线。
默认的「Sonar way」门禁规则大致是:
- 新代码 0 个 Bug。
- 新代码 0 个漏洞。
- 新代码安全热点全部复查过。
- 新代码覆盖率 ≥ 80%(没有测试的项目可调整)。
- 新代码重复率 ≤ 3%。
注意关键词是「新代码」。这就是所谓的 Clean as You Code 策略:不去追着还历史旧账,只保证「从现在开始新增的代码是干净的」。对个人站长极其友好——你可以先容忍老代码的一堆问题,但每写新代码都被门禁管住,技术债不会继续膨胀。
接到 Git 钩子里,提交前自动扫:
# .git/hooks/pre-push (记得 chmod +x)
#!/bin/bash
cd /var/www/my-site || exit 1
sonar-scanner -Dsonar.token="$SONAR_TOKEN" > /tmp/sonar_scan.log 2>&1
# 取本次分析的任务 ID,轮询门禁结果
TASK_ID=$(grep -o 'task?id=[A-Za-z0-9_-]*' /tmp/sonar_scan.log | head -1 | cut -d= -f2)
if [ -z "$TASK_ID" ]; then
echo "扫描没拿到 task id,跳过门禁检查"
exit 0
fi
sleep 5
STATUS=$(curl -s -u "$SONAR_TOKEN:" \
"http://127.0.0.1:9000/api/qualitygates/project_status?analysisId=$TASK_ID" \
| grep -o '"status":"[A-Z]*"' | head -1 | cut -d'"' -f4)
if [ "$STATUS" != "OK" ]; then
echo "❌ 质量门禁未通过($STATUS),请到 SonarQube 查看详情"
exit 1
fi
echo "✅ 质量门禁通过"
如果是用 GitLab CI 或 Jenkins,有官方插件,直接在流水线里加一步 sonar-scanner 再调 waitForQualityGate 就行,不用自己写脚本。
常见语言的重点规则
不同语言的坑不一样,这里列几个个人站最常写到的:
- PHP:SQL 拼接(应该是参数化查询)、
echo未转义的用户输入(XSS)、md5/sha1用于密码、extract()和eval()、错误抑制符@、mysql_*老函数。 - JavaScript:
==与===混用、未处理的 Promise rejection、eval、全局变量泄漏、innerHTML直接插用户内容。 - Python:可变默认参数、裸
except、assert用于业务逻辑、字符串拼接 SQL、subprocess的 shell 注入。 - Shell:未加引号的变量展开、
rm -rf $VAR当 VAR 为空时删根、set -e缺失。Shell 建议搭配 ShellCheck,SonarQube 对 shell 支持有限。
五个必须避开的坑
- 用内嵌 H2 数据库当生产用:H2 只能试用,一升级版本数据就可能全丢。正式用必须换成 PostgreSQL。
- 忘记内核参数 vm.max_map_count:SonarQube 内置的 Elasticsearch 启动时会检查这个值,不够就直接崩,而且日志报的是看起来很无关的错。装之前先设好。
- 9000 端口暴露公网:SonarQube 一旦可公开访问,你的全部源码、甚至配置和 token 都有泄露风险。务必只绑回环,用 Nginx 反代加认证或走 SSH 隧道。
- token 写进 sonar-project.properties 提交到仓库:等于把项目写入权限送人。用环境变量,并把配置文件加进 .gitignore。
- 追求历史代码全清零:一个跑了多年的老项目问题成千上万,全清零是不现实的。用「新代码门禁」策略,只管增量,才可持续。
小结
SonarQube 对个人站长的意义,不是「跑个报告好看」,而是把代码质量的把关从「靠人自觉」变成「自动卡点」。它不要求你写测试,装好就能对现有代码开扫;它不逼你清零历史债,只要求新代码干净。配合 Docker 部署、环境变量传 token、Git 钩子接门禁,一整套下来可能就一个下午的事,但从此你每次写代码,都有一只看不见的眼睛帮你盯着那些最容易出事的细节。
落地顺序建议:先在内网把 SonarQube 跑起来,扫一遍现有代码,认真看完 Blocker 和 Critical 级别的问题,顺手把硬编码密码、SQL 拼接、eval 这类高危项改掉;然后把 scanner 接到提交流程里,开启「新代码门禁」。跑上一两个月,你会发现新增代码的问题数明显下降,而自己也逐渐养成了避开那些坑的习惯——这才是静态分析最大的收益。