Security
受众:开发者 摘要:安全规范:脱敏 / 密钥管理 / 操作审计 / Token 安全 / 防 XSS / 防注入 / CSRF。
安全不是单独模块,而是贯穿配置、认证、支付、Web3、存储、日志和运维入口的项目约束。
V1.0 前上线检查、依赖树复核和运行基线请看 安全与依赖基线。
基础原则
- 不在代码、文档示例和日志中输出真实密钥、私钥、Token、证书内容。
- 敏感配置使用环境变量、部署平台密钥或数据库配置。
- 后台展示敏感值必须脱敏。
- Controller 不写安全判断细节,权限、数据范围和审计逻辑下沉到统一组件或 Service。
- 生产环境关闭不必要的调试入口。
Token
Auth 基于 Sa-Token 和 JWT。
前后端分离模式使用:
Authorization: Bearer <token>不依赖 Cookie 和 Session。
生产环境必须配置强随机 APP_SECRET,并通过安全方式注入。JWT 默认优先使用专用 SPRING_OPEN_AUTH_JWT_SECRET_KEY,未配置专用密钥时回退到 spring.application.secret,因此多实例部署至少要保证共享数据库配置中心可用,或显式配置同一份 APP_SECRET。
SpringOpen 不在启动期通过 spring.open.config.production-guard.* 强制拒绝数据库 / Redis 示例密码,也不默认要求 Redis 必须设置密码。数据库、Redis、安装入口、Provider 密钥和网络访问控制属于部署安全策略,由运维环境、密钥平台、网络边界和发布检查清单保证;dev、lite、test 等非生产 profile 继续保持宽松,避免影响开发者本地使用和隔离诊断。普通开发仍建议使用 dev profile + MySQL / Redis 等真实基础设施,不推荐用 lite 绕开依赖。
密码策略
账号密码规则由统一密码策略控制,不在注册、重置密码、修改密码、后台创建用户和安装向导里分别写死。
默认策略保持低门槛,方便私有化部署、内部系统和早期开发快速使用:
- 最少 6 位。
- 最多 72 位,避免超过 BCrypt 推荐输入长度。
- 允许简单弱口令,例如纯数字、纯字母、重复字符或常见弱密码。
- 后端仍会校验非空、长度上限和确认密码一致;弱口令在低级别只作为前端提示或安装 warning,不作为失败条件。
推荐配置分级:
| 级别 | 默认行为 | 适用场景 |
|---|---|---|
low | 最少 6 位,允许弱口令 | 本地开发、内网系统、早期安装和用户希望低门槛注册的产品 |
medium | 最少 8 位,建议至少两类字符,拒绝账号 / 邮箱 / 手机号直接作为密码 | 常规互联网产品 |
high | 最少 12 位,至少三类字符,拒绝连续 / 重复 / 常见弱口令 | 管理后台、生产系统和高安全场景 |
custom | 完全按配置项组合规则 | 企业私有化、合规或特殊业务 |
规划配置前缀:
spring:
open:
iam:
password:
policy:
level: low
min-length: 6
max-length: 72
allow-weak: true当前 low / medium / high / custom 由后端统一解析,已覆盖注册、后台创建用户、后台重置密码、当前用户修改密码、手机号 / 邮箱找回密码和安装向导创建首个管理员等写入新密码入口。数据库配置可以覆盖本地配置,前端通过公开的密码策略元数据接口展示当前规则和强度提示。
权限
后台权限使用 RBAC:
iam:user:view
iam:user:update开放平台权限使用 scope:
open:web3:balance:read
open:web3:wallet-signature:verify权限命名应表达能力域、资源和动作。不要把权限判断写死在 Controller 业务逻辑里。
资源实例级授权(Gate / Policy)
RBAC 权限串回答「有没有 iam:user:update 这个能力」(权限维度),但回答不了「这条订单是不是你的」(资源实例维度)。后者用 Gate / Policy(对标 Laravel),别在 Service 里散写 resource.getUserId().equals(currentUserId)。
按资源类型声明一个 Policy<T> Bean,判定逻辑(含 ownership、状态机、二次权限校验)收敛在一处:
@Component
public class OrderPolicy implements Policy<Order> {
@Override
public boolean allows(String ability, Order order, Long userId) {
return switch (ability) {
case "update", "delete" -> order.getUserId().equals(userId);
case "refund" -> order.getUserId().equals(userId) && Auth.hasPermission("order:refund");
default -> false;
};
}
}业务侧用 Gate 静态 Facade:
Gate.authorize("update", order); // 拒绝抛 ForbiddenException
if (Gate.allows("delete", comment)) { ... }
Gate.authorizeForUser(userId, "refund", order); // 不依赖当前登录态- 与 Sa-Token 互补:先用权限串 /
@SaCheckPermission守入口,再用 Gate 守具体资源实例。 - 默认拒绝:资源为
null或该类型无匹配Policy时判定为拒绝。 - 全局放行(如超级管理员)用
GateBeforeCheckBean,在所有 Policy 之前短路。 - 策略按资源运行时类型路由,以父类型声明可覆盖其子类。
限流
登录、短信验证码、开放平台和高成本业务接口优先复用 Rate Limit starter。
系统账号密码登录默认按登录标识和 IP 同时限流。限流 key 会哈希处理登录标识和 IP,避免把用户名、邮箱、手机号或 IP 明文写入 Redis。被限流时返回统一业务码 429,并写入登录失败审计。
账号密码注册同样按注册标识和 IP 双维度限流,默认 identifier-max-attempts=3、ip-max-attempts=10、窗口 1m。注册来源和设备会写入认证审计,便于区分 App、PC、安装页或第三方入口带来的注册行为。
认证短信验证码入口 POST /api/v1/auth/verify/sms-codes / check 默认按手机号和 IP 组合限流;认证邮箱验证码入口 POST /api/v1/auth/verify/email-codes / check 默认按邮箱和 IP 组合限流。验证码只以哈希形式写入 Cache,不把验证码明文写入缓存 key 或日志;校验成功后立即消费;短信发送通过 Notification 的 sms 通道进入统一发送日志和失败重试链路,邮箱验证码通过 Notification 的 mail 通道发送。
验证码风控总开关位于 spring.open.iam.auth.login.captcha.*,默认关闭。短信、邮箱、图形、滑动、旋转和拖动验证码分别有独立开关,配置可以通过数据库配置覆盖;GET /api/v1/auth/capabilities 会返回 captcha.enabled、captcha.riskBased 和 captcha.sms/email/image/slider/rotate/drag.enabled 能力矩阵,前端应以后端矩阵判断入口可用性。图形验证码首段已提供 POST /api/v1/auth/verify/image-captcha/challenge 和 POST /api/v1/auth/verify/image-captcha/check:挑战返回 challengeId、scene、SVG data URL、MIME 类型和 5 分钟有效期;验证码只以 challenge / scene / code 哈希写入 Cache,校验成功后立即消费。交互验证码首段已提供 POST /api/v1/auth/verify/interactive-captcha/challenge 和 /check,请求 type=slider|rotate|drag,挑战返回轨道尺寸、目标坐标或旋转角度、容差和 5 分钟有效期;校验使用 answer 哈希比对并成功即消费。后续接入风控后推荐只在风险命中时展示验证码。
外部身份登录的可替换风控入口是 IAM 的 LoginRiskGuard。该扩展点默认没有实现,不影响基础登录;部署方可以注册自己的 guard,在外部身份 verify 完成、签发会话前基于 provider、设备、IP、用户和外部主体做拒绝判断。拒绝结果会进入认证登录审计和登录风控摘要,不内建额外风控服务或数据表。LDAP / AD、SAML 和企业 OIDC 仍先由各自 adapter 完成目录 bind、SAML Assertion 验签或 OIDC code 换取与 UserInfo 校验,再进入同一风控入口。
GET /api/v1/auth/capabilities 同时返回 loginMethods 和 registerMethods,每个条目包含 key、enabled、order 和 environments。默认登录方式为账号密码、短信、邮箱、钱包和 Passkey;默认注册方式为账号、手机和邮箱。配置可通过 spring.open.auth.login.methods.<method>.enabled/order/environments 与 spring.open.auth.register.methods.<method>.enabled/order/environments 覆盖,environments 支持逗号分隔的 all、h5、app、mp 等值。spring.open.auth.register.password.enabled=false 仍作为注册总开关,会整体关闭注册方式列表。
数据权限
数据权限支持:
- 全部。
- 本部门。
- 本部门及子部门。
- 本人。
- 自定义部门。
列表接口和 ID 型接口都应做数据范围校验。新增后台实体时,不要只保护分页列表。
敏感配置
敏感配置包括:
- 数据库密码。
- Redis 密码。
- 应用共享密钥和 JWT 专用密钥。
- 支付商户私钥和证书。
- Webhook token 和 secret。
- 短信、邮件、翻译和对象存储密钥。
- Web3 私有 RPC、私钥和助记词。
数据库配置应标记敏感字段,后台展示脱敏值,日志只记录配置 key 和操作结果。
实体字段加密
需要把手机号、第三方 API Key、Webhook secret、业务授权码等可逆敏感值保存到数据库时,优先复用 spring-open-core-secret。字段使用 @EncryptedField 标记,Service 在写入前调用 SecretFieldProcessor.protect(...),在读取后调用 SecretFieldProcessor.reveal(...):
class UserSecretEntity {
@EncryptedField(namespace = "iam.user.phone", keyId = "phone-key")
private String phoneCipher;
}
class UserSecretService {
private final SecretFieldProcessor secretFieldProcessor;
void save(UserSecretEntity entity) {
secretFieldProcessor.protect(entity, SecretFieldCryptoContext.builder()
.keyMaterial(SecretKeyMaterial.of(applicationSecret))
.tenantId("tenant-1")
.operatorId("100")
.build());
mapper.insert(entity);
}
UserSecretEntity find(Long id) {
UserSecretEntity entity = mapper.selectById(id);
return secretFieldProcessor.reveal(entity, SecretKeyMaterial.of(applicationSecret));
}
}处理器只修改显式标注的可变 String 字段,密文仍使用 SecretManager 的 secret:v1: envelope;空值、已加密值和未加密值按 SecretManager 既有幂等规则处理。namespace 会作为 Secret purpose 和审计上下文;不配置时使用 fully.qualified.ClassName#fieldName。
使用边界:
- 这是 Service 层显式能力,不是全局 MyBatis 拦截器;框架不会因为类路径里存在注解就自动改写所有实体。
- 建议数据库列命名为
*_cipher,避免把密文字段误当明文字段使用。 - 加密字段不适合模糊查询或排序;确需精确匹配时,由业务额外维护不可逆摘要列,例如
phone_digest。 - 密码、支付签名私钥等不需要可逆读取的值不要用字段加密;密码应继续使用 hash-only。
- 对外展示仍应使用 DTO / VO 和
@SensitiveField脱敏,字段加密不替代权限、审计和输出脱敏。
响应字段脱敏
普通 JSON API 的对外 DTO / VO 字符串字段可以使用 Web MVC starter 提供的 @SensitiveField 标记,在 Jackson 序列化阶段输出脱敏值:
class UserProfileVO {
@SensitiveField(type = SensitiveMaskType.PHONE)
private String phone;
@SensitiveField(type = SensitiveMaskType.EMAIL)
private String email;
@SensitiveField(type = SensitiveMaskType.CUSTOM, keepPrefix = 2, keepSuffix = 2)
private String inviteCode;
}内置类型包括 PHONE、EMAIL、ID_CARD、BANK_CARD、NAME、ADDRESS、OPEN_ID、UNION_ID、SECRET、TOKEN 和 GENERIC。特殊场景使用 CUSTOM 或非负 keepPrefix / keepSuffix 定义保留前后字符数,replacement 控制中间替换文本。
使用边界:
- 优先标在对外 DTO / VO 字段、getter 或 record component 上,不直接标持久化 Entity,避免影响内部计算、导出、签名和回调。
- 仅覆盖 Jackson JSON 序列化链路,适合普通
Result<T>/ 列表 / 嵌套对象响应。 - CSV 导出、SSE、Webhook、微信 / 支付等外部协议回调和原始报文回放不自动套用该注解,应由对应服务显式控制脱敏或保持协议原文。
- 脱敏只解决展示层输出,不替代密钥加密存储、访问控制、审计和日志清洗。
文件和存储
公开文件放 public/ 或对象存储公开 bucket。
私有文件放私有 disk,通过受控下载或临时签名 URL 访问。
不要把私有文件路径直接暴露为永久公开 URL。对象存储域名更换时,业务表应尽量保存相对路径、disk、bucket 和 object key,而不是写死完整域名。
XSS 与安全响应头
Web MVC starter 默认启用基础安全响应头:
X-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: strict-origin-when-cross-originX-XSS-Protection: 0
X-XSS-Protection 属于旧浏览器机制,现代项目不要依赖它做主要 XSS 防护。XSS 防护应优先在输出编码、前端模板默认转义、富文本白名单清洗和页面级 CSP 上处理。
Content-Security-Policy 默认不设置,避免破坏 API、文档站点、样板页面或已有前端资源加载。生产环境可以通过本地配置或数据库配置设置:
spring.open.web.security.headers.content-security-policy=default-src 'self'不建议做全局请求 body 改写式 XSS 过滤。它容易破坏 JSON、Webhook 原始报文、开放平台签名、支付回调签名和富文本保存。需要富文本的业务模块应在字段级做白名单清洗,并保留原始签名链路。
Payment
支付模块必须注意:
- 回调必须验签。
- 回调日志要落库。
- 订单状态更新必须幂等。
- 退款、补偿和对账不直接信任前端入参。
- 商户私钥和证书路径不写入 Git。
支付 Provider 面向服务商,例如 alipay、wechat、paypal。IJPay 只作为内部实现,不暴露给业务层。
Web3
Web3 模块必须注意:
- 公有 RPC 可以对外展示,私有 RPC 不对外返回。
- 私有 RPC 字段已做 JSON 忽略兜底,但对外接口仍应使用 VO 白名单。
- 私钥、助记词和签名广播属于高风险能力,必须配合权限、审计、限额、脱敏和人工/自动审核策略。
- 钱包签名验签只证明签名者控制地址,不等于登录或授权业务操作。
- App 钱包登录使用“连接浏览器钱包 → 后端生成挑战 →
personal_sign签名 → 后端验签签发登录态”的流程;挑战内容、过期时间和验签结果以后端为准,前端不能自行拼接登录凭证。 - 邮箱地址、有效手机号和合法 EVM 钱包地址格式的用户名需要归属验证:只有目标用户已绑定同一邮箱、手机号或钱包地址时才允许改名或后台保存;普通用户名、非法身份标识格式和未启用钱包身份提供方的环境不受钱包策略影响。
- 钱包登录成功后,App 可以保存本地钱包连接记录用于下次识别和钱包中心展示;该记录不等于账号级钱包绑定,绑定、解绑、默认钱包和安全审计仍必须以后端正式接口为准。
日志
日志中不要输出:
- 密码。
- Authorization header。
- appSecret。
- 支付私钥。
- Webhook token。
- Web3 私钥。
- 完整证书内容。
TraceId 可以记录,用于链路定位。
依赖漏洞修复
依赖安全升级不能只按扫描器建议直接改版本。处理 CVE、Mend 或 IDE 提示时,先查阅 Maven Central / mvnrepository、官方 release notes 或 changelog、GitHub issue / advisory 和 CVE 描述,再结合 Maven dependency tree 判断传递依赖路径。
如果漏洞来自传递依赖,优先在依赖管理模块统一覆盖版本,并在实际使用模块显式声明受控依赖或排除旧传递依赖。升级后至少验证受影响模块 dependency tree、模块测试和应用聚合测试,确认旧漏洞版本不再进入最终依赖树。
依赖治理要尽量集中。公共 exclusion 优先放在 spring-open-dependencies 的 dependencyManagement 中,业务 starter 不重复维护同一段排除规则;starter 只显式声明自己运行期确实需要的受控依赖,方便安全扫描和依赖树排查。
注意 Maven 坐标变化不能只靠版本覆盖解决。例如旧 dom4j:dom4j 和新 org.dom4j:dom4j 是不同坐标,必须排除旧坐标后再显式引入新坐标,否则两个 jar 可能同时进入依赖树。
安装与初始化
项目不提供固定默认账号或默认密码。首个超级管理员应通过安装向导创建;项目不再提供平行的管理员初始化 API。
初始化成功后入口自动失效。生产环境不要长期保留安装入口。
普通用户账号通过注册、短信 / 邮箱验证码或外部身份登录创建,不通过种子数据生成。测试环境需要普通用户时,也应走同一套用户侧流程,避免文档和脚本里沉淀固定弱口令。
安全检查清单
上线前至少检查:
APP_SECRET或 JWT 专用密钥是否已覆盖默认值。- 数据库和 Redis 密码是否安全。
- 生产环境是否配置了高强度
SPRING_OPEN_INSTALL_TOKEN,并在安装入口之外保留网关访问限制和安装锁。 /actuator/**是否只暴露必要端点;默认生产配置仅暴露/actuator/health且不返回详细信息。- 数据库配置中的敏感值是否脱敏。
- 支付和对象存储密钥是否未进入 Git。
- CVE 修复是否已核对官方资料、传递依赖树和模块兼容性。
- 私有文件是否没有走公开目录。
- Webhook endpoint 中 token 是否没有写入日志。
- Open Platform appSecret 是否只在服务端使用。
/api/**、OpenAPI、WebSocket 和私有下载入口是否经过网关规则确认。- 安全响应头是否符合当前站点资源加载策略,CSP 是否经过页面验证。