Java 项目实战: 交友项目 - 完善用户信息章节复盘:套路清单与自检清单

概述

之前每个环节拆开讲过了:OSS 上传、百度人脸校验、资料保存与查询更新、统一 token、统一异常。本篇不重复细节,只做三件事:把这五个板块串成一条线、沉淀可复用的套路、给出一份可以直接勾选的自检清单。后续写圈子功能之前,建议先拿它过一遍工程。

纲要

  • 产出与知识地图(五个板块的依赖关系)
  • 可复用套路清单:第三方接入五步法 / VO 设计 / 拦截器 + ThreadLocal / 统一异常 / 增量更新
  • 关键坑回顾:10 个实测踩过或埋着的坑
  • 自检清单:8 项可验证的完成度检查
  • 练习建议:4 个可落地的改造任务(改哪里、怎么验)
  • 常见误解澄清与后续预告

本次产出与知识地图

一句话概括:接入三套第三方能力组件(OSS 存储、百度人脸,加上之前的短信),跑通资料完整链路(完善 → 上传头像 → 查询 → 更新),并完成两项横切优化(统一鉴权、统一异常处理)。

五个板块不是并列关系,画成依赖图更清楚:
#mermaid-svg-Ma7HiZJ9gJcL2VO7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .error-icon{fill:#552222;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .marker.cross{stroke:#333333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 p{margin:0;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .cluster-label text{fill:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .cluster-label span{color:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .cluster-label span p{background-color:transparent;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .label text,#mermaid-svg-Ma7HiZJ9gJcL2VO7 span{fill:#333;color:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node rect,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node circle,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node ellipse,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node polygon,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .rough-node .label text,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node .label text,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .image-shape .label,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .rough-node .label,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node .label,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .image-shape .label,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .icon-shape .label{text-align:center;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node.clickable{cursor:pointer;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .arrowheadPath{fill:#333333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .cluster text{fill:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .cluster span{color:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .icon-shape,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .icon-shape p,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .icon-shape .label rect,#mermaid-svg-Ma7HiZJ9gJcL2VO7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ma7HiZJ9gJcL2VO7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 复用
复用
第三方组件接入

OSS + 百度人脸
完善用户信息

保存资料 / 上传头像
用户信息管理

查询 / 更新
统一 token 处理

拦截器 + ThreadLocal
统一异常处理

BusinessException + Advice

组件是资料链路的前提------没有 OssTemplate.upload() 拿不到图片 URL,人脸检测连输入都没有;没有 AipFaceTemplate,头像上传的业务规则无法落地。两项优化放最后,不是它们不重要,而是必须先有写好的接口才有东西可改造 :拦截器改造的对象正是前面每个 Controller 方法里重复的 verifyToken 三行代码,异常处理器接管的是 updateHead 里抛出的 BusinessException。

工程到目前结束时的形态:

tree 复制代码
mf
├── mf-autoconfig        # 第三方组件封装(本次新增 Oss / AipFace 两套)
│   └── src/main/java/com/mf/autoconfig
│       ├── mfAutoConfiguration.java
│       ├── properties
│       │   ├── SmsProperties.java
│       │   ├── OssProperties.java
│       │   └── AipFaceProperties.java
│       └── template
│           ├── SmsTemplate.java
│           ├── OssTemplate.java
│           └── AipFaceTemplate.java
├── mf-model             # 实体与 VO(本次新增 UserInfo / UserInfoVo / ErrorResult)
│   └── src/main/java/com/mf/model
│       ├── domain
│       │   ├── User.java
│       │   ├── UserInfo.java        # 注意:直接 implements Serializable,没继承 BasePojo
│       │   └── BasePojo.java
│       └── vo
│           ├── UserInfoVo.java
│           └── ErrorResult.java
├── mf-dubbo
│   ├── mf-dubbo-interface   # UserInfoApi(findById / update / save)
│   └── mf-dubbo-db          # UserInfoApiImpl + UserInfoMapper
└── mf-app-server
    └── src/main/java/com/mf/server
        ├── controller
        │   ├── UserController.java       # loginReginfo / loginReginfo/head
        │   └── UsersControler.java       # 查询 / 更新(类名拼写就是这么写的)
        ├── service
        │   └── UserInfoService.java
        ├── interceptor                   # 本次新增
        │   ├── TokenInterceptor.java
        │   ├── UserHolder.java
        │   └── WebConfig.java
        └── exception                     # 本次新增
            ├── BusinessException.java
            └── ExceptionAdvice.java

可复用套路清单

这是真正值得带走的五条套路,每一条都至少会再用一次。

