web-prd 是 EasyFK 框架为独立部署(非微服务/无网关)的 Web 应用提供的完整 Web 基础设施模块。与 web-micro(网关后端微服务)不同,web-prd 内置了完整的安全防护链(签名验证、防重放攻击)、本地权限拦截器、Controller 层 AOP 切面、CORS 跨域过滤器、时间参数转换器、权限资源自动初始化等能力,适用于面向前端直接访问的独立应用。
dependencies {
api project(':easyfk-web:web-simple')
api project(':easyfk-web:web-common')
}web-prd
├── web-simple
│ ├── spring-boot-starter-web(排除 Tomcat + Logging)
│ ├── spring-boot-starter-undertow
│ └── easyfk-core
└── web-common
├── web-base
│ ├── easyfk-authority
│ └── easyfk-core
├── easyfk-core
└── spring-webweb-prd/
├── config/
│ ├── PrdWebConfig.java # 核心自动配置
│ ├── WebInterceptorRegister.java # 拦截器注册
│ └── ControllerAspectConfigure.java # Controller AOP 配置
├── filter/
│ └── RequestBaseFilter.java # 请求前置过滤器(含安全链)
├── interceptor/
│ └── LocalInterceptor.java # 本地权限拦截器
├── exception/
│ └── PrdExceptionHandler.java # 全局异常处理
├── aspect/
│ └── ControllerAspect.java # Controller 方法拦截切面
├── converter/
│ ├── DateConverter.java # String → Date
│ ├── LocalDateConverter.java # String → LocalDate
│ ├── LocalDateTimeConverter.java # String → LocalDateTime
│ └── LocalTimeConverter.java # String → LocalTime
├── runner/
│ └── InitResourceRunner.java # 权限资源自动初始化
├── properties/
│ ├── PrdWebProperties.java # PRD Web 配置
│ ├── ResourceInitProperties.java # 资源初始化配置
│ └── ControllerAspectProperties.java # 切面配置
├── util/
│ ├── FailRequestUtil.java # 失败响应工具
│ ├── RequestUtil.java # 请求工具(URL/Ajax/微信检测)
│ └── StaticUriUtil.java # 静态资源判断
└── vo/
├── LoginRequestVO.java # 登录请求 VO
├── SiteLoginSuccessVO.java # 登录成功响应 VO
└── RegistryRequestVO.java # 注册请求 VO配置前缀:easyfk.config.web.prd
| `open-interceptor` | Boolean | `true` | 是否开启本地拦截器 |
|---|---|---|---|
| `pic-code-cache-name` | String | `PicCodes` | 图片验证码缓存名称 |
| `code-time-to-live` | Duration | 5min | 图片验证码有效期 |
配置前缀:easyfk.config.resource.init
| `open` | Boolean | `false` | 是否启用权限资源自动初始化 |
|---|---|---|---|
| `normal` | Boolean | `false` | 是否生成非权限资源(`@LoginResource` 标记的) |
配置前缀:easyfk.config.aspect.controller
| `open` | Boolean | `true` | 是否开启 Controller 切面 |
|---|
来自 web-common:
| `easyfk.config.web.security.*` | 安全配置(签名、防重放) |
|---|---|
| `easyfk.config.web.cors.*` | 跨域配置 |
| `easyfk.config.web.jwt.*` | JWT 配置 |
easyfk:
config:
web:
prd:
open-interceptor: true
open-filter: true
pic-code-cache-name: PicCodes
code-time-to-live: 5m
security:
open: true
sign-key: "your-sign-key"
sign-key-dynamic: false
timeout: 150s
ignore-uri:
- /api/public
- /health
intercept:
exclude-paths:
- /api/login
- /api/register
auth-type: 1
refresh-user-auth: true
cors:
open: true
cors-domain: "https://yourdomain.com"
jwt:
secret: "your-jwt-secret"
aspect:
controller:
open: true
pointcut: "execution(public * com.example..*.controller..*.*(..))"
resource:
init:
open: true
group: default
normal: false注册的 Bean:
| `dateConverter` | `Converter<String, Date>` | 无条件 | Date 参数转换 |
|---|---|---|---|
| `localDateTimeConverter` | `Converter<String, LocalDateTime>` | 无条件 | LocalDateTime 参数转换 |
| `localTimeConverter` | `Converter<String, LocalTime>` | 无条件 | LocalTime 参数转换 |
| `apiInterceptorRegister` | `WebInterceptorRegister` | `open-interceptor=true`(默认生效) | 拦截器注册 |
| `localInterceptor` | `LocalInterceptor` | `open-interceptor=true` | 本地权限拦截器 |
| `requestBaseFilter` | `RequestBaseFilter` | `open-filter=true`(默认生效) | 请求前置过滤器 |
| `prdExceptionHandler` | `PrdExceptionHandler` | `@ConditionalOnMissingBean` | 全局异常处理 |
| `serializingObjectMapper` | `ObjectMapper` | `@ConditionalOnMissingBean` | Jackson 全局配置 |
| `myCorsFilter` | `CorsFilter` | `cors.open=true` | CORS 跨域过滤器 |
| `startedCallback` | `InitResourceRunner` | `resource.init.open=true` | 权限资源初始化 |
| `controllerInterceptorAdvisor` | `AspectJExpressionPointcutAdvisor` | `aspect.controller.open=true`(默认生效) | Controller AOP 切面 |
|---|
web-prd 的核心过滤器,继承 OncePerRequestFilter,包含完整的安全防护链:
HTTP 请求到达
│
├─ TRACE 方法 → 返回 405(安全防护)
│
├─ 静态资源(StaticUriUtil.isStaticUrl)
│ └─ 直接 filterChain.doFilter()(跳过全部逻辑)
│
└─ 非静态请求
│
├─ 1. createTraceId()(生成全链路追踪 ID)
│
├─ 2. doLocalHeaderFilter()(提取 10 个请求头字段)
│ ├─ accessToken(优先 URL 参数,其次 Header)
│ ├─ language / resetSign / paramSign
│ ├─ timestamp / requestToken / accessToken
│ ├─ requestNonce / deviceNo / clientType
│ └─ → RequestHeaderContext(ThreadLocal)
│
├─ 3. doDataContext()(JWT 解析 + 用户数据加载)
│
├─ 4. doSecurityFilterMethod()(安全签名校验)
│ ├─ 忽略白名单 URI → 放行
│ ├─ 检查请求头完整性(headers/nonce/sign/timestamp)
│ ├─ 获取签名密钥(静态或动态)
│ ├─ MD5 签名验证
│ └─ Nonce 防重放校验(缓存检查 + 写入)
│
├─ 5. filterChain.doFilter()
│
└─ finally: clearContext()| `accessToken` | `Access-Token`(或 URL 参数 `accessToken`) | 认证令牌 |
|---|---|---|
| `resetSign` | `Reset-Sign` | 请求签名 |
| `paramSign` | `Param-Sign` | 参数签名 |
| `timestamp` | `Timestamp` | 时间戳 |
| `requestToken` | `Request-Token` | 请求令牌 |
| `requestNonce` | `Request-Nonce` | 防重放随机串 |
| `deviceNo` | `Device-No` | 设备编号 |
| `clientType` | `Client-Type` | 客户端类型 |
签名原文 = "Request-Nonce={nonce}&Timestamp={timestamp}&Key={signKey}"
签名结果 = MD5(签名原文)
验证条件 = 签名结果 == headers.getResetSign()1. cacheService.existKey(nonce) → 已存在 → API_AGAIN 错误
2. 不存在 → cacheService.cacheObject(nonce, timeout) → 放行
3. timeout 后 nonce 自动过期基于 HandlerInterceptor 实现注解驱动的权限校验:
请求到达 Controller 方法
│
├─ URI 在排除列表中 → 放行
│
├─ handler 是 HandlerMethod?
│ │
│ ├─ 有 @LoginResource 或 @AuthResource?
│ │ │
│ │ ├─ UserData 为空 → 返回 UN_LOGIN
│ │ │
│ │ ├─ 有 @AuthResource?
│ │ │ │
│ │ │ ├─ 检查 authCachedKey → UserAuth 为空 → UN_LOGIN
│ │ │ │
│ │ │ └─ authType != 0?
│ │ │ ├─ checkAuth(key, url) 成功 → 放行
│ │ │ └─ 失败 → 返回 UN_AUTH + 自定义消息
│ │ │
│ │ └─ 只有 @LoginResource → 有 UserData 即放行
│ │
│ └─ 无权限注解 → 放行
│
└─ 非 HandlerMethod → 放行基于 MethodInterceptor 实现 Controller 层方法拦截:
默认切面表达式:
execution(public * com.mcst..*.controller..*.*(..))可通过 pointcut 配置追加自定义表达式(OR 合并)。
功能:
五级异常分类处理:
| `BindException` / `ValidationException` | `RRBuilder.buildFailByException(e)` 提取校验错误 |
|---|---|
| `HttpRequestMethodNotSupportedException` | 返回方法不支持消息 |
| `SocketTimeoutException` | I18N 消息 `ServerBusyMsg`,状态码 BUSYNESS |
| `Exception`(兜底) | I18N 消息,智能识别日期反序列化错误 |
智能日期错误识别:
if (errMsg.contains("Failed to deserialize java.time.LocalDate")) {
code = "DateFormatErrorMsg";
} else if (errMsg.contains("Failed to deserialize java.time.LocalDateTime")) {
code = "DateTimeFormatErrorMsg";
} else if (errMsg.contains("Failed to deserialize java.time.LocalTime")) {
code = "TimeFormatErrorMsg";
}启动时自动扫描带有 @ResourceController 注解的 Controller,提取权限资源并批量保存:
应用启动(CommandLineRunner,Order=0)
│
├─ open=false → 跳过
│
├─ 扫描 @ResourceController Bean
│ ├─ group="default" → 全部
│ └─ group="xxx" → 按组过滤
│
└─ 遍历每个 Controller
│
├─ 创建菜单资源(@ResourceController 元数据)
│
└─ 遍历所有方法
├─ 解析 URL(@GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@RequestMapping)
├─ 跳过通配路径(*、{参数})
│
├─ 有 @AuthResource → 创建权限资源
└─ 无 @AuthResource + normal=true
├─ 有 @LoginResource → level=1(需登录)
└─ 无注解 → level=0(公开)四种 Spring MVC 参数转换器,处理前端字符串形式的时间参数:
| `DateConverter` | `yyyy-MM-dd HH:mm:ss` / `yyyy-MM-dd` / `HH:mm:ss`(自动识别长度) | `Date` |
|---|---|---|
| `LocalDateTimeConverter` | `yyyy-MM-dd HH:mm:ss` | `LocalDateTime` |
| `LocalTimeConverter` | `HH:mm:ss` | `LocalTime` |
// 构建失败响应对象
ResponseResult<?> result = FailRequestUtil.failRequestInfo(ErrorRequest.SIGN_ERROR);
// 直接写入 HTTP 响应
FailRequestUtil.failRequest(httpServletResponse, ErrorRequest.UNLOGIN);
FailRequestUtil.failRequest(httpServletResponse, responseResult);String url = RequestUtil.getRequestUrl(request); // 获取去除 contextPath 后的 URI
boolean isAjax = RequestUtil.isAjax(request); // 判断 Ajax 请求
boolean isWeChat = RequestUtil.isWeChatRequest(request); // 判断微信浏览器
boolean isQQ = RequestUtil.isQqRequest(request); // 判断 QQ 浏览器boolean isStatic = StaticUriUtil.isStaticUrl(url);匹配 /static/ 前缀和静态文件扩展名。
| `loginAccount` | 账号/手机号/邮箱 | `@NotBlank` |
|---|---|---|
| `picToken` | 图形验证码 Token | — |
| `picCode` | 图片验证码 | — |
| `smsCode` | 手机验证码 | — |
| `type` | 登录类型:`accountAndPic` / `account` / `mobile` | — |
| `returnUrl` | 登录成功跳转 URL | — |
| `accessToken` | JWT Token |
|---|---|
| `returnUrl` | 跳转 URL |
| `status` | 状态码 |
| `phone` | 手机号码 |
|---|---|
| `loginPwd` | 登录密码(密文) |
| `email` | 邮箱 |
| `name` | 用户名 |
| `referrerId` | 推荐人 |
| `registryChannel` | 注册渠道 |
通过 easyfk.config.web.cors.open=true 启用:
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOriginPattern(corsProperties.getCorsDomain());
corsConfiguration.addAllowedHeader(corsProperties.getAllowedHeader());
corsConfiguration.addAllowedMethod(corsProperties.getAllowedMethod());
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);使用 addAllowedOriginPattern 替代 addAllowedOrigin,支持通配符模式匹配。
easyfk-web-prd — 企业级 Web 应用基础设施。