AI智慧社区--实现修改密码、退出登录、动态路由

前置说明

本篇接上一篇(登录、验证码、token校验),继续完成三个接口:动态路由、修改密码、退出登录。如果你已经跟着上篇搭好了项目骨架(SpringBoot + MyBatis + Redis + JWT),这里可以直接继续看第三步,但鉴于很多新手朋友是开发初学者,我们在这里还是简单介绍一下对照文档开发的思路和框架的基本知识。

一、核心开发思路:拿到接口文档后怎么想?

1.前端三样东西:

  • 请求方式和URL(如 PUT /sys/user/updatePassword
  • 参数(请求体 JSON 有哪些字段)
  • 返回值(响应 JSON 长什么样)

2.后端

其实就是:接参数 → 处理业务逻辑 → 返回约定格式的 JSON。每个】接口的开发步骤基本一致:

复制代码
1. 看 URL 和请求方式 → 确定 Controller 里用 @GetMapping / @PostMapping / @PutMapping
2. 看参数 → 确定方法入参用什么接收(@RequestBody、@RequestParam、HttpServletRequest 等)
3. 看返回值 → 确定要查什么数据、做什么处理
4. 如果涉及数据库操作 → 写 Mapper 接口 + XML 中的 SQL
5. 用 Postman 测试 → 对照文档检查返回格式是否一致

二、涉及的技术术语和原理

1. Controller 层注解

复制代码
@RestController  // 标记这个类是控制器,所有方法返回值自动转 JSON(不走页面渲染)
@GetMapping      // 处理 GET 请求
@PostMapping     // 处理 POST 请求
@PutMapping      // 处理 PUT 请求(通常用于"修改"操作)

其中为什么修改密码用 PUT 而不是 POST?这是 RESTful 风格的约定:POST 用于"创建",PUT 用于"更新"。前端文档写的是 PUT,后端就用 @PutMapping

2. 接收参数的几种方式

复制代码
// 方式一:用实体类接收 JSON 请求体
@PostMapping("/login")
public Result login(@RequestBody LoginForm form) { ... }

// 方式二:用 Map 接收 JSON 请求体(字段不多、不想专门建类时用)
@PutMapping("/sys/user/updatePassword")
public Result updatePassword(@RequestBody Map<String, String> params) { ... }

// 方式三:从请求头取值(比如 token)
public Result xxx(HttpServletRequest request) {
    String token = request.getHeader("token");
}

@RequestBody 的作用:告诉 Spring 把请求体里的 JSON 自动转成 Java 对象。没有这个注解,Spring 不知道去请求体里找数据,参数就全是 null。

3. JWT(JSON Web Token)

JWT 是一种无状态的认证方案。登录成功后服务端生成一个 token 返回给前端,之后前端每次请求都在 header 里带上这个 token,服务端解析 token 就知道"你是谁"。解析 token 的核心代码:

复制代码
Claims claims = Jwts.parser()
        .setSigningKey(SECRET)       // 用同一个密钥解密
        .parseClaimsJws(token)       // 解析 token
        .getBody();                  // 拿到里面存的数据
String userId = claims.getSubject(); // 取出用户ID

4. SHA-256 密码加密

数据库里不存明文密码,存的是经过 SHA-256 哈希后的字符串。校验密码时,把用户输入的明文密码也做一次 SHA-256,然后跟数据库里的比对:

复制代码
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
// 把 byte[] 转成十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
    String hex = Integer.toHexString(0xff & b);
    if (hex.length() == 1) hexString.append('0');
    hexString.append(hex);
}
// hexString.toString() 就是加密后的密码

5. MyBatis 的 Mapper 层

Mapper 是 MyBatis 里"Java 接口 + XML SQL"的组合,负责跟数据库打交道。Java 接口定义方法签名:

复制代码
@Mapper
public interface UserMapper {
    User selectByUsername(String username);
    User selectById(int id);
    void updatePassword(@Param("userId") Integer userId, @Param("password") String password);
}

XML 里写对应的 SQL:

复制代码
<select id="selectById" resultType="com.qcby.smartcommunity.entity.User">
    SELECT * FROM user WHERE user_id = #{id}
</select>

<update id="updatePassword">
    UPDATE user SET password = #{password} WHERE user_id = #{userId}
</update>

id必须跟Java接口的方法名一一对应,#{xxx}是MyBatis的参数占位符,会自动防SQL注入。

6. @Param 注解

当 Mapper 方法有多个参数时,必须用 @Param 给每个参数起名字,否则 MyBatis 在 XML 里找不到对应的值,单参数时可以不加,MyBatis 能自动识别。

