13.构建登录接口响应数据

构建登录接口响应数据

模块:ai-springboot-new

涉及接口:POST /api/user/login

文档日期:2026-09-05

一、本次变更目标

登录接口在校验通过后,需要向前端返回三类数据:

  1. JWT token ------ 后续请求的身份凭证
  2. 角色类型 ------ 前端据此渲染不同菜单
  3. 用户详细信息 ------ 用于页面展示,且不能包含密码

本次变更的核心是 UserService.login() 的最后三行(49-51 行),以及为让它跑通而修复的 JWT 配置绑定问题。

二、变更清单

文件 变更类型 说明
config/JwtConfig.java 修复 @ConfigurationProperties 补充 prefix = "jwt"
service/UserService.java 新增 新增 login() 方法,含 token 生成与响应组装
service/convert/UserConvert.java 新增 entityToDetailResponse() / entityToLoginResponse()
DTO/response/UserLoginResponseDTO.java 新增 登录响应数据结构
controller/UserController.java 新增 暴露 POST /api/user/login

三、关键修复:JWT 配置无法绑定

问题现象

复制代码
java.lang.RuntimeException: 生成token失败java.lang.IllegalArgumentException: The Secret cannot be null
	at com.example.aispringbootnew.util.JwtTokenUtil.generateToken(JwtTokenUtil.java:49)

原因

JwtConfig 上的 @ConfigurationProperties 未指定 prefix ,Spring Boot 只会去绑定配置文件的顶层 属性 secretexpiration 等,而 application.yml 中这些配置位于 jwt: 节点下,导致全部绑定失败,secret 保持 null

修复方式

java 复制代码
@Component
@ConfigurationProperties(prefix = "jwt")   // 补上 prefix
public class JwtConfig {
    private String secret;
    private long expiration;
    private long refreshExpiration;
    private String header;
    private String tokenPrefix;
}

对应 application.yml

yaml 复制代码
jwt:
  secret: MySecretKeyForJWT2025!@#$%^&*()_+SecureKeyHere
  expiration: 86400000          # 24 小时(毫秒)
  refresh-expiration: 604800000 # 7 天(毫秒)
  header: Authorization
  token-prefix: "Bearer "

四、数据流转链路

复制代码
前端 POST /api/user/login  { username, password }
        │
        ▼
UserController.login(@Valid UserLoginCommandDTO)     ← 参数校验
        │
        ▼
UserService.login(commandDTO)
        │
        ├─ 1. 按 username 或 email 查询用户 → User(含 password)
        ├─ 2. 校验密码 / 账号状态
        ├─ 3. JwtTokenUtil.generateToken(...)        → String token
        ├─ 4. UserConvert.entityToDetailResponse(user)
        │         └─ User → UserDetailResponseDTO(脱敏 + 补文案)
        └─ 5. UserConvert.entityToLoginResponse(token, userInfo)
                  └─ 组装 → UserLoginResponseDTO
        │
        ▼
Result.success(result)  →  JSON  → 前端

五、核心代码说明

5.1 Controller 层

java 复制代码
@PostMapping("/login")
public Result<UserLoginResponseDTO> login(@Valid @RequestBody UserLoginCommandDTO commandDTO) {
    UserLoginResponseDTO result = userService.login(commandDTO);
    return Result.success(result);
}

@Valid 触发 UserLoginCommandDTO 上的 @NotBlank / @Size 校验,非法参数在进入 Service 前即被拦截。

5.2 Service 层(变更主体,49-51 行)

java 复制代码
// 生成JWT token
String token = JwtTokenUtil.generateToken(
        String.valueOf(user.getId()), user.getUsername(), user.getUserType());
System.out.printf("token:%s\n", token);
UserLoginResponseDTO.UserDetailResponseDTO userInfo = UserConvert.entityToDetailResponse(user);
return UserConvert.entityToLoginResponse(token, userInfo);

三步职责分离:

  • 第 48 行 :生成 token,载荷包含 userIdusernameroleType
  • 第 50 行:实体转用户信息 DTO(去掉密码)
  • 第 51 行:token + 用户信息 → 完整登录响应

5.3 Token 生成

JwtTokenUtil 是实现了 ApplicationContextAware 的静态工具类,通过持有 ApplicationContext 获取 Spring 管理的 JwtConfig Bean:

