广告

Spring Boot 2 多页面应用:如何用 JWT 实现无 Session 的安全验证(实战与最佳实践)

概述与目标

无状态认证的设计要点

JWT 作为无状态令牌在 Spring Boot 2 的应用中实现“无服务端会话”是实现多页面应用(MPA)无缝鉴权的核心路径之一。通过在每次请求中携带经过签名的令牌,后端无需维护用户的会话信息,从而实现水平扩展与更高的并发承载能力。

在实际落地时,需要关注令牌的有效期、签名算法、密钥管理以及跨页面的一致认证体验。合理的生命周期设计能够降低被滥用的风险,同时确保用户在访问多个页面时的体验是连贯的。

在 Spring Boot 2 的落地场景

对于一个典型的 Spring Boot 2 多页面应用,核心场景包括:登录、登出、受保护页面访问、以及跨页面的 token 使用。实现要点在于:前端通过浏览器在页面之间导航时保持用户状态,后端通过一个无状态的 JWT 过滤链来鉴权而不创建 HttpSession。

要点还包括CSRF 的保护策略SameSite 属性的合理配置,以及在前端模板中尽可能减少对令牌的暴露,避免 XSS 风险扩大。

系统架构与关键选型

令牌生命周期与策略

在无会话的 JWT 拟态中,建议采用 短寿命的访问令牌 + 可选的刷新令牌的组合。访问令牌在前端或 Cookie 中短期存在,刷新令牌在合规范围内用于再获取新的访问令牌,从而降低令牌被滥用的风险。

轮换策略:当使用刷新令牌时,刷新成功应伴随新令牌和新刷新令牌一起下发,旧令牌应尽快失效。这种轮换能显著降低攻击者窃取令牌后的长期利用时间。

Cookies 与 CSRF 的搭配

为了兼顾多页面应用的表单提交与 API 调用,将 JWT 放在 HttpOnly Cookie 中,并通过 CSRF 防护机制保护状态改变操作,是常见且稳健的做法。利用同源策略和 SameSite 属性可以降低 CSRF 攻击面,但仍需配合 CSRF 令牌实现完整防护。

在服务器端,可以通过 CookieCsrfTokenRepository 或模板渲染将 CSRF 令牌暴露给前端表单,确保表单提交时附带正确的 CSRF 令牌,避免跨站请求伪造。

实现要点:后端核心组件

JWT 生成与校验(JwtTokenProvider)

核心职责是对用户身份进行签发、校验与解析。使用 RS256 等非对称算法时,私钥负责签名,公钥负责校验,从而提升密钥管理的安全性。

该组件需要提供 创建令牌校验令牌、以及 从令牌中提取用户名或用户信息的方法,确保下游过滤器和控制器的对接简洁。

import io.jsonwebtoken.*;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;public class JwtTokenProvider {private final PrivateKey privateKey;private final PublicKey publicKey;private final long validityInMs;public JwtTokenProvider(PrivateKey privateKey, PublicKey publicKey, long validityInMs) {this.privateKey = privateKey;this.publicKey = publicKey;this.validityInMs = validityInMs;}public String createToken(String subject) {Date now = new Date();Date expiry = new Date(now.getTime() + validityInMs);return Jwts.builder().setSubject(subject).setIssuedAt(now).setExpiration(expiry).signWith(privateKey, SignatureAlgorithm.RS256).compact();}public boolean validateToken(String token) {try {Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);return true;} catch (JwtException | IllegalArgumentException e) {return false;}}public String getUsername(String token) {Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody();return claims.getSubject();}
}

请求鉴权过滤器(JwtAuthenticationFilter)

该过滤器在每次请求时从 Cookie 或 Authorization 头提取 JWT,完成解码后将认证信息放入 SecurityContext,以便后续的 Spring Security 路径匹配生效。

实现要点包括:无状态读取令牌从请求中还原用户信息、以及在没有有效令牌时不建立认证上下文。

Spring Boot 2 多页面应用:如何用 JWT 实现无 Session 的安全验证(实战与最佳实践)

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.*;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;public class JwtAuthenticationFilter extends OncePerRequestFilter {private final JwtTokenProvider jwtTokenProvider;public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {this.jwtTokenProvider = jwtTokenProvider;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, java.io.IOException {String token = resolveToken(request);if (token != null && jwtTokenProvider.validateToken(token)) {String username = jwtTokenProvider.getUsername(token);// 这里通常会加载 UserDetails,这里简化为直接设置一个基于用户名的认证对象UsernamePasswordAuthenticationToken auth =new UsernamePasswordAuthenticationToken(username, null, java.util.Collections.emptyList());SecurityContextHolder.getContext().setAuthentication(auth);}filterChain.doFilter(request, response);}private String resolveToken(HttpServletRequest request) {// 从 Cookie 中读取 JWT_TOKENif (request.getCookies() != null) {for (Cookie cookie : request.getCookies()) {if ("JWT_TOKEN".equals(cookie.getName())) {return cookie.getValue();}}}// 备选:从 Authorization 头读取String bearer = request.getHeader("Authorization");if (bearer != null && bearer.startsWith("Bearer ")) {return bearer.substring(7);}return null;}
}

Security 配置(SecurityConfig)

Security 配置将无会话策略设为 STATELESS,注入自定义过滤器,并对页面访问做细粒度授权。对登录接口开放对外,其他接口需鉴权。

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;@EnableWebSecurity
public class SecurityConfig {private final JwtTokenProvider jwtTokenProvider;public SecurityConfig(JwtTokenProvider jwtTokenProvider) {this.jwtTokenProvider = jwtTokenProvider;}public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.csrfTokenRepository(org.springframework.security.web.csrf.CookieCsrfTokenRepository.withHttpOnlyFalse())).sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeRequests(req -> req.antMatchers("/login", "/public/**").permitAll().anyRequest().authenticated()).addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class);return http.build();}
}

