Skip to main content
Version: 1.0

登录接口实现(Spring Boot)

后端接收登录请求的完整分层:Controller 只做「解析请求 → 调 Service → 格式化响应」,业务逻辑在 Service,数据访问在 Mapper。依赖一律构造器注入。

请求 DTO + 入参校验​

@Data
public class LoginDTO {

@NotBlank(message = "用户名不能为空")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 32, message = "密码长度须为6-32位")
private String password;
}

依赖只需在 pom.xml 中加入 spring-boot-starter-validation,配合 Controller 上的 @Valid 即可触发校验。

Controller(接收)​

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

/** @RequestBody 把 JSON 反序列化为 LoginDTO,@Valid 触发校验 */
@PostMapping("/login")
public Result<TokenVO> login(@Valid @RequestBody LoginDTO dto,
HttpServletRequest request) {
String ip = IpUtil.getClientIp(request); // 生产需处理 X-Forwarded-For 代理头
return Result.ok(authService.login(dto, ip));
}
}

Service(业务逻辑)​

@Service
@RequiredArgsConstructor
public class AuthService {

private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder; // BCryptPasswordEncoder
private final VisitLogService visitLogService;

@Transactional(rollbackFor = Exception.class)
public TokenVO login(LoginDTO dto, String ip) {
User user = userMapper.selectByUsername(dto.getUsername());
// 用户不存在与密码错误返回同一个提示,防止撞库探测
if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPasswordHash())) {
throw new BizException(40101, "用户名或密码错误");
}
if (user.getStatus() != 1) {
throw new BizException(40102, "账号已被禁用");
}

// 生成 JWT:短时效(2h),刷新令牌放服务端 / Redis
String token = JwtUtil.generateToken(user.getId(), user.getUsername());

// 记录访问明细(异步落库,见《海量数据处理》),为访问次数统计提供数据
visitLogService.recordAsync(user.getId(), ip, "/api/auth/login");

return new TokenVO(token);
}
}

统一返回体 Result、业务异常 BizException 与 @RestControllerAdvice 全局异常处理的完整实现,参见 Spring Boot → Web 开发 → 统一返回与异常。登录场景直接复用同一套结构,参数校验失败返回 400 + 第一条校验消息,兜底异常只返回通用提示、不暴露堆栈。

令牌鉴权拦截器​

@Component
public class AuthInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest req,
HttpServletResponse resp, Object h) throws Exception {
String token = req.getHeader("Authorization"); // Bearer xxx
if (token == null || !token.startsWith("Bearer ")) {
resp.setStatus(401);
resp.getWriter().write("{\"code\":40100,\"message\":\"未登录\"}");
return false;
}
try {
Claims claims = JwtUtil.parseToken(token.substring(7));
req.setAttribute("userId", claims.get("uid", Long.class));
return true;
} catch (JwtException e) {
resp.setStatus(401);
resp.getWriter().write("{\"code\":40101,\"message\":\"令牌已过期\"}");
return false;
}
}
}

注册拦截器并放行登录接口:

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final AuthInterceptor authInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login");
}
}

请求处理全流程​

前端 fetch POST JSON
→ 拦截器放行 /api/auth/login
→ @RequestBody 反序列化 + @Valid 校验(失败 → 全局异常 → 400 + message)
→ Service:查用户 → BCrypt 比对 → 签发 JWT → 异步记录访问日志
→ 返回 Result.ok(token)

面试常问​

  • @RequestBody vs @RequestParam vs @PathVariable? JSON 请求体 / URL 查询参数 / REST 路径变量。
  • 为什么 BCrypt 而不是 MD5? 自带盐、计算慢(可调成本因子),抗彩虹表与暴力破解;MD5 是快速哈希,专为校验设计,不适合存密码。
  • JWT 无状态怎么注销? 黑名单(Redis 存 jti + 剩余 TTL)或改为服务端会话。