构建登录接口响应数据
模块:
ai-springboot-new涉及接口:
POST /api/user/login文档日期:2026-09-05
一、本次变更目标
登录接口在校验通过后,需要向前端返回三类数据:
- JWT token ------ 后续请求的身份凭证
- 角色类型 ------ 前端据此渲染不同菜单
- 用户详细信息 ------ 用于页面展示,且不能包含密码
本次变更的核心是 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", "操作失败"))。
七、待优化项
以下问题在本次变更范围内发现,建议后续处理:
-
密码校验逻辑写反(严重)
javaif (passwordEncoder.matches(inputPassword, user.getPassword())) { throw new BusinessException("密码错误"); // 密码正确时反而报错 }应为
if (!passwordEncoder.matches(...))。当前状态下正确密码无法登录,错误密码反而放行。 -
roleType空指针隐患userInfo.getUserType().toString()在userType为null时会抛 NPE,建议改为String.valueOf(userInfo.getUserType())或加判空。 -
调试代码残留
UserService第 32、49 行与UserController第 23、24、27 行的System.out.println需清理,其中打印密码存在安全风险,应改用日志框架。 -
DTO 缺少无参构造
UserLoginResponseDTO仅标注@Data+@Builder,若后续需要反序列化,需补充@NoArgsConstructor+@AllArgsConstructor。 -
密钥硬编码
jwt.secret明文写在配置文件中,建议改为${JWT_SECRET}环境变量注入。