复制代码
// 多参数必须加 @Param
void updatePassword(@Param("userId") Integer userId, @Param("password") String password);

三、三个接口的详细实现

接口1:动态路由 GET /sys/user/getRouters

**文档要求:**前端登录后请求这个接口,拿到菜单数据来渲染侧边栏。

思路: 从 token 中解析出用户ID-->查数据库拿到用户信息-->把用户信息 + 路由菜单数据组装成前端要的格式返回。这个接口的难点不在逻辑,在于返回的 JSON 结构比较复杂,是嵌套的。前端要的格式大概是这样:

复制代码
{
  "routers": [
    {
      "name": "系统管理",
      "path": "/sys",
      "component": "Layout",
      "children": [
        {
          "name": "管理员管理",
          "path": "/user",
          "component": "sys/user/index",
          "meta": { "title": "管理员管理", "icon": "user" }
        }
      ]
    }
  ]
}

在 Java 里没有现成的实体类对应这个结构,所以直接用 Map<String, Object> 手动拼装:

复制代码
// 子菜单
Map<String, Object> userMenu = new HashMap<>();
userMenu.put("name", "管理员管理");
userMenu.put("path", "/user");
userMenu.put("component", "sys/user/index");
Map<String, Object> userMeta = new HashMap<>();
userMeta.put("title", "管理员管理");
userMeta.put("icon", "user");
userMenu.put("meta", userMeta);

// 父菜单,children 里放子菜单
Map<String, Object> systemMenu = new HashMap<>();
systemMenu.put("children", Collections.singletonList(userMenu));

Collections.singletonList() 创建一个只有一个元素的 List,因为目前只有一个子菜单。

接口2:修改密码 PUT /sys/user/updatePassword

文档要求 :参数:password(旧密码)、newPassword(新密码)、confirmPassword(确认密码),需要 token 鉴权。完整业务流程:

复制代码
1. 从 header 取 token → 解析出 userId
2. 从请求体取三个密码字段
3. 校验 newPassword 和 confirmPassword 是否一致
4. 查数据库拿到用户信息
5. 把旧密码做 SHA-256 加密,跟数据库里的密码比对
6. 如果旧密码正确,把新密码做 SHA-256 加密,更新到数据库
7. 返回结果

这个接口涉及了一个新的 Mapper 方法 updatePassword,需要同时在 Java 接口和 XML 里添加。注意返回值的区别:旧密码错误时返回的不是 Result.error(),而是下面这种,这是因为前端是通过 status 字段判断密码是否正确的,不是通过 code。这种细节要看前端代码或者跟前端沟通确认。

复制代码
return Result.ok("操作成功").put("status", "passwordError");

接口3:退出登录 POST /logout

文档要求: 无参数,返回 {"msg":"操作成功","code":200}

复制代码
@PostMapping("/logout")
public Result logout(){
    return Result.ok("操作成功!");
}

为什么这么简单?因为 JWT 是无状态认证。token 存在前端,服务端不维护登录状态(不像 Session 方案需要服务端销毁 Session)。退出登录就是前端把本地存的 token 删掉,后端只需要返回一个"操作成功"让前端知道可以跳转到登录页了。

四、易错提醒

错误1:请求方式写错

文档写的是 PUT /sys/user/updatePassword,如果你用了 @PostMapping,Postman 用 PUT 请求会报 405 Method Not Allowed。一定要对照文档的请求方式选注解。

错误2:忘写 @RequestBody

复制代码
// 错误写法:参数全是 null
public Result updatePassword(Map<String, String> params) { ... }

// 正确写法:加上 @RequestBody
public Result updatePassword(@RequestBody Map<String, String> params) { ... }

没有 @RequestBody,Spring 不会去请求体里解析 JSON,拿到的 params 是空的。

错误3:Mapper 多参数不加 @Param

复制代码
// 错误:MyBatis 报 BindingException
void updatePassword(Integer userId, String password);

// 正确:多参数必须加 @Param
void updatePassword(@Param("userId") Integer userId, @Param("password") String password);

错误4:XML 里的 id 和 Mapper 方法名不一致

复制代码
<!-- 错误:id 写成了 updatePwd,但 Java 接口方法名是 updatePassword -->
<update id="updatePwd">
    UPDATE user SET password = #{password} WHERE user_id = #{userId}
</update>

MyBatis 是靠 id 和方法名匹配的,不一致就会报"找不到对应的 statement"。

错误5:SHA-256 加密逻辑不一致

