JavaFrame 空框架规范
本文件用于让 AI 生成同一套前后端分离空框架。只搭骨架,不写业务。没有用户表、订单、商品、角色权限。
目标
后端提供 HTTP 接口和统一约定,前端负责页面和请求。两边独立进程,用 JSON 通信。
验收时必须同时满足:
GET /api/v1/health不登录也返回统一外壳,data.status为UP,响应头带X-Trace-Id- 传入合法的
X-Trace-Id时,响应头和日志使用同一个号;不合法则后端自己生成 POST /api/v1/auth/login用演示账号换到访问票和刷新票。响应data同时包含token与refreshToken- 用户名和密码都为空时,一次返回全部字段错误,不能只回第一句
- 不带访问票调用
GET /api/v1/me返回 HTTP 401,正文仍是统一外壳 - 带访问票调用
/api/v1/me返回当前用户名 - 访问票失效后,前端用刷新票换一对新票,并只重试原请求一次;旧刷新票立即失效
- 多个请求同时遇到 401 时,只发一次刷新请求,其余请求等待这一次的结果
- 同一张刷新票用第二次返回 HTTP 401,
code为40101 - 带访问票请求不存在的路径返回 HTTP 404,正文
code为40400。未带票时安全过滤器先返回 401,不能用来判断 404 有没有生效 - 带访问票用错误方法调用已存在的接口(例如
POST /api/v1/health)返回 HTTP 405,code为40500。未带票时同样先返回 401 - 开发环境能打开 Swagger,生产环境打不开
- 未登录可以访问
GET /actuator/health与/actuator/info - 未登录访问
GET /actuator与/actuator/env返回 401,正文不得包含环境变量 - 本地页面
http://localhost:5173能登录 docker compose up --build后,页面http://localhost:8088能登录
技术栈
版本不要自行升级。
| 部分 | 选择 |
|---|---|
| JDK | 21 |
| 后端 | Spring Boot 4.1.1,包名 org.example.javaframe |
| 构建 | Maven 3.10.0,由 Maven Wrapper 3.3.4 下载。Wrapper 版本不是 3.10.0 |
| 接口文档 | springdoc-openapi-starter-webmvc-ui 3.1.1。禁止使用 2.x |
| 安全 | spring-boot-starter-security,jjwt 0.12.6(api、impl、jackson) |
| 密码 | Argon2id。调用 Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()。该项目已在 Spring Boot 4.1.1 上运行此方法,不要改成手写参数。依赖 org.bouncycastle:bcprov-jdk18on:1.81.1 |
| 观测 | spring-boot-starter-actuator。Web 只暴露 health、info |
| 前端 | Vue 3.5、TypeScript、Vite、Vue Router、Pinia、axios |
| 前端目录 | 小写,位于后端同级,例如 javaframeff |
| 容器 | 后端 Eclipse Temurin 21,前端 Node 24 构建、Nginx 1.27 发布 |
前端用 create-vue 创建。选择 TypeScript、Vue Router、Pinia、ESLint、Prettier。不要 JSX、Vitest、端到端测试。不要试验特性:Oxfmt、Vue 3.6 候选版、tsgo。删掉脚手架留下的 HelloWorld.vue、TheWelcome.vue、AboutView.vue、stores/counter.ts。
不要做
- 不调 Tomcat 线程数、数据库连接池,也不加 Redis、消息队列、分布式锁、微服务、自建网关
- 不建业务表,不接入 Flyway 和数据库
- 刷新票不做 JWT,也不用 Argon2id
- 密码不明文比对,票和密码不写进日志
- 失败响应不返回 Java 堆栈
- 跨域不允许
* - 生产配置不把文档接口打开
- Actuator 只暴露 health 和 info。env、beans 等其余端点不公开,也不要写成「关掉整个
/actuator/**」 - 前端
request.ts与auth.ts不得互相引用 prod不为演示密码和 JWT 密钥提供默认值
目录
后端
└── src/main/java/org/example/javaframe
├── common Result、FieldErrors、ErrorCode、BizException、GlobalExceptionHandler、TraceIdFilter
├── config WebConfig、OpenApiConfig、SecurityConfig
├── auth 登录请求、刷新请求、演示账号、JWT、过滤器、RefreshTokenStore
└── controller 只放 Health、Auth、Me
前端
└── src/api types.ts、request.ts、refresh.ts、health.ts、auth.ts
仓库根
└── docker-compose.yml
refreshTokens 放在 refresh.ts,使用原生 axios。request.ts 只引用 refresh.ts。auth.ts 只引用 request.ts。
配置
只用一份 application.yml,用 --- 分成三段,不另建 application-dev.yml 或 application-prod.yml。删除 application.properties。
- 第一段没有 profile,放公共配置。
- 第二段
spring.config.activate.on-profile: dev。 - 第三段
spring.config.activate.on-profile: prod。
spring.profiles.active: dev 必须写在第一段。写进带 on-profile 的段里,Spring 会启动失败。生产用环境变量 SPRING_PROFILES_ACTIVE=prod 切换。
公共段包含:
server:
shutdown: graceful
error:
include-stacktrace: never
include-message: never
spring:
profiles.active: dev
threads.virtual.enabled: true
lifecycle.timeout-per-shutdown-phase: 30s
logging:
pattern.correlation: "[%X{traceId:-}] "
management:
endpoints.web.discovery.enabled: false
endpoints.web.exposure.include: health,info
endpoint.health.show-details: never
endpoint.health.probes.enabled: true
info.env.enabled: true
discovery.enabled: false 关掉 /actuator 索引页,避免它列出端点链接。health 与 info 仍然按白名单访问。
本地端口 8080。dev 打开 Swagger,路径 /swagger-ui.html。prod 关闭 springdoc.api-docs 和 springdoc.swagger-ui。
演示账号和密钥按环境分开。dev 可以带本地默认值。prod 只写环境变量,不写冒号后面的默认值;变量缺失时启动失败。
# 仅 dev 段
app:
auth:
username: admin
password: ${APP_AUTH_PASSWORD:admin123}
jwt:
secret: ${APP_JWT_SECRET:dev-only-secret-change-me-32bytes}
access-token-minutes: 30
refresh-token-days: 7
# 仅 prod 段
app:
auth:
username: ${APP_AUTH_USERNAME}
password: ${APP_AUTH_PASSWORD}
jwt:
secret: ${APP_JWT_SECRET}
access-token-minutes: 30
refresh-token-days: 7
JWT 密钥不得短于 32 个字符。演示密码只在启动时用 Argon2id 哈希进内存。
接口外壳
成功:
{ "code": 0, "message": "success", "data": {} }
普通失败时 data 为 null。参数校验失败时 message 固定为「请求参数不正确」,全部字段错误放进 data.fields,不能只取第一个:
{
"code": 40000,
"message": "请求参数不正确",
"data": {
"fields": [
{ "field": "username", "message": "用户名不能为空" },
{ "field": "password", "message": "密码不能为空" }
]
}
}
@NotBlank 必须写中文 message。MethodArgumentNotValidException、HandlerMethodValidationException、ConstraintViolationException 都收成这个列表。
路径不存在时捕获 NoResourceFoundException,不要依赖已经过时的 NoHandlerFoundException,也不要打开 spring.mvc.throw-exception-if-no-handler-found。方法不允许时捕获 HttpRequestMethodNotSupportedException。这两种异常的 data 都是 null。不要让 Spring 默认错误页直接出站。
HTTP 状态和业务码必须同时遵守:
| 场景 | HTTP | code | message | |---|---|---|---| | 成功 | 200 | 0 | success | | 参数不正确 | 400 | 40000 | 请求参数不正确 | | 账号或密码错误 | 401 | 40100 | 账号或密码错误 | | 未登录或访问票无效 | 401 | 40100 | 未登录 | | 刷新票无效或已使用 | 401 | 40101 | 刷新票无效或已使用 | | 路径不存在 | 404 | 40400 | 路径不存在 | | 方法不允许 | 405 | 40500 | 方法不允许 | | 未处理异常 | 500 | 50000 | 系统繁忙 |
业务码只使用上表这些。除参数校验外,失败时 data 为 null。路径使用 /api/v1。
安全与追踪
无状态会话,关闭 CSRF。票放在 Authorization 头,不放 Cookie。CORS 只允许 http://localhost:5173,允许携带凭证。
白名单:
GET /api/v1/healthPOST /api/v1/auth/loginPOST /api/v1/auth/refresh/swagger-ui.html、/swagger-ui/**、/v3/api-docs/**/actuator/health、/actuator/health/**、/actuator/info
其余请求必须带访问票。票缺失、过期或签名错误时不抛 500,由 AuthenticationEntryPoint 返回 HTTP 401 和统一 JSON。
访问票是 JWT,主体是用户名,有效期 30 分钟。登录和刷新的响应都是:
{ "code": 0, "message": "success", "data": { "token": "...", "refreshToken": "..." } }
刷新请求是 POST /api/v1/auth/refresh,请求体只有一个字段:
{ "refreshToken": "..." }
刷新票是 32 字节随机数的 Base64URL 字符串。服务器只存 SHA-256、用户名和过期时间。刷新时先删除旧记录,再签发一对新票。
前端把访问票存入 sessionStorage 的 token,把刷新票存入 sessionStorage 的 refreshToken。
每个请求有响应头 X-Trace-Id。外来值只接受 8 到 64 位字母、数字和横线,否则自己生成。MDC 的键名必须是 traceId,与日志里的 %X{traceId:-} 一致,不能写成 trace-id 或 X-Trace-Id。请求结束必须移除这个键。过滤器优先级高于安全过滤器。
前端
Vite 把 /api 代理到 http://localhost:8080。页面只请求 /api 开头的路径。
axios 超时 10 秒。请求拦截器读取 sessionStorage 的 token,放进 Authorization: Bearer。
成功响应是 HTTP 200 且 code 为 0。失败响应的 HTTP 状态不是 200,axios 会进入错误回调,不会进入检查 code 的成功回调。从 error.response.data 读取 code、message 和 data.fields,字段错误不能丢。不要在成功回调里用 code !== 0 判断失败。
账号或密码错误、未登录都是 HTTP 401 加 code 40100。是否刷新只看请求路径,不看 code。只有受保护接口返回的 401 才刷新。/api/v1/auth/login 与 /api/v1/auth/refresh 返回的 401 不得再触发刷新。刷新成功后只重试原请求一次。并发的 401 共用同一次刷新结果,不能各自拿旧票去刷新。刷新失败就同时删除 token 和 refreshToken。
前端只有一个路由 /,组件是 HomeView.vue。不要登录页、后台页和路由守卫。这一页同时放健康状态、登录表单、每个输入框自己的字段错误,以及登录成功后的当前用户名。登录成功后就在这一页请求 /api/v1/me,把返回的用户名显示出来。
容器
后端 Dockerfile 先复制 pom.xml 与 Maven Wrapper 下载依赖,再复制源码打包。前端 Dockerfile 先复制 package.json 与锁文件执行 npm ci,再复制源码构建。Nginx 把 /api/ 转到 Compose 服务 backend:8080,其余路径回退到 index.html。
docker-compose.yml 放在仓库根目录,环境变量直接写在这个文件里,不另建 .env。后端使用 prod,前端发布在 8088:80。本地开发用下面这组值,密钥只给本机 Compose 用:
services:
backend:
environment:
SPRING_PROFILES_ACTIVE: prod
APP_AUTH_USERNAME: admin
APP_AUTH_PASSWORD: admin123
APP_JWT_SECRET: compose-only-secret-change-me-32b
web:
ports:
- "8088:80"
APP_JWT_SECRET 这一值长度为 34,满足不少于 32 个字符。正式环境换成别的密钥,不要把这组值当成生产口令。
本地开发仍用:
cd JavaFrame
.\mvnw.cmd spring-boot:run
前端另开终端执行 npm run dev。不要用编辑器里的运行按钮,它会沿用旧依赖清单。
还不要做
Prometheus、登录限流、登出接口、过期刷新票的定时清理。登出如果要做,必须先让服务器删除刷新票指纹,再清浏览器里的两张票。数据库出现之后才加 Flyway。
<!-- OB-S:ZrUrEyDprVC5eAYEdIbUHA -->
微信扫一扫