
Spring Boot+JWT 实战:登录鉴权全流程与安全最佳实践
一、技术铺垫:为何选择 Spring Boot+JWT?
在前后端分离、微服务架构成为主流的今天,传统 Session 认证暴露出明显短板:分布式环境下需额外实现 Session 共享,跨端(Web / 移动端)对接复杂,扩展性差。而Spring Boot+JWT的组合恰好解决这些痛点,成为当前主流的鉴权方案之一。
Spring Boot 的自动配置能力可大幅简化项目搭建,减少冗余配置;JWT(JSON Web Token)则是一种无状态认证机制,用户登录后服务端仅需签发一个加密 Token,后续请求无需在服务端存储状态 —— 只需验证 Token 有效性即可完成鉴权。这种组合的核心优势的在于:
- 无状态扩展:微服务集群无需同步认证状态,直接通过 Token 验证,降低部署复杂度;
- 跨端兼容:Token 可通过请求头、URL 参数等方式传递,适配 Web、APP、小程序等多端场景;
- 开发高效:Spring Boot 的 Starter 生态(如 spring-boot-starter-web、mybatis-plus-boot-starter)可快速集成依赖,JWT 工具类封装后可直接复用。
其典型应用场景包括:前后端分离项目的登录注册、微服务 API 的身份校验、第三方系统对接的临时授权等,几乎覆盖所有需要 “身份确认” 的后端场景。
二、完整实战流程:从环境搭建到鉴权落地
2.1 环境搭建与依赖配置(Maven)
第一创建 Spring Boot 项目(推荐版本 2.7.x,兼容性更优),在pom.xml中引入核心依赖,涵盖 Web、ORM、JWT、安全加密、数据校验等功能:
xml
<dependencies>
<!-- Spring Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus:简化数据库操作 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- JWT核心依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Security:密码加密、权限控制 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 数据校验:注册参数验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok:简化实体类代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
同时在application.yml中配置数据库连接、JWT 核心参数(避免硬编码):
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/jwt_demo?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.jwt.entity
# JWT自定义配置
jwt:
secret-key: 6f4a8d1e9b2c7e3a5d8f2b4e6a9d3c8b7e5f1a3d9b4c6e2a8f1d3b5e7a9c4f2d # 提议生产环境用配置中心管理
access-expire: 7200000 # Access Token有效期:2小时(毫秒)
refresh-expire: 604800000 # Refresh Token有效期:7天(毫秒)
2.2 核心实体与数据库设计
2.2.1 User 实体类
使用 Lombok 简化 Getter/Setter,添加 JSR380 数据校验注解(用于注册参数验证):
java
运行
package com.example.jwt.entity;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.persistence.Id;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Data
public class User {
@Id
private Long id;
// 用户名:非空、长度2-20
@NotBlank(message = "用户名不能为空")
@Length(min = 2, max = 20, message = "用户名长度需在2-20字符之间")
private String username;
// 密码:存储加密后的密文
@NotBlank(message = "密码不能为空")
@Length(min = 6, message = "密码长度不能少于6字符")
private String password;
// 昵称
private String nickname;
// 角色:如ADMIN/USER,用于权限控制
private String role;
// 创建时间
private LocalDateTime createTime;
}
2.2.2 数据库表 SQL
sql
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '加密密码',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`role` varchar(20) NOT NULL DEFAULT 'USER' COMMENT '角色',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`) COMMENT '用户名唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
2.3 JWT 工具类封装(核心)
封装 Token 生成、解析、验证逻辑,解决硬编码问题,支持携带用户角色信息(为权限控制做铺垫):
java
运行
package com.example.jwt.util;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class JwtUtil {
// 从配置文件读取密钥
@Value("${jwt.secret-key}")
private String secretKey;
// Access Token有效期
@Value("${jwt.access-expire}")
private Long accessExpire;
// Refresh Token有效期
@Value("${jwt.refresh-expire}")
private Long refreshExpire;
/**
* 生成Access Token(携带用户名、角色)
*/
public String generateAccessToken(String username, String role) {
return generateToken(username, role, accessExpire);
}
/**
* 生成Refresh Token(仅携带用户名,用于刷新Access Token)
*/
public String generateRefreshToken(String username) {
return generateToken(username, null, refreshExpire);
}
/**
* 通用Token生成方法
*/
private String generateToken(String username, String role, Long expireTime) {
// 1. 设置Token负载(Payload):不存敏感信息(如密码)
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
if (role != null) {
claims.put("role", role);
}
// 2. 构建JWT:Header.Payload.Signature
return Jwts.builder()
.setClaims(claims) // 负载信息
.setExpiration(new Date(System.currentTimeMillis() + expireTime)) // 过期时间
.signWith(SignatureAlgorithm.HS512, secretKey) // 签名算法+密钥
.compact();
}
/**
* 解析Token,获取负载信息
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.error("Token已过期:{}", e.getMessage());
throw new RuntimeException("Token已过期");
} catch (SignatureException e) {
log.error("Token签名无效:{}", e.getMessage());
throw new RuntimeException("Token签名无效");
} catch (IllegalArgumentException | MalformedJwtException e) {
log.error("Token格式错误:{}", e.getMessage());
throw new RuntimeException("Token格式错误");
}
}
/**
* 验证Token有效性(是否过期、签名是否正确)
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 从Token中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("username", String.class);
}
/**
* 从Token中获取角色
*/
public String getRoleFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("role", String.class);
}
}
2.4 登录与注册功能实现
2.4.1 Service 层(含密码加密)
使用 Spring Security 的BCryptPasswordEncoder加密密码,避免明文存储:
java
运行
package com.example.jwt.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.jwt.entity.User;
import com.example.jwt.mapper.UserMapper;
import com.example.jwt.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
@Autowired
private JwtUtil jwtUtil;
// 密码加密器
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 用户注册:密码加密后保存
*/
public boolean register(User user) {
// 1. 检查用户名是否已存在
User existUser = getOne(new QueryWrapper<User>().eq("username", user.getUsername()));
if (existUser != null) {
throw new RuntimeException("用户名已存在");
}
// 2. 密码加密(BCrypt自动生成盐值,无需手动处理)
String encryptedPwd = passwordEncoder.encode(user.getPassword());
user.setPassword(encryptedPwd);
// 3. 默认角色为USER
user.setRole("USER");
// 4. 保存用户
return save(user);
}
/**
* 用户登录:验证密码,返回Token对(Access+Refresh)
*/
public Map<String, String> login(String username, String password) {
// 1. 查询用户
User user = getOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
throw new RuntimeException("用户名不存在");
}
// 2. 验证密码(BCrypt匹配密文与明文)
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
// 3. 生成Token对
String accessToken = jwtUtil.generateAccessToken(user.getUsername(), user.getRole());
String refreshToken = jwtUtil.generateRefreshToken(user.getUsername());
// 4. 返回结果
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("accessToken", accessToken);
tokenMap.put("refreshToken", refreshToken);
return tokenMap;
}
/**
* 刷新Access Token:通过Refresh Token获取新的Access Token
*/
public String refreshAccessToken(String refreshToken) {
// 1. 验证Refresh Token有效性
if (!jwtUtil.validateToken(refreshToken)) {
throw new RuntimeException("Refresh Token无效");
}
// 2. 从Refresh Token中获取用户名,查询角色
String username = jwtUtil.getUsernameFromToken(refreshToken);
User user = getOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 3. 生成新的Access Token
return jwtUtil.generateAccessToken(username, user.getRole());
}
}
2.4.2 Controller 层(接口暴露)
处理 HTTP 请求,实现注册、登录、Token 刷新接口,添加参数校验:
java
运行
package com.example.jwt.controller;
import com.example.jwt.entity.User;
import com.example.jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService;
/**
* 用户注册接口
*/
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(
@Valid @RequestBody User user,
BindingResult bindingResult
) {
// 1. 处理参数校验错误
if (bindingResult.hasErrors()) {
Map<String, Object> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(error ->
errorMap.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
}
// 2. 调用Service注册
boolean success = userService.register(user);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", success);
resultMap.put("msg", success ? "注册成功" : "注册失败");
return new ResponseEntity<>(resultMap, HttpStatus.OK);
}
/**
* 用户登录接口
*/
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> loginParam) {
// 1. 获取请求参数
String username = loginParam.get("username");
String password = loginParam.get("password");
// 2. 调用Service登录,获取Token
Map<String, String> tokenMap = userService.login(username, password);
// 3. 构造返回结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", true);
resultMap.put("msg", "登录成功");
resultMap.put("data", tokenMap);
return new ResponseEntity<>(resultMap, HttpStatus.OK);
}
/**
* 刷新Access Token接口
*/
@PostMapping("/refresh-token")
public ResponseEntity<Map<String, Object>> refreshToken(@RequestBody Map<String, String> param) {
String refreshToken = param.get("refreshToken");
String newAccessToken = userService.refreshAccessToken(refreshToken);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", true);
resultMap.put("data", newAccessToken);
return new ResponseEntity<>(resultMap, HttpStatus.OK);
}
}
2.5 Token 拦截器与全局配置
通过拦截器统一校验 Token,排除登录、注册等公开接口:
2.5.1 JWT 拦截器
java
运行
package com.example.jwt.interceptor;
import com.example.jwt.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 设置跨域支持(避免OPTIONS请求拦截问题)
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Authorization,Content-Type");
// 2. 跳过OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
// 3. 获取Authorization请求头(格式:Bearer <token>)
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("请携带有效的Token");
return false;
}
// 4. 提取Token(去掉"Bearer "前缀)
String token = authHeader.substring(7);
// 5. 验证Token有效性
if (!jwtUtil.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token无效或已过期");
return false;
}
// 6. 将用户信息存入SecurityContext(用于后续权限控制)
String username = jwtUtil.getUsernameFromToken(token);
String role = jwtUtil.getRoleFromToken(token);
UserDetails userDetails = User.withUsername(username)
.password("") // 密码无需存储(Token已验证身份)
.roles(role)
.build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
);
return true;
}
}
2.5.2 注册拦截器(WebConfig)
java
运行
package com.example.jwt.config;
import com.example.jwt.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns( // 排除公开接口
"/auth/login",
"/auth/register",
"/auth/refresh-token",
"/swagger-ui/**", // 排除Swagger文档(如果使用)
"/v3/api-docs/**"
);
}
}
2.6 权限控制与异常处理
2.6.1 权限控制示例
通过@PreAuthorize注解实现方法级权限控制,需在启动类添加@
EnableGlobalMethodSecurity(prePostEnabled = true)开启功能:
java
运行
package com.example.jwt.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class TestController {
// 所有登录用户可访问
@GetMapping("/public")
public String publicApi() {
return "所有登录用户可访问的接口";
}
// 仅ADMIN角色可访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminApi() {
return "仅管理员可访问的接口";
}
}
2.6.2 全局异常处理
统一异常响应格式,提升用户体验:
java
运行
package com.example.jwt.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理业务异常(如用户名已存在、密码错误)
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException e) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", false);
resultMap.put("msg", e.getMessage());
return new ResponseEntity<>(resultMap, HttpStatus.BAD_REQUEST);
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidException(MethodArgumentNotValidException e) {
Map<String, Object> errorMap = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(error ->
errorMap.put(error.getField(), error.getDefaultMessage())
);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", false);
resultMap.put("msg", "参数校验失败");
resultMap.put("errors", errorMap);
return new ResponseEntity<>(resultMap, HttpStatus.BAD_REQUEST);
}
// 处理通用异常
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception e) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("success", false);
resultMap.put("msg", "服务器内部错误");
resultMap.put("error", e.getMessage());
return new ResponseEntity<>(resultMap, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
三、JWT 安全最佳实践(生产环境必看)
- 采用 “双 Token” 刷新机制
- 生产环境避免单独使用长期 Token:短期Access Token(10-30 分钟)用于接口访问,长期Refresh Token(7-30 天)用于刷新 Access Token。当 Access Token 过期时,前端用 Refresh Token 静默获取新 Token,无需用户重新登录,兼顾安全性与用户体验。
- 密码加密与敏感信息防护
- 绝对禁止明文存储密码,推荐使用BCrypt(自动加盐)或Argon2加密算法;JWT 的 Payload 部分可被 Base64 解码,因此严禁存储密码、手机号、身份证号等敏感信息,仅保留用户名、角色等非敏感标识。
- 密钥安全管理
- 避免将 JWT 密钥硬编码在代码中,生产环境需通过配置中心(如 Nacos、Apollo)或环境变量注入;推荐使用非对称加密算法(如 RSA)替代 HS256—— 用私钥签名 Token,公钥验证 Token,降低密钥泄露风险。
- Token 存储与 XSS 防护
- 前端存储 Token 时,优先选择HttpOnly + Secure Cookie:HttpOnly 可防止 JS 脚本读取 Token(防 XSS 攻击),Secure 可确保 Token 仅通过 HTTPS 传输(防中间人攻击);避免使用localStorage或sessionStorage,二者易受 XSS 攻击窃取 Token。
- Token 过期与黑名单机制
- 若需实现 “用户登出即失效 Token”(JWT 无状态的短板),可维护一个 Token 黑名单:用户登出时将 Token 加入黑名单,拦截器验证 Token 时先检查是否在黑名单中。黑名单需设置过期时间(与 Token 有效期一致),避免内存溢出。
四、常见问题排查与优化
4.1 Token 失效问题
- 现象 1:Token 已过期
- 排查:检查 JWT 工具类中expireTime配置是否过短,或前端未及时刷新 Token。
- 解决方案:调整 Access Token 有效期至合理范围(10-30 分钟),前端监听 Token 过期状态,通过 Refresh Token 自动刷新。
- 现象 2:Token 签名无效
- 排查:服务端密钥与生成 Token 时的密钥不一致(如环境配置错误),或 Token 被篡改。
- 解决方案:统一环境密钥配置,生产环境使用非对称加密,增加签名校验日志。
4.2 跨域认证问题
- 现象:前端跨域请求时 Token 未携带,或 OPTIONS 预检请求被拦截
- 排查:跨域配置未允许Authorization请求头,或拦截器未跳过 OPTIONS 请求。
- 解决方案:在拦截器中添加跨域响应头(Access-Control-Allow-Origin、Access-Control-Allow-Headers),并跳过 OPTIONS 预检请求。
4.3 性能优化
- 拦截器性能瓶颈
- 若系统 QPS 较高,JWT 验证(尤其是 RSA 算法)可能成为瓶颈,可通过 Redis 缓存已验证的 Token(缓存 key 为 Token,value 为用户信息,过期时间与 Token 一致),减少重复解析 Token 的开销。
- 数据库查询优化
- 用户登录、刷新 Token 时需查询数据库获取角色信息,可将用户角色缓存至 Redis(key 为用户名,value 为角色),有效期设置为 1 小时,降低数据库访问压力。
#Java 实战# #SpringBoot 安全# #JWT 鉴权# #前后端分离开发# #后端安全最佳实践#
感谢关注【AI码力】,获取更多Java秘籍!



收藏了,感谢分享