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 只会去绑定配置文件的顶层 属性 secret、expiration 等,而 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,载荷包含 userId、username、roleType
  • 第 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() 在 userType 为 null 时会抛 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} 环境变量注入。

相关推荐
猿究院_夏燊11 小时前
Spring Bean 生命周期
java·后端·spring
Wang's Blog11 小时前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
信誓旦旦的程序猿12 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河12 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
Jlzn888812 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
香吧香12 小时前
GRPC连接机制---长连接分析
java·网络与传输协议
我是人✓13 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端
Wang's Blog13 小时前
Java 项目实战: 交友项目 - 阿里云 OSS 入门案例:从创建 Bucket 到上传拿到可访问 URL
java·阿里云·交友
裕晟资质规划14 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全
念念清晰14 小时前
000-OpenJDK17 源码构建:MacOS环境(M芯片)
java·后端