登录时和修改密码时都要做 SHA-256 加密,加密逻辑必须完全一样。如果登录时用的是 password.getBytes(StandardCharsets.UTF_8),修改密码时也必须用同样的编码,否则同一个密码加密出来的结果不同,永远校验不过。

错误6:Postman 测试时忘记带 token

修改密码和动态路由接口都需要从 header 里取 token。在 Postman 里测试时,要在 Headers 里加一行:Key 填 token,Value 填登录接口返回的 token 值。不带 token 会返回"token为空"。

错误7:返回格式跟前端对不上

比如前端期望 "status": "success",你返回的是 "status": "ok",前端判断逻辑就走不通。一定要严格对照文档的返回格式,字段名、字段值都要一致。

五、新手常见疑问

Q:为什么有的接口逻辑写在 Controller 里,有的写在 Service 里?

A:规范做法是 Controller 只负责接收参数和返回结果,业务逻辑放 Service 层。但对于简单接口(比如 logout 就一行代码),直接写在 Controller 里也没问题。项目初期可以灵活处理,后期重构时再统一抽到 Service。

Q:为什么修改密码用 Map<String, String>接收参数,而不是像登录那样建一个 Form 类?

A:两种方式都可以。字段少且只有一个接口用到时,用 Map 更快。字段多或者多个接口复用时,建专门的 Form 类更清晰。这是开发效率和代码规范之间的取舍。

Q:为什么退出登录后端几乎什么都不做?

A:因为 JWT 是无状态的,服务端不存储登录信息。token 的"失效"是靠过期时间自动失效的,服务端没有"主动让某个 token 失效"的能力(除非引入 Redis 黑名单机制)。所以退出登录本质上是前端行为------删掉本地 token 就算退出了。

Q:每个需要鉴权的接口都要写一遍解析 token 的代码,能不能复用?

A:可以。常见做法是写一个拦截器(Interceptor)或者 AOP 切面,统一解析 token 并把用户信息放到请求上下文里,Controller 里直接取就行。当前阶段为了学习理解,每个接口手动解析也没问题,后面可以优化。

Q:Result.ok().put("data", xxx) 这个链式调用是怎么实现的?

A:Result 类继承了 HashMapput 方法返回的是 this(即 Result 对象本身),所以可以连续 .put().put() 链式调用。这是一个常见的 Builder 模式变体。

总结

写到这里,三个接口都跑通了,回头看其实也没有多难。但过程中确实踩了不少坑------忘加 `@RequestBody` 参数全是 null 的时候真的会怀疑人生,`@Param` 没写导致 MyBatis 报错也排查了好一会儿。这些东西看文档的时候觉得"我知道了",真正上手写才发现"知道"和"会用"之间还隔着好几个报错。

不过也正是这些错误让我对整个流程理解得更深了。现在再拿到一个新的接口文档,我已经能比较清晰地拆解了:先看请求方式和 URL 确定用什么注解,再看参数决定怎么接收,然后想清楚业务逻辑该查什么、改什么,最后把返回格式跟文档对齐。这个套路一旦跑通,后面的接口就是重复和熟练的过程。

把这些踩坑经历和思路整理出来,是希望跟我一样的新手少走一些弯路。如果你也是第一次对着接口文档写后端,遇到报错别慌,大概率就是那几个常见问题。一个一个排查,跑通第一个接口之后,后面会越来越顺的。

另外,针对这期实操,我整理了相关的面试题《写完动态路由、改密码、退出登录,我顺手整理了 10 个面试题》,感兴趣的小伙伴可以学习收藏~

相关推荐
白狐_7982 小时前
从零构建飞书 × OpenClaw 自动化情报站(二)
java·自动化·飞书
smxgn2 小时前
【SpringBoot整合系列】SpringBoot3.x整合Swagger
java·spring boot·后端
liuyao_xianhui2 小时前
动态规划_最大子数组和_C++
java·开发语言·数据结构·c++·算法·链表·动态规划
BingoGo2 小时前
告别阻塞!用 PHP TrueAsync 实现 PHP 脚本提速 10 倍
后端·php
KD2 小时前
阿里云服务器迁移实战(一)——Mysql平滑迁移
后端
清汤饺子2 小时前
Cursor 独有的 12 个技巧:这些是 Claude Code 没有的
前端·后端·ai编程
周末程序猿2 小时前
技术总结|十分钟了解Git的Worktree
后端·ai编程
倾颜2 小时前
零成本本地大模型!用 Next.js + Ollama + Qwen3 打造流式聊天应用
前端·后端·ai编程
Victor3562 小时前
MongoDB(45) 嵌入式文档与引用的优缺点是什么?
后端