生产环境安全
本小节,汇总项目部署到公网生产环境前,需要检查的配置项、默认数据和网络边界。
适用范围
- 本文只覆盖项目自带的配置、初始化数据,以及 Nginx 与端口的收口,不能代替代码审计、漏洞扫描、主机加固和依赖升级。
- 按本文完成配置,不代表系统已经安全,请结合自己启用的模块和部署方式检查。
- 文中的
<...>都是占位符,需要替换成你自己的值。不要把真实的密码、密钥提交到代码仓库。
# 1. 生产配置从哪里来?
yudao-server/src/main/resources 目录下,项目自带三份配置文件:
| 文件 | 用途 | 生产注意 |
|---|---|---|
application.yaml | 所有环境共用 | 其中的密钥是示例值,需要在生产配置中覆盖 |
application-local.yaml | 本地开发 | 开启了 yudao.security.mock-enable 模拟登录,关闭了 yudao.captcha.enable 验证码,不能用于生产 |
application-dev.yaml | 开发、测试环境 | 部署文档多以它为例,Actuator、Druid 等监控全部开放 |
推荐的做法:
① 复制 application-dev.yaml 为 application-prod.yaml,修改数据库、Redis 等地址,再合并「2.1 示例」中的安全配置。
② 启动时使用 --spring.profiles.active=prod 参数,或 SPRING_PROFILES_ACTIVE=prod 环境变量。
③ 密码、密钥使用 ${环境变量名} 引用,由启动脚本、Docker 或 K8s 的 Secret 注入。application.yaml 中 AI 的 api-key 已经是 ${OPENAI_API_KEY:sk-xxxx} 这种写法,可以参考。
# 2. 全局配置
# 2.1 示例
下面是 application-prod.yaml 中与安全相关的片段,可以合并到你的文件里。每个顶层配置项只出现一次,避免和已有内容重复。
springdoc:
api-docs:
enabled: false # 关闭 /v3/api-docs 接口(OpenAPI JSON)
swagger-ui:
enabled: false # 关闭 /swagger-ui 页面
knife4j:
enable: false # 关闭 Knife4j 增强
management:
endpoints:
web:
exposure:
include: health # 只通过 HTTP 暴露 health 端点
endpoint:
health:
show-details: never # 不返回数据库、Redis 等组件的详情
show-components: never
spring:
datasource:
druid:
web-stat-filter:
enabled: false # 关闭 Web 统计
stat-view-servlet:
enabled: false # 关闭 /druid 控制台
# boot: # 仅启用 Spring Boot Admin 时需要,见「2.5」
# admin:
# client:
# username: ${SBA_USERNAME}
# password: ${SBA_PASSWORD}
mybatis-plus:
encryptor:
password: ${YUDAO_ENCRYPTOR_PASSWORD} # 见「2.6」:已有加密数据时,不要随意更换
yudao:
captcha:
enable: true # 开启图片验证码
security:
mock-enable: false # 关闭模拟登录
sms-code:
begin-code: 100000 # 短信验证码在 [begin-code, end-code] 之间随机
end-code: 999999
demo: false # 关闭演示模式
# 2.2 Swagger、Knife4j 接口文档
| 配置项 | 作用 |
|---|---|
springdoc.api-docs.enabled | 关闭 /v3/api-docs 接口,即全部接口的定义。项目的 YudaoSwaggerAutoConfiguration 也以它为开关 |
springdoc.swagger-ui.enabled | 只关闭 /swagger-ui 页面 |
knife4j.enable | 关闭 Knife4j 的增强功能 |
注意,infra 模块的 SecurityConfiguration 对 /v3/api-docs/**、/swagger-ui/**、/webjars/** 设置了免登录。只关页面、不关 springdoc.api-docs.enabled 的话,接口定义仍然可以免登录获取。
# 2.3 Actuator 监控端点
application-local.yaml、application-dev.yaml 中的 management.endpoints.web.exposure.include 是 '*',即开放所有端点。并且 infra 模块对 /actuator/** 设置了免登录,任何能访问到后端端口的人,都可以读取 env、heapdump、loggers 等端点。
Actuator 是否生效,以启动日志中 Exposing ... beneath base path '/actuator' 这一行为准,也可以在后端服务器上请求 /actuator 确认。
生产环境下,按「2.1 示例」只暴露 health。如果需要保留更多观测端点(例如给 Spring Boot Admin、Prometheus 使用),有两种方式:
- 方式一:后端端口只允许内网访问,Nginx 不对公网转发
/actuator,参见「5. Nginx、HTTPS 与端口」。 - 方式二:设置
management.server.port和management.server.address,让 Actuator 监听单独的内网端口。这会改变 Spring Boot Admin 拉取端点的地址,需要实际验证。
为什么不能直接配置 `spring.security.user` 账号密码?
项目的 YudaoWebSecurityConfigurerAdapter 自定义了基于访问令牌的 SecurityFilterChain,没有开启 HTTP Basic;infra 模块的 SecurityConfiguration 又对 /actuator、/actuator/** 设置了 permitAll()。所以只配置 spring.security.user.name、spring.security.user.password,不会保护 Actuator 端点。
如果希望由应用自己校验账号密码,需要为 /actuator/** 单独编写一个 SecurityFilterChain,并去掉上述 permitAll(),项目没有内置这个开关。不改代码时,可以使用「5.3」的网段限制 + Nginx Basic 认证。
Spring Boot Admin、Druid 有各自的账号密码,按「2.4」「2.5」配置即可。
单个端点的开关,Spring Boot 2.x 使用 management.endpoint.<id>.enabled,Spring Boot 3.4 起改为 management.endpoint.<id>.access。本文示例只用 exposure.include 控制暴露范围,不需要这两个配置项。shutdown 端点默认就是关闭的,没有加入 exposure.include 时也不会通过 HTTP 暴露。
# 2.4 Druid 监控
application-dev.yaml 中,spring.datasource.druid.stat-view-servlet.enabled 为 true,allow、login-username、login-password 都为空,并且 infra 模块对 /druid/** 设置了免登录。
- 不需要时:按「2.1 示例」关闭
stat-view-servlet和web-stat-filter。filter.stat的慢 SQL 日志不受影响。 - 需要时:设置
login-username、login-password,并且只通过内网访问。经过 Nginx 转发时,后端看到的来源地址是 Nginx,所以不能只依赖allow白名单。
前端的 [基础设施 -> 监控中心 -> MySQL 监控] 菜单,通过 iframe 嵌入 url.druid 配置的地址。公网关闭后该菜单无法打开,属于预期。
# 2.5 Spring Boot Admin
- Server 端:只有引入
spring-boot-admin-starter-server依赖后才会启用(yudao-module-infra的pom.xml中默认是<optional>true</optional>),由AdminServerConfiguration的@ConditionalOnClass控制。不需要时,保持该依赖为 optional 即可。 - 登录认证:
AdminServerConfiguration已经为spring.boot.admin.context-path(默认/admin)下的路径注册了独立的认证:浏览器使用表单登录,Client 注册使用 HTTP Basic。账号、密码读取spring.boot.admin.client.username、spring.boot.admin.client.password,未配置时都是admin。 - Client 端:
spring.boot.admin.client.enabled: false只是停止向 Server 注册,不会关闭 Server 和/admin页面。
需要使用时:
① 修改上述账号、密码,通过环境变量注入。
② /admin 只允许内网或 VPN 访问,Nginx 不对公网转发。
③ 修改 spring.boot.admin.frame-ancestors 为管理后台的域名,限制可以 iframe 嵌入的页面。
④ 不要删除 spring.boot.admin.context-path 配置,AdminServerConfiguration 依赖它确定认证拦截的路径范围。
更多说明,可见 《服务监控》 文档。
# 2.6 登录、验证码与密钥
| 配置项 | 默认情况 | 生产要求 |
|---|---|---|
yudao.security.mock-enable | 代码默认 false,application-local.yaml 为 true | 必须为 false。开启后,以 yudao.security.mock-secret 开头的 token 可以模拟任意用户 |
yudao.captcha.enable | 代码默认 true,application-local.yaml 为 false | 开启。前端的 VITE_APP_CAPTCHA_ENABLE 需要保持一致 |
yudao.sms-code.begin-code、end-code | application.yaml 中两者相同,即固定验证码,方便测试 | 改为一个范围,例如 100000、999999 |
mybatis-plus.encryptor.password | application.yaml 中是示例值 | 上线前替换。它用于加解密数据库字段(例如 infra_data_source_config.password),已有加密数据后再更换,旧数据将无法解密 |
yudao.api-encrypt.* | 默认关闭,附带示例密钥 | 启用时,替换 request-key、response-key |
yudao.demo | false | 演示模式会拦截写操作,普通生产环境保持 false |
yudao.security.permit-all_urls | 默认放行 /admin-api/mp/open/** | 只添加确实需要免登录的回调地址 |
免登录的接口,有三个来源:接口上的 @PermitAll 注解、yudao.security.permit-all_urls 配置项、各模块 SecurityConfiguration 中的 permitAll()。新增免登录接口时,需要接口自身完成签名或令牌校验。
# 3. 模块配置与数据库
只需要检查已启用的模块。
# 3.1 配置文件
| 模块 | 配置项 | 处理要求 |
|---|---|---|
| 通用 | spring.datasource.dynamic.datasource.*.username、password,spring.data.redis.password,spring.rabbitmq.username、password 等 | 使用独立账号和强密码,通过环境变量注入。Redis 在 dev 配置中没有设置密码,生产需要开启。JDK 8(Spring Boot 2)分支使用 spring.redis.password,Spring Boot 3、4 分支使用 spring.data.redis.password |
| system / member | justauth.type.*.client-id、client-secret,wx.mp.app-id、secret,wx.miniapp.appid、secret | 换成自己的应用,不用的三方登录直接删除 |
| pay | yudao.pay.order-notify-url、refund-notify-url、transfer-notify-url | 改为生产域名下的 HTTPS 地址,不要保留内网穿透地址 |
| mall | yudao.trade.express.kd-niao.api-key、business-id,yudao.trade.express.kd100.key、customer,yudao.tencent-lbs-key | 换成自己的密钥 |
| ai | spring.ai.*.api-key、yudao.ai.*.api-key | 只开启用到的平台,通过环境变量注入 |
| ai | yudao.ai.midjourney.notify-url | 改为生产域名 |
| ai | spring.ai.mcp.server.enabled(默认 false) | 开启后,MCP 端点免登录,见「4」 |
| bpm | flowable.database-schema-update(默认 true) | 可选:表结构稳定后改为 false |
| iot | yudao.iot.gateway.token.secret | IoT 网关 application.yaml 中是示例值,替换为至少 32 位的随机值 |
| iot | yudao.iot.gateway.protocols 中各协议的 enabled、port,emqx 协议的 mqtt-username、mqtt-password,SSL 的 key-store-password、trust-store-password | 只开启用到的协议,替换示例账号密码 |
| iot | yudao.iot.gateway.rpc.url | 指向 yudao-server 的内网地址 |
| iot | TDengine、IoTDB 数据源的 username、password | 替换默认账号密码 |
# 3.2 数据库与后台配置
以下数据来自初始化 SQL,建议在管理后台对应的页面修改。不要直接删除数据行,部分数据是系统运行所必需的。
其中 infra_config 是参数配置表,下表中它的几行是配置项的 config_key 值:按 config_key 找到对应的配置项,再修改它的 value。
| 表 | 字段 / 配置 key | 处理要求 |
|---|---|---|
system_users | username、password | 登录后修改内置账号(例如 admin)的密码,不用的演示账号停用 |
infra_config | system.user.init-password | 新建用户的初始密码,默认是弱口令,改为强密码 |
infra_config | system.user.register-enabled | 不需要后台自助注册时,改为 false |
infra_config | url.druid、url.spring-boot-admin、url.swagger、url.skywalking | 前端监控菜单 iframe 的地址,填内网地址或留空 |
system_oauth2_client | client_id、secret、redirect_uris、authorized_grant_types、status | client_id 为 default 的客户端是后台登录使用的,不要删除、禁用或改名。其它演示客户端不用时禁用,使用时更换 secret、收紧 redirect_uris |
system_social_client | client_id、client_secret、agent_id、public_key | 换成自己的应用 |
system_sms_channel | api_key、api_secret、callback_url | 换成自己的短信账号 |
system_mail_account | username、password、host、port、ssl_enable | 换成自己的邮箱,开启 SSL |
infra_file_config | config(JSON,包含存储的访问密钥) | 换成自己的存储配置 |
infra_data_source_config | url、username、password | password 由 mybatis-plus.encryptor.password 加密 |
pay_app | order_notify_url、refund_notify_url、transfer_notify_url | 改为生产域名 |
pay_channel | config(JSON,包含商户号、密钥、证书) | 换成自己的商户配置 |
mp_account | app_id、app_secret、token、aes_key | 换成自己的公众号 |
ai_api_key | api_key、url | 换成自己的密钥 |
# 4. 需要保留的业务入口
以下接口是业务必需的,不能一刀切封禁。它们大多在 /admin-api/ 下,随 API 一起转发即可。
| 路径 | 用途 | 鉴权方式 | Nginx |
|---|---|---|---|
/admin-api/pay/notify/order/{channelId}、refund/{channelId}、transfer/{channelId} | 支付渠道回调 | 免登录,由支付渠道的签名校验 | 随 /admin-api/ 转发 |
/admin-api/mp/open/{appId} | 公众号消息、服务器地址校验 | 免登录,校验微信签名 | 随 /admin-api/ 转发 |
/admin-api/ai/image/midjourney/notify | Midjourney 回调 | 免登录 | 启用 AI 时保留 |
/admin-api/infra/file/{configId}/get/** | 文件读取 | 免登录 | 保留。只有读取免登录,上传、删除等接口仍需登录和权限 |
/infra/ws | WebSocket | 通过 ?token= 参数传递访问令牌 | 需要单独配置 Upgrade 头 |
/jmreport/**、/drag/**、/jimubi/** | 积木报表、仪表盘 | Spring Security 放行,积木内部由 JmReportTokenServiceImpl 校验访问令牌 | 启用 report 模块时才转发,并参考积木官方的 《生产安全》 (opens new window) 文档 |
/sse、/mcp/message(以 spring.ai.mcp.server.* 配置为准) | MCP Server | 免登录 | 未启用时不转发;启用时只对受信的客户端开放 |
| IoT 网关的协议端口 | 设备接入 | 由各协议的设备认证完成 | 不经过 Nginx,按启用的协议开放端口 |
WebSocket 的访问令牌在 URL 参数中,会出现在 Nginx 的 access log 里,注意控制日志文件的访问权限。
# 5. Nginx、HTTPS 与端口
# 5.1 端口边界
| 端口 | 服务 | 公网 |
|---|---|---|
| 80、443 | Nginx | 开放 |
| 48080 | yudao-server | 不开放,只允许 Nginx、IoT 网关等内网访问 |
| 3306、6379、MQ 等 | 中间件 | 不开放 |
| IoT 协议端口(例如 HTTP 8092、MQTT 1883) | IoT 网关 | 按启用的协议开放 |
48080 不能对公网开放的原因:/actuator/**、/druid/**、/v3/api-docs/** 在后端是免登录的;IoT 网关调用的 /rpc-api/iot/** 接口也是免登录的,依赖“只有受信的网关能访问”这个前提。
友情提示:Docker 的端口映射
使用 docker run -p 48080:48080 时,Docker 会直接修改 iptables,可能绕过 ufw、firewalld 等主机防火墙规则。建议写成 -p 127.0.0.1:48080:48080,或者在云厂商的安全组中限制。
# 5.2 Nginx 示例
在 《Docker Compose 部署》 的 nginx/default.conf 基础上,增加 HTTPS 和管理端点的拦截:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name <你的域名>;
return 301 https://$host$request_uri; # HTTP 跳转 HTTPS
}
server {
listen 443 ssl;
server_name <你的域名>;
ssl_certificate /etc/nginx/cert/<证书文件>.pem; # 证书申请,见《HTTPS 证书》
ssl_certificate_key /etc/nginx/cert/<证书文件>.key;
root /usr/share/nginx/html; # 前端 dist 目录
index index.html;
client_max_body_size 32m; # 与后端 spring.servlet.multipart.max-request-size 保持一致
# ① 管理端点:显式返回 404。正则 location 按书写顺序匹配,必须写在 ② 之前
location = /doc.html { return 404; }
location = /swagger-ui.html { return 404; }
location ~ ^/(actuator|druid|admin|swagger-ui|v3/api-docs|webjars|rpc-api)(/|$) {
return 404;
}
# ② 管理后台 API、用户 App API。支付、公众号回调和文件读取都在其中,不要单独拦截
location ~ ^/(admin-api|app-api)/ {
proxy_pass http://127.0.0.1:48080; # 后端的内网地址
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr; # 最外层 Nginx 使用 $remote_addr 覆盖,见「5.4」
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off; # 流式响应(SSE)不缓冲
proxy_read_timeout 300s;
}
# ③ WebSocket
location ^~ /infra/ws {
proxy_pass http://127.0.0.1:48080;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
# ④ 按需:启用积木报表时,才取消注释
# location ~ ^/(jmreport|drag|jimubi)/ {
# proxy_pass http://127.0.0.1:48080;
# proxy_set_header Host $http_host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $remote_addr;
# }
# ⑤ 前端 history 路由的兜底
location / {
try_files $uri $uri/ /index.html;
}
}
几个注意点:
- Nginx 匹配 location 的优先级是:
=精确匹配,然后是带^~的最长前缀,然后按书写顺序匹配正则,最后才是普通前缀。如果把 ② 改成location ^~ /admin-api/,写在它后面的正则拦截规则就不会生效。 - ① 拦截的路径,原本也不会被转发到后端,而是落到 ⑤ 返回前端的
index.html。显式返回 404,是为了在「6. 上线检查」时能明确区分。 - 不要使用
location /直接转发到后端。
# 5.3 内网访问管理面
如果需要使用 Spring Boot Admin、Druid 等管理页面,可以增加一个只监听内网地址的 server,安全组不对公网开放该端口:
server {
listen <内网 IP>:8081; # 只监听内网地址
allow <VPN 或办公网段>; # 例如 10.8.0.0/24
deny all;
# Actuator:在网段限制之外,再加一层 Nginx Basic 认证
location ^~ /actuator {
auth_basic "actuator";
auth_basic_user_file /etc/nginx/.htpasswd-actuator; # 不要提交到代码仓库
proxy_pass http://127.0.0.1:48080;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Authorization ""; # 不把 Basic 认证头传给后端
}
# Druid、Spring Boot Admin 等页面,使用各自的账号密码登录
location / {
proxy_pass http://127.0.0.1:48080;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
htpasswd 文件在服务器上创建,执行后按提示输入密码,密码不会留在命令历史里:
# htpasswd 命令来自 httpd-tools(CentOS)或 apache2-utils(Ubuntu)
htpasswd -c /etc/nginx/.htpasswd-actuator <用户名> # -c 表示新建文件,只在首次创建时使用
chown root:<Nginx 进程用户组> /etc/nginx/.htpasswd-actuator # 常见为 nginx 或 www-data
chmod 640 /etc/nginx/.htpasswd-actuator # 只允许 root 和 Nginx 进程用户组读取
<Nginx 进程用户组> 按 Nginx worker 进程实际的运行组填写,可以查看 nginx.conf 中的 user 指令。如果该组读不到文件,即使密码正确,Nginx 也会返回 500。以后新增账号时,去掉 -c 参数,例如 htpasswd /etc/nginx/.htpasswd-actuator <新用户名>,否则会覆盖已有的账号。
几个注意点:
- 网段限制仍然是主要的保护手段。
allow、deny和auth_basic同时存在时,默认两者都要满足,Basic 认证只是多加一层,不能因此把 48080 或这个端口开放到公网。 - Basic 认证的账号密码只做了 Base64 编码,必须通过 VPN 访问,或者为这个
server配置 HTTPS。 - 通过该入口拉取指标的监控客户端,例如 Prometheus,需要配置同样的账号密码(Prometheus 的
basic_auth)。 - 不要在
/admin上叠加 Nginx Basic 认证:Spring Boot Admin Client 注册时,也使用 HTTP Basic 的Authorization请求头,两者会冲突。 - 启用 Spring Boot Admin 时,需要按需扩大
exposure.include,此时更要保证后端端口只有内网可以访问。
# 5.4 客户端 IP
后端通过 X-Forwarded-For、X-Real-IP 等请求头获取客户端 IP,用于登录日志、操作日志等。最外层 Nginx 如果使用 $proxy_add_x_forwarded_for,会保留客户端自己传入的值,导致 IP 可以伪造。所以:
- Nginx 是最外层时,使用
proxy_set_header X-Forwarded-For $remote_addr;覆盖。 - Nginx 前面还有 SLB、CDN 时,使用
set_real_ip_from <SLB 网段>;和real_ip_header X-Forwarded-For;,让$remote_addr变成真实的客户端 IP。allow、deny也依赖它。
# 6. 上线检查
# 6.1 公网侧
在公网的机器上执行:
DOMAIN=https://<你的域名>
for p in /actuator /actuator/env /druid/index.html /admin /admin/applications \
/v3/api-docs /swagger-ui/index.html /doc.html /rpc-api/iot/device/get; do
printf '%-28s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "$DOMAIN$p")"
done
预期都是 404。如果返回 200,并且内容是前端的 index.html,说明请求落到了 SPA 的兜底,没有命中拦截规则,需要检查 location 的写法。
再执行 curl -s $DOMAIN/admin-api/system/user/profile/get(不携带访问令牌)。该接口需要登录,预期返回 code 为 401 的 JSON,例如 {"code":401,"data":null,"msg":"账号未登录"},说明 API 转发正常。如果返回前端的 HTML 页面,或 Nginx 的 404 页面,都不算通过。
最后,从公网检查后端和中间件的端口,例如 nc -zv -w 3 <公网 IP> 48080,预期连接失败。
# 6.2 内网侧
在后端服务器上执行:
curl -s http://127.0.0.1:48080/actuator # 只应包含 health;未引入 Actuator 时,返回未找到
curl -s http://127.0.0.1:48080/actuator/env # 不应返回配置内容
curl -s http://127.0.0.1:48080/v3/api-docs | grep -c '"openapi"' # 预期为 0
后端对不存在的路径,可能返回 HTTP 200,但 JSON 中的 code 为 404。请以响应内容判断,不要只看状态码。
如果启用了 Spring Boot Admin,从 VPN 访问 /admin,确认需要输入账号密码。
# 6.3 业务回归
- 管理后台、用户 App 可以正常登录,登录需要图片验证码。
- 短信验证码是随机值,原来的固定验证码无法通过。
- 支付渠道的沙箱回调可以到达,订单状态正常变化。
- 公众号的服务器地址校验可以通过。
- WebSocket 可以连接,文件链接可以打开。
- 启用积木报表时,登录后可以预览报表;未登录时无法访问报表数据。
- 启用 IoT 时,设备可以通过开放的协议端口接入。