Spring Boot 登录实现:JWT 与 Session 全面对比与实战讲解
2025.5.21-23:11今天在学习黑马点评时突然发现用的是与苍穹外卖jwt不一样的登录方式-Session,于是就想记录一下这两种方式有什么不同
在实际开发中,登录认证是后端最基础也是最重要的模块之一。常见的实现方式主要有两种:Session 登录 和 JWT(JSON Web Token)登录。
下面将从原理、使用场景、优缺点、代码结构等角度,来对比这两种方案,然后给出 Spring Boot 中的实际应用建议,帮助在实际项目中做出合理选择。
一、登录认证基本流程
无论是 JWT 还是 Session,登录的本质流程都是:
- 用户发送用户名和密码;
- 后端验证成功后,生成认证信息;
- 后端将认证信息返回给客户端;
- 客户端携带认证信息访问受保护接口;
- 后端验证该认证信息是否合法。
二、Session 登录机制
1. 原理说明
- 用户第一次登录成功后,服务器创建一个
Session
,并在服务器内存或 Redis 中保存用户信息; - 同时将 Session ID 写入到浏览器的 Cookie 中;
- 用户每次请求都会自动携带该 Cookie,服务器根据 Session ID 获取用户信息。
2. 代码实现示例
登录接口:
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO, HttpSession session) {
User user = userService.login(loginDTO);
session.setAttribute("user", user);
return Result.success();
}
拦截器判断是否登录:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Object user = request.getSession().getAttribute("user");
if (user == null) {
response.setStatus(401);
return false;
}
return true;
}
三、JWT 登录机制
1. 原理说明
- 用户登录成功后,服务器签发一个加密的 JWT Token;
- 客户端将 Token 保存在 LocalStorage 或 Cookie;
- 每次请求都将 Token 放在 Authorization 请求头中;
- 服务器解析并验证 Token,从中读取用户信息。
2. JWT Token 的组成
JWT 一般由三部分组成:
Header.Payload.Signature
例如:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJsaTIzIn0.sD_Kpdi2M...
3. JWT 登录代码示例
登录生成 Token:
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
User user = userService.login(loginDTO);
String token = JwtUtil.generateToken(user); // 自定义工具类生成 token
return Result.success(token);
}
前端请求携带 Token:
GET /user/info HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
拦截器验证 Token:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
Claims claims = JwtUtil.parseToken(token);
if (claims == null) {
response.setStatus(401);
return false;
}
return true;
}
四、Session vs JWT 对比总结
特性 | Session | JWT |
---|---|---|
存储位置 | 服务端(内存/Redis) | 客户端(LocalStorage/Cookie) |
状态性 | 有状态 | 无状态 |
跨域支持 | 不友好(需处理 Cookie) | 友好 |
性能 | 频繁读写服务端存储 | 只需签名验证 |
安全性 | 较高(信息在服务端) | 中等(需防止 Token 泄露) |
适用场景 | 传统 Web 应用 | 前后端分离、微服务、移动端 |
五、实战项目推荐
在实际项目中,建议根据以下场景选择:
- 中小型 Web 应用:推荐使用 Session,实现简单,安全性高;
- 前后端分离项目:推荐使用 JWT,无状态,跨域友好;
- 大型系统:考虑 JWT + Session 混合使用,结合两者优势。
六、JWT 常见安全建议
- 设置过期时间(exp),避免 token 永久有效;
- 使用 HTTPS,防止 Token 被中间人截获;
- 配合 Refresh Token 实现续签;
- 用户登出时将 Token 加入 Redis 黑名单;
- Token 不应包含敏感信息(如密码、身份证号等)。
七、总结
Session 和 JWT 各有优缺点,选择时需根据项目实际情况权衡:
- Session 适合传统 Web 应用,安全性高,实现简单;
- JWT 适合前后端分离、微服务架构,无状态,扩展性强。
在实际开发中,还可以结合两者的优势,实现更灵活、安全的认证方案。
八、登录超时控制
1. Session 登录的过期控制
# application.yml 中配置 Session 失效时间(单位分钟)
server:
servlet:
session:
timeout: 30
Session 登录通常依赖浏览器 Cookie,每次请求自动续期,适合长期在线场景。
2. JWT 的过期控制
JWT 自带 exp 字段,在生成 Token 时设置过期时间:
// JwtUtil 生成 Token 示例
public static String generateToken(User user) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME); // 设置过期时间
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(user.getId().toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.claim("username", user.getUsername())
.signWith(SignatureAlgorithm.HS512, SECRET) // 签名加密
.compact();
}
九、JWT 的刷新机制(Refresh Token)
为了避免用户频繁登录,可以实现 Refresh Token 机制:
- 登录时生成两个 Token:AccessToken(短过期)和 RefreshToken(长过期);
- AccessToken 过期后,使用 RefreshToken 重新获取 AccessToken;
- RefreshToken 也过期时,才需要用户重新登录。
// 登录接口返回两个 Token
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
User user = userService.login(loginDTO);
String accessToken = JwtUtil.generateAccessToken(user);
String refreshToken = JwtUtil.generateRefreshToken(user);
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", accessToken);
tokens.put("refreshToken", refreshToken);
return Result.success(tokens);
}
// 刷新 Token 接口
@PostMapping("/refreshToken")
public Result refreshToken(@RequestBody RefreshTokenDTO dto) {
// 验证 RefreshToken 有效性
Claims claims = JwtUtil.parseRefreshToken(dto.getRefreshToken());
if (claims == null) {
return Result.error("登录已过期,请重新登录");
}
// 重新生成 AccessToken
User user = userService.getById(Long.valueOf(claims.getSubject()));
String accessToken = JwtUtil.generateAccessToken(user);
return Result.success(accessToken);
}
十、登出与 Redis 黑名单机制
JWT 本身无法主动失效,但可以通过 Redis 黑名单实现:
// 登出接口
@PostMapping("/logout")
public Result logout(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
// 解析 Token 获取过期时间
Claims claims = JwtUtil.parseToken(token);
if (claims != null) {
Date expireDate = claims.getExpiration();
long expireSeconds = (expireDate.getTime() - System.currentTimeMillis()) / 1000;
// 将 Token 加入 Redis 黑名单,直到过期
redisTemplate.opsForValue().set("jwt:blacklist:" + token, "invalid", expireSeconds, TimeUnit.SECONDS);
}
}
return Result.success();
}
// 拦截器中检查黑名单
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
// 检查黑名单
if (redisTemplate.hasKey("jwt:blacklist:" + token)) {
response.setStatus(401);
return false;
}
// 验证 Token
Claims claims = JwtUtil.parseToken(token);
if (claims == null) {
response.setStatus(401);
return false;
}
// 将用户信息存入请求
request.setAttribute("userId", claims.getSubject());
}
return true;
}
十一、实际开发中推荐的 JWT 完整实现流程图
用户登录请求
│
▼
后端验证用户名密码
│
▼
验证成功
│
├─┬─ 生成 AccessToken(含用户 ID、角色等)
│ │
│ ├─ 生成 RefreshToken(与用户 ID 绑定)
│ │
│ └─ 将 RefreshToken 存入 Redis(设置过期时间)
│
▼
返回 AccessToken 和 RefreshToken 给前端
│
▼
前端保存 Token(如 LocalStorage)
│
▼
前端每次请求携带 AccessToken(放在 Header)
│
▼
后端拦截器验证 AccessToken
│
├─ 验证失败 ──┐
│ │
│ ▼
│ 返回 401 未授权
│ │
│ ▼
│ 前端使用 RefreshToken 请求新 AccessToken
│ │
│ ▼
│ 后端验证 RefreshToken
│ │
│ ┌───────┴───────┐
│ │ │
│ 有效 无效
│ │ │
│ ▼ ▼
│ 生成新 Token 用户重新登录
│ │
│ ▼
返回新 AccessToken
十二、附录:JWT 工具类参考实现
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.accessTokenExpireTime}")
private long accessTokenExpireTime; // 分钟
@Value("${jwt.refreshTokenExpireTime}")
private long refreshTokenExpireTime; // 天
/**
* 生成 Access Token
*/
public String generateAccessToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("roles", user.getRoles());
return generateToken(user.getId().toString(), claims, accessTokenExpireTime * 60 * 1000);
}
/**
* 生成 Refresh Token
*/
public String generateRefreshToken(User user) {
return generateToken(user.getId().toString(), null, refreshTokenExpireTime * 24 * 60 * 60 * 1000);
}
/**
* 生成 Token
*/
private String generateToken(String subject, Map<String, Object> claims, long expireTime) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + expireTime);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
return null;
}
}
/**
* 验证 Token 是否过期
*/
public boolean isTokenExpired(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return true;
}
Date expiration = claims.getExpiration();
return expiration.before(new Date());
}
}
十三、JWT 与 Session 混合使用的实践方案
在一些大型系统中,可能存在不同端口或子系统使用不同的认证方式(如后台管理系统用 Session,移动端用 JWT),这时可以采用 混合认证策略:
1. 使用建议
模块 | 建议认证方式 |
---|---|
管理后台(单体、Spring MVC) | 使用 Session(Cookie 自动管理,方便权限控制) |
移动端 / 小程序 / H5 | 使用 JWT(无状态认证,跨域安全,适合 API) |
多端统一用户体系 | JWT + Session 混合,并通过 Redis 存储登录状态 |
2. 核心实现思路
- 登录成功时,服务端生成 JWT,同时创建一个短 Session,用于后台系统交互;
- Redis 中统一存储用户 Token 状态,便于管理和失效控制;
- 拦截器判断请求来源(如是否含
Authorization
),自动适配认证方式。
十四、常见问题 FAQ
Q1:JWT 一旦泄露是否会被无限使用?
是的,如果没有设置过期时间或失效机制(如黑名单),Token 会一直有效。所以必须:
- 设置过期时间;
- 使用 HTTPS;
- 实现登出逻辑(Redis 黑名单);
Q2:JWT 是不是比 Session 更安全?
不完全正确。JWT 只是在无状态架构下更合适,但它暴露信息在客户端,若加密不严密,反而更容易被篡改或伪造。而 Session 只存在服务端,反而更隐蔽和安全。
Q3:JWT 必须存放在 LocalStorage 吗?
不一定。也可以存放在:
LocalStorage
(常见方式,刷新页面不丢失);SessionStorage
(更安全,但刷新页面会清空);HttpOnly Cookie
(防 XSS,但不支持 JS 访问,需配合后端设置跨域 Cookie);
十六、推荐学习与实践路径
1. 学会使用 Spring MVC + Session 登录机制
在 Spring MVC 项目中,Session 登录是一种传统且有效的认证方式。
登录接口实现:
通过 HttpSession
对象将用户信息存入 Session:
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpSession session) {
User user = userService.login(username, password);
if (user != null) {
session.setAttribute("user", user); // 存入 Session
return "redirect:/home"; // 登录成功跳转
}
return "login"; // 登录失败返回登录页
}
拦截器验证登录状态:
编写拦截器检查 Session 中是否存在用户信息:
@Component
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
if (user == null) { // 未登录则重定向到登录页
response.sendRedirect("/login");
return false;
}
return true; // 已登录,允许访问
}
}
注册拦截器:
在 Spring MVC 配置类中指定拦截路径(如 /api/**
)和排除路径(如 /login
):
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**") // 拦截所有 API 接口
.excludePathPatterns("/login", "/static/**"); // 排除登录页和静态资源
}
}
2. 掌握前后端分离项目中 JWT 的基本使用方式
JWT 工具类实现:
生成和解析 Token,包含过期时间和签名加密:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
private static final String SECRET = "your_secret_key_keep_it_secure"; // 密钥(需保密)
private static final long EXPIRATION_TIME = 3600000; // 1小时(毫秒)
// 生成 Token
public static String generateToken(String username) {
Claims claims = Jwts.claims().setSubject(username); // 载荷存储用户标识
Date now = new Date();
Date expiration = new Date(now.getTime() + EXPIRATION_TIME); // 设置过期时间
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, SECRET) // HS256 签名算法
.compact();
}
// 解析 Token
public static Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody(); // 成功解析返回载荷
} catch (Exception e) {
return null; // 解析失败返回 null
}
}
}
登录接口返回 Token:
验证用户信息后生成 Token 并返回给前端:
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody UserLoginRequest request) {
User user = userService.authenticate(request.getUsername(), request.getPassword());
if (user != null) {
String token = JwtUtil.generateToken(user.getUsername());
Map<String, String> result = new HashMap<>();
result.put("token", token);
return ResponseEntity.ok(result); // 返回 Token
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); // 认证失败
}
后端验证 Token:
通过拦截器从请求头中获取 Token 并解析验证:
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 去除 "Bearer " 前缀
Claims claims = JwtUtil.parseToken(token);
if (claims != null) {
// 解析成功,将用户信息存入请求(如用户 ID、角色)
request.setAttribute("userId", claims.getSubject());
return true;
}
}
response.setStatus(HttpStatus.UNAUTHORIZED); // 未认证或 Token 无效
return false;
}
}
3. 实现 Token 过期 + 刷新机制
双 Token 设计:
- AccessToken:短有效期(如 1 小时),用于接口认证;
- RefreshToken:长有效期(如 7 天),用于刷新 AccessToken。
登录时返回双 Token:
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody UserLoginRequest request) {
User user = userService.authenticate(request.getUsername(), request.getPassword());
if (user != null) {
String accessToken = JwtUtil.generateToken(user.getUsername(), "access"); // 短过期
String refreshToken = JwtUtil.generateToken(user.getUsername(), "refresh"); // 长过期
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", accessToken);
tokens.put("refreshToken", refreshToken);
return ResponseEntity.ok(tokens);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
}
刷新 Token 接口:
使用 RefreshToken 生成新的 AccessToken:
@PostMapping("/refresh-token")
public ResponseEntity<Map<String, String>> refreshToken(@RequestHeader("Refresh-Token") String refreshToken) {
Claims claims = JwtUtil.parseToken(refreshToken);
if (claims != null && "refresh".equals(claims.get("type"))) { // 验证 Token 类型
String username = claims.getSubject();
String newAccessToken = JwtUtil.generateToken(username, "access"); // 生成新 AccessToken
Map<String, String> result = new HashMap<>();
result.put("accessToken", newAccessToken);
return ResponseEntity.ok(result);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
}
4. 实现 Redis 黑名单、登录限制、踢出机制
Redis 黑名单:
用户登出时将 Token 加入 Redis 黑名单,设置与 Token 剩余有效期一致的过期时间:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 登出逻辑
public void logout(String token) {
Claims claims = JwtUtil.parseToken(token);
if (claims != null) {
long expireTime = claims.getExpiration().getTime() - System.currentTimeMillis();
if (expireTime > 0) {
redisTemplate.opsForValue().set("blacklist:" + token, "invalid", expireTime, TimeUnit.MILLISECONDS);
}
}
}
// 拦截器中检查黑名单
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (redisTemplate.hasKey("blacklist:" + token)) { // 存在于黑名单则拒绝访问
response.setStatus(HttpStatus.UNAUTHORIZED);
return false;
}
// 其他验证逻辑...
return true;
}
登录频率限制:
使用 Redis 记录用户登录次数,限制每分钟最多 5 次尝试:
public boolean checkLoginFrequency(String username) {
String key = "login:attempts:" + username;
Integer count = (Integer) redisTemplate.opsForValue().get(key);
if (count != null && count >= 5) { // 超过限制
return false; // 禁止登录
}
// 次数加 1,设置过期时间 1 分钟
redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
return true;
}
管理员踢出用户:
将用户所有有效 Token 加入黑名单(需结合用户 ID 批量操作):
public void kickUser(String userId) {
// 查询用户所有有效 Token(需业务系统存储 Token 与用户的映射)
List<String> tokens = tokenRepository.findByUserId(userId);
tokens.forEach(token -> logout(token)); // 批量加入黑名单
}
5. 掌握 Spring Security 对 JWT 的整合
自定义 JWT 认证过滤器:
继承 OncePerRequestFilter
,解析 Token 并设置安全上下文:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
Claims claims = jwtUtil.parseToken(token);
if (claims != null) {
String username = claims.getSubject();
// 构建认证对象(可添加角色权限)
Authentication authentication = new UsernamePasswordAuthenticationToken(
username, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
SecurityContextHolder.getContext().setAuthentication(authentication); // 设置安全上下文
}
}
filterChain.doFilter(request, response); // 继续执行后续过滤器
}
}
Spring Security 配置:
禁用 CSRF,添加 JWT 过滤器并配置权限规则:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtUtil jwtUtil;
public SecurityConfig(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离场景禁用 CSRF
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll() // 允许登录接口
.antMatchers("/admin/**").hasRole("ADMIN") // 管理员接口需 ADMIN 角色
.anyRequest().authenticated() // 其他接口需认证
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); // 添加 JWT 过滤器
}
}
通过 Spring Security 的整合,可更便捷地实现细粒度权限控制(如 @PreAuthorize
注解)和安全策略管理。