java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Security集成JWT

Spring Security中集成JWT的完整实战指南

作者:身如柳絮随风扬

本文深入探讨了现代Web应用中的认证授权方案,对比分析了JWT、Session-Cookie、OAuth2/OIDC、SAML 2.0等主流方案的优缺点及适用场景,感兴趣的小伙伴可以了解下

在构建现代 Web 应用和微服务时,认证与授权是绕不开的核心话题。JWT(JSON Web Token)凭借无状态、跨域友好等特性成为业界宠儿,但并非唯一选择。同时,很多人疑惑:Spring Security 中到底怎么和 JWT 一起使用?随机数又在哪些安全场景中发挥作用? 本文将从替代方案对比、随机数应用、Spring Security + JWT 实战三个方面,为你彻底理清这些面试高频问题。

一、除了 JWT,你还用过哪些认证/授权方案

在实际项目中,我根据场景还使用过以下方案:

方案原理适用场景优点缺点
传统 Session-Cookie服务端存储 Session,客户端存储 Cookie单体应用、用户量不大实现简单,主动失效方便占用服务端内存,横向扩展需共享 Session
OAuth2 / OIDC第三方授权,颁发 Access Token + Refresh Token允许用户授权第三方应用访问资源(如“使用微信登录”)标准协议,广泛支持实现复杂,需要引入授权服务器
SAML 2.0基于 XML 的安全断言标记语言,通过 SSO 认证企业级单点登录(如 Microsoft ADFS)成熟的企业标准XML 臃肿,配置复杂
API Key客户端携带固定 Key 调用 API内部服务调用、简单开放 API实现极简易泄露,无有效期,无用户身份
Basic AuthHTTP 头携带 base64(user:pass)内部测试、简单对接HTTP 原生支持明文传输(必须配合 HTTPS),无法注销

1.1 什么时候选择 JWT

1.2 项目中的真实选型

二、随机数在安全中的妙用(你一定用过!)

随机数并非仅仅“随机生成一个数字”,在安全领域有广泛且关键的用途:

应用场景随机数作用示例
密码加盐(Salt)为每个密码生成唯一随机数,与密码一起哈希,抵抗彩虹表攻击hash(password + salt)
验证码随机生成数字/字母,短期有效,防止暴力 破解短信验证码 123456
CSRF Token每个会话或每个请求生成随机 Token,防御跨站请求伪造Spring Security 默认开启 _csrf
重置密码 Token用户请求重置密码时,生成不可猜测的随机 Token 发送至邮箱token = UUID.randomUUID()
会话 ID随机生成会话标识,难以伪造JSESSIONID
OAuth2 State 参数防止 CSRF 攻击,随机字符串与请求绑定state=abc123
API 签名 Nonce一次性的随机数,防止请求重放攻击微信支付 nonce_str
ID 生成器(雪花算法)随机+时间+机器号,生成全局唯一 IDSnowflake ID

2.1 Java 中生成随机数的正确姿势

// 安全的随机数(推荐)
SecureRandom secureRandom = new SecureRandom();
byte[] bytes = new byte[16];
secureRandom.nextBytes(bytes);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
// UUID(部分随机,不应用于高安全场景)
String uuid = UUID.randomUUID().toString();
// 生成指定位数的数字验证码
String code = String.valueOf(secureRandom.nextInt(900000) + 100000);

三、Spring Security 中如何使用 JWT?——完整实战

Spring Security 本身不直接提供 JWT 支持,需要通过 过滤器 解析 JWT 并手动将认证信息存入 SecurityContext。下面是标准做法。

3.1 整体架构流程图

3.2 关键组件与代码实现

3.2.1 添加依赖(JJWT)

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

3.2.2 JwtTokenUtil(生成与解析)

@Component
public class JwtTokenUtil {
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    public String generateToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(SignatureAlgorithm.HS256, secret)
            .compact();
    }

    public String getUsernameFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

3.2.3 JwtAuthenticationFilter(继承 OncePerRequestFilter)

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) throws IOException, ServletException {
        String token = extractToken(request);
        if (token != null && jwtTokenUtil.validateToken(token)) {
            String username = jwtTokenUtil.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

3.2.4 SecurityConfig 配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private JwtAuthenticationFilter jwtFilter;
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/login", "/register").permitAll()
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3.2.5 登录接口(生成 JWT)

@RestController
public class AuthController {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
            );
            String token = jwtTokenUtil.generateToken(request.getUsername());
            return ResponseEntity.ok(new JwtResponse(token));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(401).body("用户名或密码错误");
        }
    }
}

3.3 Spring Security 在 JWT 中所起的作用

角色说明
AuthenticationManager负责验证用户名密码
SecurityContextHolder存储已认证的用户信息,供后续业务代码使用(如 @PreAuthorize
过滤器链我们插入的 JwtAuthenticationFilterUsernamePasswordAuthenticationFilter 之前执行
UserDetailsService加载用户权限,用于生成 Authentication 对象

核心:Spring Security 负责管理认证状态,JWT 只是传递凭证的一种形式。我们编写的过滤器将 JWT 解析后转换为 Spring Security 能识别的 Authentication 对象。

四、总结与面试回答模板

面试官问:除了 JWT 你还用过其他认证方案吗?随机数用过没?

参考回答

“用过。在传统单体项目中我使用 Session + Redis 共享存储;对接第三方登录时使用 OAuth2;内部服务间调用使用 API Key + 签名。随机数在安全方面应用广泛:比如用户密码的 盐值、短信验证码、重置密码的 Token、OAuth2 的 state 参数 以及防止重放攻击的 Nonce。我们项目中用 SecureRandom 生成高强度的随机数,比 Random 更安全。”

面试官问:你在 JWT 哪儿使用过 Spring Security?

参考回答:“在 Spring Boot 项目中,我将 JWT 作为无状态认证方案集成到 Spring Security。具体做法是:

  1. 编写 JwtAuthenticationFilter 继承 OncePerRequestFilter,解析请求头中的 JWT。
  2. 如果 JWT 有效,从中提取用户名,调用 UserDetailsService 加载权限,构造 UsernamePasswordAuthenticationToken 并存入 SecurityContextHolder
  3. SecurityConfig 中禁用 Session,设置 SessionCreationPolicy.STATELESS,并将自定义过滤器加到 UsernamePasswordAuthenticationFilter 之前。
  4. 登录接口使用 AuthenticationManager 校验用户名密码,校验通过后生成 JWT 返回。

Spring Security 在这里提供认证管理权限控制的骨架,JWT 只是凭证载体。”

到此这篇关于Spring Security中集成JWT的完整实战指南的文章就介绍到这了,更多相关Spring Security集成JWT内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文