java 复制代码
Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getSecret());
Date expiration = new Date(System.currentTimeMillis() + jwtConfig.getExpiration());

String token = JWT.create()
        .withClaim("userId", userId)
        .withClaim("username", username)
        .withClaim("roleType", roleType)
        .withExpiresAt(expiration)
        .withIssuedAt(new Date())
        .withIssuer(ISSUER)
        .sign(algorithm);

5.4 Convert 层:实体转 DTO

entityToDetailResponse 做两件事:

脱敏 ------ 不调用 .password(...),密码天然不会流出到前端。

补文案 ------ 填充数据库没有的展示字段:

目标字段 来源
genderDisplayName getGenderDisplayName()1→男,2→女,null/其他→未知
userTypeDisplayName User.getUserTypeDisplayName(),枚举转换
statusDisplayName User.getStatusDisplayName(),枚举转换
displayName User.getDisplayName(),优先昵称,否则用户名

entityToLoginResponse 完成最终组装:

java 复制代码
return UserLoginResponseDTO.builder()
        .userInfo(userInfo)
        .token(token)
        .roleType(userInfo.getUserType().toString())
        .build();

六、响应数据结构

json 复制代码
{
  "code": "200",
  "msg": "操作成功",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "roleType": "1",
    "userInfo": {
      "id": 1,
      "username": "zhangsan",
      "email": "zs@example.com",
      "nickname": "张三",
      "avatar": null,
      "phone": "13800138000",
      "gender": 1,
      "genderDisplayName": "男",
      "birthday": "2000-01-01",
      "userType": 1,
      "userTypeDisplayName": "普通用户",
      "status": 1,
      "statusDisplayName": "正常",
      "displayName": "张三",
      "createdAt": "2026-09-05T10:00:00",
      "updatedAt": "2026-09-05T10:00:00"
    }
  }
}

外层 code / msg 来自 common/ResultCode 枚举(SUCCESS("200", "操作成功")ERROR("-1", "操作失败"))。

七、待优化项

以下问题在本次变更范围内发现,建议后续处理:

  1. 密码校验逻辑写反(严重)

    java 复制代码
    if (passwordEncoder.matches(inputPassword, user.getPassword())) {
        throw new BusinessException("密码错误");   // 密码正确时反而报错
    }

    应为 if (!passwordEncoder.matches(...))。当前状态下正确密码无法登录,错误密码反而放行。

  2. roleType 空指针隐患

    userInfo.getUserType().toString()userTypenull 时会抛 NPE,建议改为 String.valueOf(userInfo.getUserType()) 或加判空。

  3. 调试代码残留

    UserService 第 32、49 行与 UserController 第 23、24、27 行的 System.out.println 需清理,其中打印密码存在安全风险,应改用日志框架。

  4. DTO 缺少无参构造

    UserLoginResponseDTO 仅标注 @Data + @Builder,若后续需要反序列化,需补充 @NoArgsConstructor + @AllArgsConstructor

  5. 密钥硬编码

    jwt.secret 明文写在配置文件中,建议改为 ${JWT_SECRET} 环境变量注入。

相关推荐
水巷石子1 小时前
学习langChain4j的第二天,体验springBoot中的starter
java·spring boot·学习·spring·langchain4j
ly76891 小时前
Spring Bean生命周期全流程:从BeanDefinition到销毁
java·后端·spring·bean生命周期·beandefinition·初始化回调
摇滚侠2 小时前
《Spring Boot 3:高级与架构设计》第 1 章 BeanDefinitionRegistry 阅读笔记 3
spring boot·笔记·后端
君顾12 小时前
智慧场馆解决方案小程序系统实战:从架构设计到上线指南
java·开发语言·智慧场馆
zhougl9962 小时前
Dockerfile实战教程
java·开发语言·spring boot
新时代牛马2 小时前
Linux 驱动调试完整篇:从printk/dev_dbg、动态调试到 debugfs/ftrace 排障
java·linux·服务器
计算机毕设定制辅导-无忧学长2 小时前
《基于Spring Boot传承之光非遗陶瓷烧造产品交易平台的设计与实现》
java·vue.js·spring boot·后端·毕业设计
Zane19942 小时前
内存都要回收,为什么JVM偏要把堆分成新生代和老年代
java·后端