认证鉴权(Sa-Token)
ArchForge 使用 Sa-Token 1.45.0 做认证,不是 JWT,也不是 Spring Security。
两套相互独立的登录类型,会话保存在 Redis:
| 应用 | 工具类 | Sa-Token type | 端口 |
|---|---|---|---|
| 管理端 | StpAdminUtil | "admin" | :8080 |
| C 端 | StpWebUtil | "web" | :8081 |
登录流程(管理端)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Frontend │ │ LoginController│ │ TokenService│
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ POST /admin/auth/login │ │
│ {username, password, code} │ │
├──────────────────────────────►│ │
│ │ Verify captcha │
│ │ Decrypt RSA password │
│ │ BCrypt verify │
│ │ StpAdminUtil.login(userId) │
│ │ │
│ {accessToken, refreshToken, │ │
│ expires, roles, perms} │ │
│◄──────────────────────────────┤ │
│ │ │
│ Subsequent requests: │ │
│ Authorization: Bearer <token>│ │
└──────────────────────────────►│ │登录有限流:@RateLimit(key = "login", time = 60, maxCount = 5, limitType = IP) —— 每个 IP 每分钟最多 5 次。
令牌管理
Sa-Token 签发的是 UUID 访问令牌(token-style: uuid),不是签名后的 JWT。
访问令牌
- 由
StpAdminUtil.login(userId)/StpWebUtil.login(userId)创建 - 存放在 Sa-Token 会话(Redis)
- 请求头:
Authorization: Bearer <token> - 默认有效期:7 天(
sa-token.timeout: 604800)
刷新令牌(管理端)
- 接口:
POST /admin/auth/refresh-token - 随机 UUID 存在 Redis,映射到用户 ID
- 返回新的 Sa-Token 访问令牌以及新的刷新令牌
配置
sa-token:
token-name: Authorization
token-prefix: Bearer
timeout: 604800
active-timeout: -1
is-concurrent: true
is-share: false
is-read-cookie: false
token-style: uuid
is-log: false
is-print: false生产环境密钥 没有默认值。Redis、数据源密码、RSA 密钥等都需要自行配置,不要把空的生产凭据提交进仓库。
授权
控制器使用带 type 的 Sa-Token 注解:
@SaCheckLogin(type = StpAdminUtil.TYPE)
@SaCheckPermission(value = "system:user:add", type = StpAdminUtil.TYPE)
@PostMapping
public void addUser(...) { }权限字符串来自 sys_menu.permission,例如 system:user:add、system:role:query、system:scheduler-job:list。
获取当前管理端用户:
SystemLoginUser loginUser = LoginContext.getAdminUser();密码加密
仍然是双层:
- RSA —— 登录密码的传输加密:RSA-OAEP / SHA-256。管理端登录页从
GET /admin/auth/getConfig取rsaPublicKey(由arch-forge.rsa-private-key推导),用浏览器自带的 WebCrypto 加密,不需要额外的库。prod 下解不开的密码按"用户名或密码错误"处理(10101),并计入登录失败锁定;其他 profile 回退为按原文处理。没有安全上下文时(非 localhost 的纯 HTTP),浏览器没有 WebCrypto,会发送明文——prod 请用 HTTPS。 - BCrypt —— 数据库中存储哈希
公开接口(管理端)
无需登录。名单即 AdminSaTokenConfig.PUBLIC_PATHS;若某个 /admin/auth/** 条目背后没有真实端点,契约测试会失败——不会给尚不存在的端点预先开放免登录:
POST /admin/auth/login、POST /admin/auth/refresh-tokenGET /admin/auth/captchaImage、GET /admin/auth/getConfigGET /livez、GET /readyz、GET /actuator/health/**、/actuator/info、/actuator/prometheus——staging/prod下 actuator 本身在管理端口上(见配置)GET /swagger-ui/**与GET /v3/api-docs/**(生产环境关闭)
其余接口都需要登录,并且每个管理端处理器都有自己的 @SaCheckPermission(只有登录/会话接口例外)。两条契约测试守着它:处理器缺权限会失败;权限码没有任何菜单或按钮授予也会失败——那样的权限只有 * 超级管理员能用。
验证码
arch-forge:
captcha:
enabled: true
captcha-type: math # "math" 或 "text"图片由 Kaptcha 生成,临时存放在 Redis。
登录响应(管理端)
管理端 JSON 包装为 {code, message, data}。data 大致如下:
{
"accessToken": "xxxxxxxx-uuid-style-token",
"refreshToken": "hex-without-dashes",
"expires": "2025/01/14 00:00:00",
"username": "admin",
"roles": ["admin"],
"permissions": ["*:*:*"]
}C 端(archforge-server-web)错误使用 RFC 9457 ProblemDetail,而不是上述信封。
XSS 过滤
XssFilter 会清理 查询参数和请求头。跳过 multipart/*、application/octet-stream、application/pdf 与 image/*。JSON 请求体仍需控制器级校验。
API 请求签名
敏感接口或第三方接入可使用 @ApiSign。拦截器校验:
| Header | 说明 |
|---|---|
X-App-Key | 应用标识 |
X-Timestamp | 请求时间戳(毫秒) |
X-Nonce | 一次性随机串(防重放) |
X-Sign | 小写十六进制 HMAC-SHA256 签名 |
HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)arch-forge:
security:
sign:
enabled: true
timeoutSeconds: 300
nonceTtlSeconds: 600
apps:
test-app: test-secret幂等 Token
@Idempotent 防止重复提交:
- PARAM:方法签名 + 参数哈希生成 Redis SETNX 锁
- TOKEN:客户端调用
GET /idempotent/token,再通过X-Idempotent-Token提交 - HEADER:基于指定请求头值加锁