Spring Boot 登录实现:JWT 与 Session 全面对比与实战讲解

Spring Boot 登录实现:JWT 与 Session 全面对比与实战讲解

2025.5.21-23:11今天在学习黑马点评时突然发现用的是与苍穹外卖jwt不一样的登录方式-Session,于是就想记录一下这两种方式有什么不同

在实际开发中,登录认证是后端最基础也是最重要的模块之一。常见的实现方式主要有两种:Session 登录JWT(JSON Web Token)登录

下面将从原理、使用场景、优缺点、代码结构等角度,来对比这两种方案,然后给出 Spring Boot 中的实际应用建议,帮助在实际项目中做出合理选择。

一、登录认证基本流程

无论是 JWT 还是 Session,登录的本质流程都是:

  1. 用户发送用户名和密码;
  2. 后端验证成功后,生成认证信息;
  3. 后端将认证信息返回给客户端;
  4. 客户端携带认证信息访问受保护接口;
  5. 后端验证该认证信息是否合法。

二、Session 登录机制

1. 原理说明

  • 用户第一次登录成功后,服务器创建一个 Session,并在服务器内存或 Redis 中保存用户信息;
  • 同时将 Session ID 写入到浏览器的 Cookie 中;
  • 用户每次请求都会自动携带该 Cookie,服务器根据 Session ID 获取用户信息。

2. 代码实现示例

登录接口:
java 复制代码
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO, HttpSession session) {
    User user = userService.login(loginDTO);
    session.setAttribute("user", user);
    return Result.success();
}
拦截器判断是否登录:
java 复制代码
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:
java 复制代码
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
    User user = userService.login(loginDTO);
    String token = JwtUtil.generateToken(user); // 自定义工具类生成 token
    return Result.success(token);
}
前端请求携带 Token:
http 复制代码
GET /user/info HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
拦截器验证 Token:
java 复制代码
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 常见安全建议

  1. 设置过期时间(exp),避免 token 永久有效;
  2. 使用 HTTPS,防止 Token 被中间人截获;
  3. 配合 Refresh Token 实现续签;
  4. 用户登出时将 Token 加入 Redis 黑名单;
  5. Token 不应包含敏感信息(如密码、身份证号等)。

七、总结

Session 和 JWT 各有优缺点,选择时需根据项目实际情况权衡:

  • Session 适合传统 Web 应用,安全性高,实现简单;
  • JWT 适合前后端分离、微服务架构,无状态,扩展性强。

在实际开发中,还可以结合两者的优势,实现更灵活、安全的认证方案。

八、登录超时控制

1. Session 登录的过期控制

yaml 复制代码
# application.yml 中配置 Session 失效时间(单位分钟)
server:
  servlet:
    session:
      timeout: 30

Session 登录通常依赖浏览器 Cookie,每次请求自动续期,适合长期在线场景。

2. JWT 的过期控制

JWT 自带 exp 字段,在生成 Token 时设置过期时间:

java 复制代码
// 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 机制:

  1. 登录时生成两个 Token:AccessToken(短过期)和 RefreshToken(长过期);
  2. AccessToken 过期后,使用 RefreshToken 重新获取 AccessToken;
  3. RefreshToken 也过期时,才需要用户重新登录。
java 复制代码
// 登录接口返回两个 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 黑名单实现:

java 复制代码
// 登出接口
@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 工具类参考实现

java 复制代码
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:

java 复制代码
@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 中是否存在用户信息:

java 复制代码
@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):

java 复制代码
@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,包含过期时间和签名加密:

java 复制代码
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 并返回给前端:

java 复制代码
@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 并解析验证:

java 复制代码
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

java 复制代码
@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:

java 复制代码
@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 剩余有效期一致的过期时间:

java 复制代码
@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 次尝试:

java 复制代码
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 批量操作):

java 复制代码
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 并设置安全上下文:

java 复制代码
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 过滤器并配置权限规则:

java 复制代码
@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 注解)和安全策略管理。

相关推荐
Mr YiRan6 小时前
网络请求API监控与网络切换埋点
android·网络
AlienZHOU6 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
李昊哲小课8 小时前
Spring Boot SSE 实时推送教程
spring boot·websocket·flux·sseemitter
狗头大军之江苏分军9 小时前
《潮水漫过十七岁》开学了
后端
苏三说技术9 小时前
如何看待GPT-6在UP主众测中碾压夺冠?它是现在最强大模型吗?
后端
mldong10 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
wno70410 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
Captaincc10 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
aramae11 小时前
模拟实现strlen()函数 (C语言)
c语言·开发语言·后端
行百里er12 小时前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring cloud·监控