套路 本次怎么用 后续哪里还会用
第三方接入五步法:申请配置 → 先调通再封装 → 四件套(Properties / Template / AutoConfiguration / 注册文件)→ 业务只依赖 Template → 配置外置 + 最小权限 用了两次:OssProperties + OssTemplate、AipFaceProperties + AipFaceTemplate,都挂在 mfAutoConfiguration 上,靠 META-INF/spring.factories 注册 后续内容审核(图片文本鉴黄)完全照抄,还是这四件套
VO 设计 :接口只暴露该暴露的字段;实体过 Dubbo 必须 Serializable,VO 不过 Dubbo 就不需要序列化 UserInfo 过 Dubbo 所以实现 Serializable;UserInfoVo 只给前端,字段和类型跟接口文档对齐 圈子、小视频、消息所有对外接口都要配 VO
拦截器 + ThreadLocal 统一鉴权 :preHandle 校验并 set,afterCompletion 必须 remove TokenInterceptor 解析 token 存 UserHolder,Controller 里一行 UserHolder.getUserId() 替代原来三行重复代码 整个项目所有需要登录态的接口;换 Spring Security 前一直是主力方案
统一异常处理 :BusinessException 携带 ErrorResult + @RestControllerAdvice 按类型匹配;业务异常 warn、系统异常 error 且绝不返回堆栈 updateHead 检测不通过时 throw new BusinessException(ErrorResult.faceError()),ExceptionAdvice 兜底 后续每个业务校验失败都只写一行 throw
增量更新 :MyBatis-Plus updateById 动态 SQL,只更新非 null 字段 更新资料时只传要改的字段,null 字段不会把数据库值冲掉 用户资料、动态设置等所有局部更新场景

第三条里有个容易忽略的细节:afterCompletion 里的 remove 不是可选项。Tomcat 是线程池,线程处理完请求会复用,如果不清理,下一个落到同一线程的请求会拿到上一个用户的 id------串号比报错严重得多。

第四条的日志级别也是有讲究的:业务异常(用户传了张风景照)走 warn,系统异常(NPE)走 error 并且只返回 ErrorResult.error() 这种固定文案,堆栈绝不响应给前端。

关键坑回顾表

按「坑 → 症状 → 解法」列出来,复习时重点看第 6 行,那是工程里真实存在的安全洞。

坑 症状 解法
SpringMVC multipart 默认上限 1MB 上传正常照片报 MaxUploadSizeExceededException application.yml 配 spring.servlet.multipart.max-file-size
类型校验只看扩展名 改后缀的 a.jpg(实为 html)照样过校验 看 contentType / 魔数,扩展名只做参考
人脸检测 error_code == 0 ≠ 合规头像 双人合照也返回 0,检测通过 判定要用 result.face_num 等业务字段,不能只看错误码
OSS URL 双斜杠拼接 url 配置以 / 结尾,Template 又拼了一个 /,地址变成 ...com//2026/... 工程代码里 properties.getUrl() + "/" + filename 实测有此问题,二选一:配置去掉尾斜杠或拼接时去重
updateById 改不动 null 想把某字段清空,传 null 后数据库纹丝不动 动态 SQL 的特性不是 bug;清空要么用 UpdateWrapper set null,要么业务上不允许清空
更新接口白名单只在文档没在代码 PUT /users 直接收 UserInfo 实体,传 avatar 字段可绕过人脸校验直接改头像 改用 DTO 只收允许修改的字段(见下方练习一)
ThreadLocal 忘 remove 压测后偶现「用户 A 看到用户 B 的资料」 afterCompletion 里 UserHolder.remove(),并写自检项
@ControllerAdvice 覆盖不到 Controller 之前 拦截器里抛的异常不进 Advice,直接 500 白页 拦截器里自己 try-catch 或写 ErrorController;鉴权失败用 response.setStatus(401) 直接返回
JWT claims 数字类型强转 (Long) claims.get("id") 抛 ClassCastException jjw 库反序列化整型默认是 Integer,先 Number 再 longValue()
BeanUtils.copyProperties 静默不拷贝 UserInfo.age 是 Integer、VO 里是 String,age 字段一直是 null,不报错 同名不同类型不拷贝也不告警;类型对不上的字段手动 set(本次 findById 就是这么补的)

自检清单

对照勾选,全过才算真的完成:

检查项 怎么验
OSS 能上传并拿到可访问 URL 跑 OssTest,把输出的 URL 贴进浏览器能打开图片
人脸检测能区分自拍 / 风景 / 合照 三张图各调一次 detect(),风景返回 false,自拍返回 true;合照如果也返回 true,说明你踩了上表第 3 行
资料能保存且重复提交不炸 同一 token 连续 POST 两次 /user/loginReginfo,第二次观察主键冲突的处理方式
查询返回 VO 字段 GET /users 响应里没有 created / updated / tags,age 是字符串
更新只改允许改的字段 PUT /users 只传 nickname,查库确认其他字段没被冲掉
不带 token 返回 401 去掉 Authorization 请求头请求任意接口,状态码 401 且不进 Controller
业务异常返回统一结构 传一张风景照做头像,响应体是 {"errCode":"000003","errMessage":"图片非人像,请重新上传!"} 而不是堆栈
ThreadLocal 无残留 在 afterCompletion 打日志确认每次请求结束都执行了 remove

