← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

JavaFrame 前后端分离空框架

生成或核对 JavaFrame 前后端分离空框架时使用。适用于 Spring Boot 4.1.1 与 Vue 3.5 的骨架,包含统一返回、Argon2id、JWT 与刷新票、追踪号、Actuator 和 Docker。不用于业务功能、数据库表、微服务或高并发调参。

personAuthor: ReveriethubModelScope

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。

  1. 第一段没有 profile,放公共配置。
  2. 第二段 spring.config.activate.on-profile: dev。
  3. 第三段 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/health
  • POST /api/v1/auth/login
  • POST /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 -->