登录、登出与令牌刷新流程

登录流程

用户通过提交用户名与密码至服务器端的登录端点,服务器校验通过后生成 JWT 访问令牌,并通过 HttpOnly Cookie 下发给浏览器,以实现后续请求的自动携带。

登录成功后,浏览器会在同源请求中自动带上 JWT_TOKEN Cookie,后续对需要鉴权的页面访问即可走无会话鉴权路径。

@RestController
@RequestMapping("/auth")
public class AuthController {private final AuthenticationManager authenticationManager;private final JwtTokenProvider tokenProvider;public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider tokenProvider) {this.authenticationManager = authenticationManager;this.tokenProvider = tokenProvider;}@PostMapping("/login")public void login(@RequestBody LoginRequest login, HttpServletResponse response) {Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword()));String token = tokenProvider.createToken(auth.getName());Cookie cookie = new Cookie("JWT_TOKEN", token);cookie.setHttpOnly(true);cookie.setPath("/");cookie.setMaxAge(60 * 60); // 1 小时示例,实际按 validity 设置response.addCookie(cookie);}
}

登出与令牌刷新

登出时清除浏览器中的 JWT_TOKEN Cookie,令牌刷新通常通过单独的刷新端点完成,刷新端点在成功时下发新的 JWT 令牌,并更新相应的 Cookie。

刷新策略需要考虑对 刷新令牌的轮换与失效处理,以避免在未授权请求中继续使用旧令牌。

@PostMapping("/logout")
public void logout(HttpServletResponse response) {Cookie cookie = new Cookie("JWT_TOKEN", "");cookie.setHttpOnly(true);cookie.setPath("/");cookie.setMaxAge(0); // 删除 cookieresponse.addCookie(cookie);
}

前端与模板层的协同

页面访问与模板注入

在多页面应用的模板引擎中,可以通过在模板渲染阶段将当前用户信息注入到页面,避免在客户端暴露敏感信息,同时确保导航链接的可用性。

服务器端渲染的页面应对受保护区域进行判定,以免未认证用户看到受限内容。

CSRF 管控与表单提交

尽管使用了 JWT 栈实现无状态鉴权,状态变更请求仍需 CSRF 防护。表单提交时应包含 CSRF 令牌,或使用同源策略下的其它保护策略,确保安全。

// 演示前端向受保护接口提交表单,需带上 CSRF 令牌
fetch('/api/submit', {method: 'POST',credentials: 'include',headers: {'Content-Type': 'application/json','X-CSRF-TOKEN': getCsrfTokenFromCookieOrMeta()},body: JSON.stringify({ field: 'value' })
});

示例代码汇总

后端核心代码清单

下面的片段聚焦于实现无会话 JWT 验证的核心组件:JwtTokenProviderJwtAuthenticationFilter、以及 SecurityConfig

// JwtTokenProvider、JwtAuthenticationFilter、SecurityConfig 的完整实现见上方段落

配置与依赖

在 pom.xml 或 build.gradle 中引入 JWT 及 Spring Security 相关依赖,并配置私钥、公钥、令牌有效期等参数。确保密钥以安全方式存放,避免硬编码在代码中。

spring:security:oauth2:client:provider:jwt:issuer-uri: https://example.com

部署与运维要点

密钥管理与证书轮换

密钥对需要稳定且安全地存放在受信任的密钥库中,定期轮换密钥并实现密钥版本控制,以减少长期使用同一密钥的风险。

通过公钥分发机制,使前端与后端在签名校验时可以正确验证令牌的有效性,确保密钥的可用性与一致性

监控与告警

对鉴权失败率、令牌过期率、刷新失败率等指标进行监控,及时发现异常行为,并触发告警以便快速处置。

日志应包含对关键鉴权流程的审计信息,避免记录敏感数据,保护用户隐私

测试要点

在集成测试中,模拟不同令牌状态(有效、过期、无效、未授权)的场景,确保过滤器、CSRF、模板注入等各环节协同工作。

@Test
public void testAccessWithValidToken() {// 构造有效 JWT,并通过 Cookie 注入,请求受保护资源,断言返回 200
}

广告