深度解析:除了 JWT,你还用过哪些认证方案?Spring Security 中如何集成 JWT?
在构建现代 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 Auth | HTTP 头携带 base64(user:pass) | 内部测试、简单对接 | HTTP 原生支持 | 明文传输(必须配合 HTTPS),无法注销 |
1.1 什么时候选择 JWT?
- 分布式/微服务架构:无需共享存储,自带用户信息。
- 移动端/跨域:Cookie 限制少,头字段通用。
- 短期授权:如 API 调用、单点登录的临时凭证。
1.2 项目中的真实选型
- 内部运维系统:Session + Redis 共享存储。
- 开放平台 API:API Key + Secret 签名(防止重放)。
- 用户端 App + Web:JWT(无状态,便于水平扩展)。
- 企业 SSO 接入:OAuth2 或 SAML(对接公司统一认证)。
二、随机数在安全中的妙用(你一定用过!)
随机数并非仅仅"随机生成一个数字",在安全领域有广泛且关键的用途:
| 应用场景 | 随机数作用 | 示例 |
|---|---|---|
| 密码加盐(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 生成器(雪花算法) | 随机+时间+机器号,生成全局唯一 ID | Snowflake ID |
2.1 Java 中生成随机数的正确姿势
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 整体架构流程图
无效
有效
客户端登录
发送用户名密码
AuthenticationManager 验证
验证成功生成 JWT
返回 JWT 给客户端
后续请求携带 JWT
JwtAuthenticationFilter
解析 JWT 是否有效
返回 401
从 JWT 中提取用户信息
创建 UsernamePasswordAuthenticationToken
存入 SecurityContextHolder
放行请求
3.2 关键组件与代码实现
3.2.1 添加依赖(JJWT)
xml
<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(生成与解析)
java
@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)
java
@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 配置
java
@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)
java
@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) |
| 过滤器链 | 我们插入的 JwtAuthenticationFilter 在 UsernamePasswordAuthenticationFilter 之前执行 |
| 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。具体做法是:
- 编写 JwtAuthenticationFilter 继承
OncePerRequestFilter,解析请求头中的 JWT。- 如果 JWT 有效,从中提取用户名,调用
UserDetailsService加载权限,构造UsernamePasswordAuthenticationToken并存入SecurityContextHolder。- 在
SecurityConfig中禁用 Session,设置SessionCreationPolicy.STATELESS,并将自定义过滤器加到UsernamePasswordAuthenticationFilter之前。- 登录接口使用
AuthenticationManager校验用户名密码,校验通过后生成 JWT 返回。Spring Security 在这里提供认证管理 和权限控制的骨架,JWT 只是凭证载体。"