练习建议

四个可以直接动手的改造任务,每个都给验收标准。

任务一:修掉更新接口的白名单漏洞。 UsersControler.updateUserInfo() 现在 @RequestBody UserInfo 直接收实体,实测请求体里带上 avatar 字段就能绕过人脸校验改头像。改法:新建 UserInfoUpdateDto,只声明 nickname、birthday、gender、city、income、education、profession、marriage 这些接口文档允许的字段,Controller 收 DTO 再转实体。验证:用 Postman 传 {"avatar":"https://evil.com/x.jpg"},查库确认 avatar 没变。

任务二:补上自动填充。 工程 UserInfo 是直接 implements Serializable,没继承 BasePojo,所以 created / updated 全靠数据库默认值,updated 在更新时不会自动刷新。改法:让 UserInfo extends BasePojo,配合 MyBatis-Plus 的 MetaObjectHandler 做插入/更新自动填充。验证:改一次昵称,查库确认 updated 变了。注意权衡:继承后这两个字段参与序列化,确认 Dubbo 传输没受影响。

任务三:异常状态码分流。 ExceptionAdvice 现在把业务异常也映射成 500,前端没法区分「用户的错」和「服务器的错」。改法:业务异常返回 200 携带 errCode 或 400 段状态码,系统异常保持 500------具体跟前端约定好即可,关键是别让两类异常共用一个状态码。验证:风景照头像返回 4xx/200+错误码,人为制造一个 NPE 返回 500。

任务四:错误码常量化。 ErrorResult 里 "999999"、"000003" 全是魔法数字字符串散落在静态方法里。改法:定义 ErrorCode 枚举,收编 code 和默认 message,ErrorResult.error() 等静态方法改从枚举构造。验证:全局搜索确认不再有裸的错误码字符串。

常见误解澄清

误解 实际情况
「ThreadLocal 存的数据会随请求结束自动清理」 不会。清理靠你在 afterCompletion 手动 remove,Tomcat 线程池复用线程,不清理就串号
「error_code == 0 表示图片合规」 只表示「接口调用成功、检测到了人脸」,双人合照也返回 0;合规判定要看 face_num 等业务字段
「实体直接当请求参数最方便」 实体字段集是数据库视角,不是接口契约。直收实体的接口,攻击者多传一个字段就多一个攻击面
「Serializable 加在哪都行」 只有真正过 Dubbo 网络传输的对象(实体)必须要;VO 只在本进程内序列化给前端,加不加都不影响功能
「@RestControllerAdvice 能接住所有异常」 只能接住 DispatcherServlet 分发之后(Controller 及视图渲染阶段)的异常,拦截器、过滤器阶段的不归它管
「updateById 传 null 会把字段清空」 恰好相反,动态 SQL 会跳过 null 字段,「改不动 null」;真要清空得用 UpdateWrapper
「封装 Template 是过度设计」 Template 隔离的是第三方 SDK 的初始化和协议细节。直接在 Service 里 new AipFace,换厂商时你要改所有业务类;用 Template 只改一处

后续预告

接下来是 MongoDB 基础与圈子功能。本节的产物会直接被复用:第三方接入五步法照搬到 MongoDB 的配置管理上(届时用 @ConfigurationProperties 封装连接参数的思路一致),圈子的所有对外接口继续走 VO + UserHolder.getUserId() 取当前用户,发布动态的校验失败继续 throw new BusinessException(...)。把本节的自检清单过完再往前走,成本最低。

总结

  • 我们的主线是「组件 → 链路 → 横切」:OSS 和百度人脸是砖,资料四接口是墙,统一鉴权和统一异常是水电改造。
  • 五步法是含金量最高的套路,OSS、百度人脸验证了两次,内容审核会是第三次。
  • 坑清单里最值得动手的是更新接口的 avatar 绕过漏洞------白名单必须落在代码里,文档防不了攻击者。
  • 四个练习任务都来自工程里真实存在的代码,改完拿自检清单回归一遍。
相关推荐
用户3126874877201 小时前
Java 11-21 新特性盘点:var、record、sealed、虚拟线程
java
TangGeeA1 小时前
命令安全设置
java·服务器·安全
欣栀♡1 小时前
Spring 循环依赖不止三级缓存,Boot 2.6 起这个开关默认已经关了
java·spring·缓存
SL_staff1 小时前
3分钟上线新规则:业务人员拖拽完成合同超期自动暂停服务
java·mysql·全栈
Android小码家1 小时前
Vehicle 实战 自定义属性+测试demo
java·c++·framework·vhal·vehicle
范同学~1 小时前
固定表头+第一列+最后一列
java·服务器·前端
随风爱学习2 小时前
JAVA学习之异常
java
m0_639906942 小时前
HashMap:哈希扰动、扩容源码、红黑树转换、并发问题
java·算法·哈希算法
Roll圈圈2 小时前
长链接太丑影响转化率?这个短链工具让你的推广链接更专业
java