在这里插入图片描述

目录

    • 1. 引言:无状态认证的崛起
    • 2. JWT (JSON Web Token) 核心概念
      • 2.1 什么是JWT?
      • 2.2 JWT的组成:Header, Payload, Signature
      • 2.3 JWT的工作原理
      • 2.4 JWT的优缺点与适用场景
    • 3. Spring Security中的JWT集成策略
      • 3.1 禁用Session管理与CSRF防护
      • 3.2 JWT认证流程概述
    • 4. 实战演练:构建JWT认证系统
      • 4.1 引入JWT库依赖
      • 4.2 JWT工具类:生成与解析Token
      • 4.3 自定义 JwtAuthenticationToken
      • 4.4 自定义 JwtAuthenticationConverter (或 AuthenticationProvider)
      • 4.5 自定义 JwtAuthenticationFilter
      • 4.6 更新 SecurityFilterChain 配置,集成JWT过滤器
      • 4.7 改造登录接口,返回JWT
      • 4.8 认证失败与权限不足的自定义处理
      • 4.9 测试JWT认证流程
    • 5. JWT的安全性与挑战
      • 5.1 Token过期与刷新机制
      • 5.2 JWT注销/黑名单机制
      • 5.3 密钥管理
      • 5.4 防止令牌盗用
    • 6. 常见陷阱与注意事项
    • 7. 阶段总结

1. 引言:无状态认证的崛起

传统的Web应用通常依赖于服务器端的HTTP Session来维护用户状态。每次用户登录后,服务器会创建一个Session并将其Session ID通过Cookie发送给客户端。客户端在后续请求中携带这个Cookie,服务器通过Session ID查找对应的Session,从而识别用户身份。

然而,这种基于Session的方式在以下场景中面临挑战:

  • 前后端分离: 前端(React, Vue, Angular)和后端(Spring Boot API)是独立的,它们之间可能存在跨域请求。Cookie通常受同源策略限制,且在前端应用中直接操作Cookie不方便。
  • 微服务架构: 用户请求可能需要经过多个微服务,Session的共享和管理(例如使用Sticky Session或Redis共享Session)变得复杂且增加了系统耦合度。
  • 移动应用/第三方应用: 移动客户端不能很好地支持Cookie,更倾向于通过Authorization Header传递凭证。
  • 水平扩展: 当服务器集群需要水平扩展时,Session共享成为瓶颈。

无状态认证应运而生。它意味着服务器不再存储用户会话信息,每次请求都携带完整的认证凭证。JWT (JSON Web Token) 是实现无状态认证的主流方案之一。

2. JWT (JSON Web Token) 核心概念

2.1 什么是JWT?

JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。这些信息以JSON对象的形式传输,可以被数字签名,从而可以验证其真实性和完整性。

  • 紧凑: JWT的体积很小,可以通过URL、POST参数或HTTP头轻松传输。
  • 自包含: JWT包含了所有必要的用户信息(通常是用户ID、角色、权限等),服务器无需查询数据库即可获取这些信息。
  • 安全: JWT可以通过数字签名进行验证,确保其未被篡改。

2.2 JWT的组成:Header, Payload, Signature

一个JWT通常由三部分组成,用.分隔:Header.Payload.Signature

A. Header (头部)
通常包含两个信息:

  • alg (algorithm):签名算法,如HMAC SHA256 (HS256) 或 RSA (RS256)。
  • typ (type):Token类型,通常是JWT
{"alg": "HS256","typ": "JWT"
}

Header会被Base64Url编码。

B. Payload (载荷)
包含声明 (claims),是关于实体(通常是用户)和附加数据的断言。声明分为三类:

  • Registered claims (注册声明): 预定义的一些声明,非强制,但推荐使用,例如:
    • iss (issuer):颁发者
    • exp (expiration time):过期时间
    • sub (subject):主题(通常是用户ID)
    • aud (audience):受众
    • iat (issued at):签发时间
  • Public claims (公共声明): 可以在JWT中自由定义的声明,但为了避免冲突,应该在IANA JWT Registry中注册,或者将其定义为URI。
  • Private claims (私有声明): 约定俗成的声明,用于在特定方之间共享信息,既不是注册声明也不是公共声明。例如,可以包含用户角色、权限列表等业务信息。
{"sub": "1234567890","name": "John Doe","iat": 1516239022,"exp": 1516242622, // 签发时间 + 有效期"roles": ["USER", "ADMIN"] // 私有声明
}

Payload也会被Base64Url编码。

C. Signature (签名)
用于验证Token的发送者,并确保Token在传输过程中没有被篡改。
签名是使用Header中指定的算法(例如HS256),将Base64Url编码后的Header、Base64Url编码后的Payload和密钥(secret)进行加密计算得到。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

2.3 JWT的工作原理

  1. 用户登录: 用户使用用户名和密码向认证服务器(应用后端)发送登录请求。
  2. 生成JWT: 认证服务器验证用户凭证。如果验证成功,根据用户ID、角色、权限等信息生成一个JWT,并用一个密钥进行签名。
  3. 返回JWT: 服务器将生成的JWT返回给客户端(通常在HTTP响应体中)。
  4. 客户端存储JWT: 客户端接收到JWT后,通常将其存储在本地存储(如LocalStorage或SessionStorage)中。
  5. 访问受保护资源: 客户端在后续每次访问受保护的API时,都会在HTTP请求头的Authorization字段中携带JWT,格式为Authorization: Bearer <JWT>
  6. 验证JWT: 资源服务器(应用后端)接收到请求后,从Authorization头中提取JWT。然后,它使用之前用于签名的密钥验证JWT的签名、检查Token是否过期,以及解析其中的声明(如用户ID、权限)。
  7. 授权与响应: 如果JWT有效且用户具有所需权限,服务器处理请求并返回数据。如果JWT无效或过期,或者用户权限不足,则返回错误(如401 Unauthorized或403 Forbidden)。

2.4 JWT的优缺点与适用场景

优点:

  • 无状态: 服务器无需存储Session,易于水平扩展,适用于微服务。
  • 紧凑自包含: 包含了所有必要的用户信息,减少了数据库查询。
  • 跨域友好: 不依赖Cookie,易于跨域请求。
  • 移动兼容性: 广泛应用于移动应用。

缺点:

  • Token无法实时注销: JWT一旦签发,在其有效期内都是有效的,服务器端无法强制使其失效(除非引入黑名单机制)。
  • Token过大: 如果Payload中包含太多信息,Token会变大,增加请求头大小。
  • 安全性考量:
    • 密钥安全: 签名密钥一旦泄露,攻击者可以伪造Token。
    • 传输安全: JWT应始终通过HTTPS传输,防止Token被截获。
    • XSS风险: 如果存储在LocalStorage,容易受到XSS攻击。
    • 无CSRF防护: 因为不依赖Session Cookie,JWT本身不提供CSRF防护,因此无需特别开启CSRF。

适用场景:

  • 前后端分离的Web应用。
  • 微服务架构中的API认证。
  • 移动应用和桌面应用。
  • 第三方OAuth2/OpenID Connect认证。

3. Spring Security中的JWT集成策略

在Spring Security中集成JWT,通常需要进行以下调整:

3.1 禁用Session管理与CSRF防护

由于JWT是无状态的,我们不再需要Spring Security的Session管理和CSRF防护功能。

            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置为无状态).csrf(csrf -> csrf.disable()) // 禁用CSRF防护

3.2 JWT认证流程概述

  1. JWT生成: 在用户登录成功后,后端生成JWT并返回。
  2. JWT传输: 客户端将JWT存储起来,并在每次请求时通过Authorization: Bearer <JWT>请求头发送。
  3. JWT解析与验证: Spring Security过滤器链中会插入一个自定义的JWT过滤器:
    • 它拦截所有请求,从Authorization头中提取JWT。
    • 使用预设的密钥解析并验证JWT的签名和有效期。
    • 如果验证成功,从JWT中提取用户ID和权限,创建Authentication对象。
    • Authentication对象设置到SecurityContextHolder中。
  4. 授权: 后续的Spring Security授权过滤器(如FilterSecurityInterceptor)会根据SecurityContextHolder中的认证信息进行授权决策。

4. 实战演练:构建JWT认证系统

我们将改造之前的项目,实现JWT认证。

4.1 引入JWT库依赖

我们将使用jjwt库来处理JWT。

        <!-- JJWT (JWT Library) --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.12.5</version> </dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.12.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.12.5</version><scope>runtime</scope></dependency>

4.2 JWT工具类:生成与解析Token

创建一个工具类来处理JWT的生成、解析和验证。

package com.example.springsecuritystage1.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;@Component
public class JwtUtil {// 密钥。生产环境务必从安全通道获取,不能硬编码。@Value("${jwt.secret:thisismyjwtsecretkeythatiwilluseforsigningandvalidatingtokensanditshouldbeverylongandcomplex}")private String secret;// JWT有效期 (毫秒),这里设置为1小时@Value("${jwt.expiration:3600000}")private long expiration; // 1 hourprivate SecretKey getSigningKey() {// 使用 HS256 算法生成密钥return Keys.hmacShaKeyFor(secret.getBytes());}// 生成Tokenpublic String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();// 将用户权限添加到claims中List<String> authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());claims.put("authorities", authorities);return createToken(claims, userDetails.getUsername());}private String createToken(Map<String, Object> claims, String subject) {Date now = new Date();Date expiryDate = new Date(now.getTime() + expiration);return Jwts.builder().setClaims(claims) // 自定义声明.setSubject(subject) // 用户名.setIssuedAt(now) // 签发时间.setExpiration(expiryDate) // 过期时间.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 签名算法和密钥.compact();}// 从Token中获取所有声明public Claims extractAllClaims(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}// 从Token中获取用户名public String extractUsername(String token) {return extractAllClaims(token).getSubject();}// 从Token中获取过期时间public Date extractExpiration(String token) {return extractAllClaims(token).getExpiration();}// 检查Token是否过期private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}// 验证Token是否有效public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}// 额外的:从Token中获取权限@SuppressWarnings("unchecked")public List<String> extractAuthorities(String token) {return (List<String>) extractAllClaims(token).get("authorities");}
}

application.yml中添加JWT配置:

jwt:secret: your_jwt_secret_key_that_is_very_long_and_complex_and_should_be_kept_secure_in_production # 至少32位,生产环境务必使用更长更随机的密钥expiration: 3600000 # 1小时,单位毫秒

4.3 自定义 JwtAuthenticationToken

ApiKeyAuthenticationToken类似,我们需要一个Authentication实现来承载从JWT解析出的认证信息。

package com.example.springsecuritystage1.security.token;import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;import java.util.Collection;public class JwtAuthenticationToken extends AbstractAuthenticationToken {private final Object principal; // 用户名或UserDetails对象private String credentials; // JWT字符串本身public JwtAuthenticationToken(String jwtToken) {super(null);this.principal = null; // 初始时principal是nullthis.credentials = jwtToken; // JWT Token作为凭证setAuthenticated(false);}public JwtAuthenticationToken(Object principal, String jwtToken, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = jwtToken;setAuthenticated(true);}@Overridepublic Object getCredentials() {return credentials;}@Overridepublic Object getPrincipal() {return principal;}
}

4.4 自定义 JwtAuthenticationConverter (或 AuthenticationProvider)

Spring Security 6.x 推荐使用BearerTokenAuthenticationConverterReactiveJwtDecoder等用于OAuth2 Resource Server,但对于自定义的JWT,我们可以继续使用AuthenticationProvider

package com.example.springsecuritystage1.security.provider;import com.example.springsecuritystage1.security.token.JwtAuthenticationToken;
import com.example.springsecuritystage1.service.CustomUserDetailsService; // 你的UserDetailsService
import com.example.springsecuritystage1.util.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {private final JwtUtil jwtUtil;private final CustomUserDetailsService userDetailsService; // 用于加载用户详情public JwtAuthenticationProvider(JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) {this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;String jwt = (String) jwtAuthenticationToken.getCredentials();try {String username = jwtUtil.extractUsername(jwt);List<String> authoritiesStrings = jwtUtil.extractAuthorities(jwt); // 从JWT中提取权限// 可以选择从数据库再次加载UserDetails,以确保用户状态最新// 或者仅仅使用JWT中的信息构建User对象UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails)) {Set<SimpleGrantedAuthority> authorities = authoritiesStrings.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());return new JwtAuthenticationToken(userDetails, jwt, authorities);} else {throw new BadCredentialsException("Invalid JWT token");}} catch (ExpiredJwtException e) {throw new BadCredentialsException("JWT Token has expired", e);} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {throw new BadCredentialsException("Invalid JWT Token", e);}}@Overridepublic boolean supports(Class<?> authentication) {return JwtAuthenticationToken.class.isAssignableFrom(authentication);}
}

注意:JwtAuthenticationProvider中,我们从JWT中提取了权限信息。但为了确保用户状态(如enabledaccountNonLocked)是最新的,我们仍然通过userDetailsService.loadUserByUsername(username)从数据库加载了完整的UserDetails。如果JWT中包含足够的信息且不关心实时状态,可以直接基于JWT信息构建User对象。

4.5 自定义 JwtAuthenticationFilter

这个过滤器负责拦截请求,提取JWT,并将其提交给AuthenticationManager

package com.example.springsecuritystage1.filter;import com.example.springsecuritystage1.security.token.JwtAuthenticationToken;
import com.example.springsecuritystage1.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;// JWT 认证过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final AuthenticationManager authenticationManager; // 注入 AuthenticationManagerpublic JwtAuthenticationFilter(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// 1. 从 Authorization header 中获取 JWT TokenString authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);String jwt = null;if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {jwt = authorizationHeader.substring(7); // 提取Bearer Token}// 如果没有JWT,或者SecurityContext中已经有认证信息(例如通过Session登录),则跳过if (jwt == null || SecurityContextHolder.getContext().getAuthentication() != null) {filterChain.doFilter(request, response);return;}try {// 2. 创建一个未认证的 JwtAuthenticationTokenJwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(jwt);// 3. 将Token提交给 AuthenticationManager 进行认证Authentication authentication = authenticationManager.authenticate(authenticationToken);// 4. 认证成功,将认证信息存入 SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(authentication);System.out.println("JWT authenticated successfully for: " + authentication.getName());} catch (Exception e) {// 认证失败,清除SecurityContext,并返回401 UnauthorizedSecurityContextHolder.clearContext();response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("JWT authentication failed: " + e.getMessage());return; // 阻止请求继续往下走}// 继续过滤器链filterChain.doFilter(request, response);}
}

4.6 更新 SecurityFilterChain 配置,集成JWT过滤器

现在,我们需要在CustomSecurityConfig中添加JwtAuthenticationProviderAuthenticationManager,并将JwtAuthenticationFilter插入到过滤器链中。同时,禁用Session管理和CSRF防护。

package com.example.springsecuritystage1.config;// ... 省略其他 imports
import com.example.springsecuritystage1.filter.ApiKeyAuthenticationFilter;
import com.example.springsecuritystage1.filter.JwtAuthenticationFilter; // 导入 JWT 过滤器
import com.example.springsecuritystage1.security.provider.ApiKeyAuthenticationProvider;
import com.example.springsecuritystage1.security.provider.JwtAuthenticationProvider; // 导入 JWT Provider
import com.example.springsecuritystage1.util.JwtUtil; // 导入 JWT 工具类
import org.springframework.http.HttpMethod; // 导入
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.http.SessionCreationPolicy; // 导入@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class CustomSecurityConfig {private final DataSource dataSource;private final UserDetailsService userDetailsService;private final PasswordEncoder passwordEncoder;private final ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;private final JwtAuthenticationProvider jwtAuthenticationProvider; // 注入 JWT Providerprivate final JwtUtil jwtUtil; // 注入 JWTUtilpublic CustomSecurityConfig(DataSource dataSource,UserDetailsService userDetailsService,PasswordEncoder passwordEncoder,ApiKeyAuthenticationProvider apiKeyAuthenticationProvider,JwtAuthenticationProvider jwtAuthenticationProvider,JwtUtil jwtUtil) {this.dataSource = dataSource;this.userDetailsService = userDetailsService;this.passwordEncoder = passwordEncoder;this.apiKeyAuthenticationProvider = apiKeyAuthenticationProvider;this.jwtAuthenticationProvider = jwtAuthenticationProvider;this.jwtUtil = jwtUtil;}@Beanpublic PasswordEncoder passwordEncoder() { /* ... */ return new BCryptPasswordEncoder(); }@Beanpublic UserDetailsService userDetailsService() { /* ... */ return new CustomUserDetailsService(sysUserMapper); }@Beanpublic PersistentTokenRepository persistentTokenRepository() { /* ... */ return tokenRepository; }@Beanpublic ProviderManager authenticationManager() {DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();daoProvider.setUserDetailsService(userDetailsService);daoProvider.setPasswordEncoder(passwordEncoder);// ProviderManager 现在包含 DaoAuthenticationProvider, ApiKeyAuthenticationProvider 和 JwtAuthenticationProviderreturn new ProviderManager(daoProvider, apiKeyAuthenticationProvider, jwtAuthenticationProvider);}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()) // <<-- HERE: 禁用CSRF防护.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // <<-- HERE: 设置为无状态会话策略).authorizeHttpRequests(authorize -> authorize// 允许所有请求,因为我们现在是无状态API,登录获取Token.requestMatchers("/api/auth/**", "/public/**", "/register", "/login").permitAll()// 不需要这些Web页面的权限配置了,因为它们现在应该由前端路由控制// .requestMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN", "USER_MANAGE")// .requestMatchers("/user/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN", "USER_VIEW").requestMatchers("/api/v2/**").hasAuthority("API_KEY_AUTH").anyRequest().authenticated() // 其他所有 API 请求都需要认证 (JWT 或 API Key))// 移除了 formLogin 和 rememberMe, 因为现在是无状态API.httpBasic(Customizer.withDefaults()) // 可以在测试阶段保留HTTP Basic.addFilterBefore(new ApiKeyAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)// <<-- HERE: 将 JwtAuthenticationFilter 添加到 ApiKeyAuthenticationFilter 之后,UsernamePasswordAuthenticationFilter 之前// 但因为我们禁用了 Session,UsernamePasswordAuthenticationFilter 实际上不会被用到,可以考虑移除// 这里我们放在 ApiKeyAuthenticationFilter 之后,保证 JWT 认证在 API Key 认证之后尝试.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), ApiKeyAuthenticationFilter.class);// TODO: 为JWT认证添加适当的异常处理器,例如 AuthenticationEntryPoint// .exceptionHandling(exception -> exception//     .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 稍后添加// )return http.build();}@Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher(); // 即使是 STATELESS,这个Bean本身没有什么副作用,可以保留}
}

重要的更新点:

  1. JWT相关注入: JwtAuthenticationProviderJwtUtil被注入,并JwtAuthenticationProvider添加到ProviderManager中。
  2. 禁用CSRF和Session: csrf(csrf -> csrf.disable())sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 是实现无状态的关键。
  3. 移除Session相关配置: formLogin()rememberMe()配置被移除,因为它们依赖于Session。
  4. JWT过滤器添加: JwtAuthenticationFilter通过 addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), ApiKeyAuthenticationFilter.class) 添加到过滤器链中,它将在ApiKeyAuthenticationFilter之前尝试处理JWT认证。你可以自行调整顺序。
  5. UsernamePasswordAuthenticationFilter的去留: 由于我们禁用了Session和表单登录,UsernamePasswordAuthenticationFilter实际上不再具有作用。此处将其保留在addFilterBefore的参考中,但如果你不打算使用HTTP Basic或传统的表单登录,可以完全移除对它的引用,或者直接将其替换。对于纯API,我们通常不会使用UsernamePasswordAuthenticationFilter
    • 更新: 为了清晰,我们将JWT过滤器放在所有认证过滤器之前,让它优先处理Bearer Token。
    • UsernamePasswordAuthenticationFilter.class 如果不使用表单登录,可以将其作为参考位置,或者使用更通用的过滤器,如 BasicAuthenticationFilter.class。这里,我们将API key认证放在它之前,JWT认证放在API key认证之前,形成优先顺序。

4.7 改造登录接口,返回JWT

我们需要创建一个新的登录Controller,它接收用户名和密码,并在认证成功后返回JWT。

LoginApiController.java

package com.example.springsecuritystage1.controller;import com.example.springsecuritystage1.model.LoginRequest;
import com.example.springsecuritystage1.model.LoginResponse;
import com.example.springsecuritystage1.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;// 登录请求体
class LoginRequest {private String username;private String password;// Getters and Setterspublic String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public String getPassword() { return password; }public void setPassword(String password) { this.password = password; }
}// 登录响应体 (包含JWT)
class LoginResponse {private String token;private String type = "Bearer";private Long id;private String username;private String email; // 假设有private List<String> roles; // 假设有// Constructors, Getters, Setterspublic LoginResponse(String accessToken, Long id, String username, String email, List<String> roles) {this.token = accessToken;this.id = id;this.username = username;this.email = email;this.roles = roles;}public String getToken() { return token; }public void setToken(String token) { this.token = token; }public String getType() { return type; }public void setType(String type) { this.type = type; }public Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public String getEmail() { return email; }public void setEmail(String email) { this.email = email; }public List<String> getRoles() { return roles; }public void setRoles(List<String> roles) { this.roles = roles; }
}@RestController
@RequestMapping("/api/auth")
public class LoginApiController {private final AuthenticationManager authenticationManager;private final JwtUtil jwtUtil;public LoginApiController(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {this.authenticationManager = authenticationManager;this.jwtUtil = jwtUtil;}@PostMapping("/login")public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));// 如果上面认证失败,会抛出 AuthenticationException,不会走到这里SecurityContextHolder.getContext().setAuthentication(authentication);UserDetails userDetails = (UserDetails) authentication.getPrincipal();String jwt = jwtUtil.generateToken(userDetails);// 这里仅为了演示,id, email, roles可以从 userDetails 中提取或从数据库查询List<String> roles = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());return ResponseEntity.ok(new LoginResponse(jwt, null, userDetails.getUsername(), null, roles));}
}

4.8 认证失败与权限不足的自定义处理

由于我们禁用了Session和表单登录,Spring Security默认的重定向行为将不再适用。对于API,我们应该返回JSON格式的错误响应。

A. 未认证 (AuthenticationEntryPoint)
当用户未提供凭证或凭证无效时,AuthenticationEntryPoint会被触发。

package com.example.springsecuritystage1.security.handler;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import java.io.IOException;// 处理未认证的请求,返回401 Unauthorized
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)throws IOException, ServletException {System.out.println("Unauthorized error: " + authException.getMessage());response.setContentType("application/json");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getOutputStream().println("{ \"error\": \"" + authException.getMessage() + "\", \"code\": 401 }");}
}

B. 权限不足 (AccessDeniedHandler)
当用户已认证但没有所需权限时,AccessDeniedHandler会被触发。

package com.example.springsecuritystage1.security.handler;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import java.io.IOException;// 处理权限不足的请求,返回403 Forbidden
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)throws IOException, ServletException {System.out.println("Access Denied error: " + accessDeniedException.getMessage());response.setContentType("application/json");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getOutputStream().println("{ \"error\": \"" + accessDeniedException.getMessage() + "\", \"code\": 403 }");}
}

C. 更新SecurityFilterChain,集成异常处理器

            .exceptionHandling(exception -> exception // <<-- HERE: 集成自定义异常处理器.authenticationEntryPoint(jwtAuthenticationEntryPoint) // 未认证.accessDeniedHandler(customAccessDeniedHandler) // 权限不足)

需要注入这两个handler:

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;private final CustomAccessDeniedHandler customAccessDeniedHandler;public CustomSecurityConfig(// ... 其他注入JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,CustomAccessDeniedHandler customAccessDeniedHandler) {// ... 初始化this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;this.customAccessDeniedHandler = customAccessDeniedHandler;}

4.9 测试JWT认证流程

  1. 启动应用。
  2. 获取JWT: 使用Postman向 http://localhost:8080/api/auth/login 发送POST请求,Content-Type: application/json
    Body:
    {"username": "user","password": "password"
    }
    
    成功后,你应该会收到一个包含JWT的JSON响应,例如:
    {"token": "eyJhbGc...","type": "Bearer","username": "user","roles": ["ROLE_USER", "PRODUCT_READ", "USER_VIEW"]
    }
    
  3. 使用JWT访问受保护资源:
    • 复制得到的token
    • http://localhost:8080/user/profile 发送GET请求,在请求头中添加 Authorization: Bearer <你的JWT>
    • 你应该会收到 200 OK 响应,表示访问成功。
  4. 访问无权限资源:
    • 继续使用同一个JWT(user用户的),尝试访问 http://localhost:8080/admin/dashboard
    • 你应该收到 403 Forbidden 响应,内容为我们自定义的JSON错误。
  5. 访问需要API Key的资源:
    • 尝试使用JWT访问 http://localhost:8080/api/v2/secret-data
    • 由于这个路径需要API_KEY_AUTH权限,而JWT中可能没有,所以还是会收到403 Forbidden
    • 此时,如果你在请求头中同时提供正确的X-API-KEY,API Key认证会优先触发,导致最终成功。这展示了多认证机制的协同工作。
  6. 无效/过期JWT:
    • 尝试随便修改JWT的某个字符,或者等待JWT过期(如果设置了短有效期)。
    • 再次发送请求,你应该收到 401 Unauthorized 响应。

5. JWT的安全性与挑战

5.1 Token过期与刷新机制

  • 过期目的: JWT的exp声明是其安全性的关键。短有效期可以限制令牌被盗用后的风险。
  • 刷新Token: 通常通过引入Refresh Token机制。
    • 用户登录后,同时获取一个短期的Access Token(JWT)和一个长期的Refresh Token
    • Access Token用于访问资源。
    • Access Token过期时,客户端使用Refresh Token向认证服务器请求新的Access TokenRefresh Token
    • Refresh Token通常存储在更安全的地方(如HttpOnly Cookie),并且只能使用一次,或者有被撤销的机制。

5.2 JWT注销/黑名单机制

JWT无法像Session一样简单地“注销”。一旦签发,只要签名和有效期都没问题,它就是有效的。
为了实现注销功能或禁用被盗用的Token,可以采取:

  • 黑名单机制: 在服务器端维护一个已注销/失效的JWT列表(通常存储在Redis中,设置与JWT有效期相同的过期时间)。每次验证JWT时,除了验证签名和有效期,还需检查其是否在黑名单中。
  • 短有效期结合刷新: 这是更常见的做法。Access Token有效期设置很短,Refresh Token有效期长。当用户登出时,只销毁Refresh Token,Access Token自然很快过期。

5.3 密钥管理

  • 生成与存储: 签名JWT的密钥(secret)至关重要,必须是复杂、随机且妥善保管的。生产环境应通过环境变量、配置文件或密钥管理服务(如Vault)注入,绝不能硬编码。
  • 轮换: 定期轮换密钥是一种良好的安全实践。

5.4 防止令牌盗用

  • Https: 始终通过HTTPS传输JWT,防止中间人攻击窃取Token。
  • HttpOnly: 如果Token存储在Cookie中,应设置为HttpOnly,防止XSS攻击。
  • LocalStorage的风险: 将JWT存储在LocalStorage中虽然方便,但易受XSS攻击。

6. 常见陷阱与注意事项

  • 禁用CSRF与Session的警惕性: 只有当你确定你的应用不再依赖于Session,并且有其他安全措施时,才禁用它们。
  • JWT密钥安全: 生产环境的JWT密钥必须是强随机字符串,且妥善保管。
  • JWT负载信息: 不要在JWT的Payload中存放敏感信息。JWT只是Base64编码,不是加密。
  • JWT有效期: 根据业务需求合理设置JWT有效期。Access Token通常短,Refresh Token长。
  • 异常处理: 务必为AuthenticationEntryPointAccessDeniedHandler提供友好的JSON响应。
  • AuthenticationManager的构建: 确保ProviderManager包含了所有你需要的AuthenticationProvider

7. 阶段总结

至此,你已经完成了Spring Security深度学习的第六阶段!你现在已经能够:

  • 理解JWT的核心概念、组成和工作原理。
  • 使用jjwt库生成、解析和验证JWT。
  • 在Spring Security中禁用Session和CSRF防护,构建一个无状态的API认证系统。
  • 设计JwtAuthenticationTokenJwtAuthenticationProviderJwtAuthenticationFilter,并将其集成到Spring Security过滤器链中。
  • 改造登录接口,使其返回JWT。
  • 定制API认证失败和权限不足的JSON响应。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/95596.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/95596.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/95596.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无名信号量

include <myhead.h> oid *task( void *file_size)int file_size1*(int*)file_size;//打开源文件int fdopen("./hello",O_RDONLY);if(fd-1){perror("open error\n");return NULL;}//打开目标文件int fd1open("./world",O_WRONLY);if(fd1-1)…

免费CRM系统与Excel客户管理的区别

很多中小企业在客户管理初期&#xff0c;会选择使用Excel表格进行客户数据的整理与维护。但随着业务规模扩大&#xff0c;客户信息日益复杂&#xff0c;Excel逐渐暴露出诸多局限性。此时&#xff0c;免费CRM系统应运而生&#xff0c;成为企业客户管理升级的重要选择。本文将深入…

linux Nginx服务配置介绍,和配置流程

1、Nginx 配置介绍认识Nginx服务的主配置文件 nginx.confnginx的配置文件一般在 /usr/local/nginx/conf/下&#xff0c;然后直接vim nginx.com 即可编辑1.1 全局配置介绍全局配置位于主配置文件最顶部&#xff0c;作用于整个Nginx服务进程&#xff0c;影响服务的资源分配、运行…

文字识别接口-文字识别技术-ocr api

文字识别接口&#xff0c;顾名思义&#xff0c;就是一种将图像文字或手写文字转换为可编辑文本的技术。文字识别接口&#xff0c;基于深度学习算法与自主ocr核心实现多种场景字符的高精度识别与结构化信息提取&#xff0c;现已被广泛应用于银行、医疗、财会、教育等多个领域。随…

DeepSeek R1大模型微调实战-llama-factory的模型下载与训练

文章目录概要1.下载模型2.llama factory 训练模型2.1 模型微调2.2 模型评估2.3 模型对话2.4 导出模型3.硬件选择概要 LLaMA Factory 是一个简单易用且高效的大型语言模型训练与微调平台。通过它&#xff0c;用户可以在无需编写任何代码的前提下&#xff0c;在本地完成上百种预…

C++ map和set

C参考文献&#xff1a;cplusplus.com - The C Resources Network 目录 一、序列式容器和关联式容器 二、set系列 &#xff08;1&#xff09;set类的介绍 &#xff08;2&#xff09;set的构造和迭代器 &#xff08;3&#xff09;set的接口 1.insert​编辑 2.find和erase 3…

头一次见问这么多kafka的问题

分享一篇粉丝朋友整理的面经&#xff0c;第一次遇见问那么多kafka的问题&#xff0c;看看他是怎么回答的。 先来看看 职位描述&#xff1a; 岗位职责&#xff1a; 负责基于 Go 的后端服务的设计、开发和维护&#xff1b;参与系统架构设计&#xff0c;确保系统的高可用性、高性能…

自底向上了解CPU的运算

文章目录 引言 CPU如何实现逻辑运算 NMOS和PMOS 基于MOS管组合下的逻辑门运算 逻辑运算下运算的实现 ALU的诞生 CPU的诞生 关于二进制运算的研究 十进制转二进制基础换算 为什么负数要使用补码进行表示 为什么反码就能解决正负数相加问题,我们还需要用补码来表示负数呢? 小数…

apache poi与Office Open XML关系

以下内容来自AI https://ecma-international.org/publications-and-standards/standards/ecma-376/ 官方规范 https://poi.apache.org/components/oxml4j/index.html java中针对Office Open XML的实现 Apache poi中各个组件 https://poi.apache.org/components/index.html …

S32K328上芯片内部RTC的使用和唤醒配置

1&#xff1a;RTC介绍 1.1 RTC基础功能介绍 参考《S32K3xx Reference Manual》&#xff0c;S32K328芯片内部自带RTC功能&#xff0c;并且支持从低功耗状态下唤醒设备&#xff1b;1.2 RTC电源介绍 由以下三张图可知 1&#xff1a;RTC由V11供电&#xff0c;V11依赖外部V15供电&am…

【Python】数据可视化之分类图

目录 条形图 箱形图 散点图 分簇散点图 小提琴 分簇小提琴 条形图 条形图是一种直观的图表形式&#xff0c;它通过不同长度的矩形条&#xff08;即“条形”&#xff09;来展示数值变量的中心趋势估计值&#xff0c;其中每个矩形的高度直接对应于该组数据的某个中心量度&…

RabbitMQ模型详解与常见问题

项目demo地址&#xff1a;https://github.com/tian-qingzhao/rabbitmq-demo 一、RabbitMQ组件概念 1.1 Server&#xff1a;接收客户端的连接&#xff0c;实现AMQP实体服务。 1.2 Connection&#xff1a;连接 应用程序与Server的网络连接&#xff0c;TCP连接。 1.3 Channel&…

网络:相比于HTTP,HTTPS协议到底安全在哪?

网络&#xff1a;相比于HTTP&#xff0c;HTTPS协议到底安全在哪&#xff1f; 我们知道HTTPS也是一种应用层协议&#xff0c;它在HTTP的基础上有一层加密&#xff0c;因为HTTP的数据传输都是以明文方式传输的&#xff0c;所以加密主要是为了防止数据在传输的时候被篡改 今天我…

AI 基础设施新范式,百度百舸 5.0 技术深度解析

本文整理自 2025 年 8 月 29 日百度云智大会 —— AI 算力平台专题论坛&#xff0c;百度智能云 AI 计算首席科学家王雁鹏的同名主题演讲。大家下午好&#xff01;昨天在主论坛&#xff0c;我们正式发布了百度百舸 AI 计算平台 5.0&#xff0c;并展示了多项亮眼的性能数据。今天…

IO进程线程;多线程;线程互斥同步;互斥锁;无名信号量;条件变量;0905

思维导图多线程打印ABC运用无名面量 实现进程同步#include<myhead.h> //定义 无名信号量 sem_t sem1; sem_t sem2; sem_t sem3; //线程1 void* task1(void *arg) {while(1){sem_wait(&sem1);printf("A");fflush(stdout);sleep(1);sem_post(&sem2);} } …

固高 GTS-800 运动控制卡完全使用指南:从硬件部署到高阶应用

固高 GTS-800 系列运动控制卡作为中端工业控制领域的标杆产品,以其 8-16 轴同步控制能力、丰富的插补功能和稳定的性能,广泛应用于激光加工、PCB 制造、精密装配等自动化设备中。本文将系统讲解 GTS-800 的硬件架构、开发环境搭建、核心功能实现及工程实践技巧,帮助工程师快…

STM32F103_Bootloader程序开发15 - 从Keil到vscode + EIDE + GCC的迁移实践

导言 STM32 - Embedded IDE - GCC - 如何在工程中生成.bin格式固件 STM32 - Embedded IDE - GCC - 使用 GCC 链接脚本限制 Flash 区域 STM32 - Embedded IDE - GCC - 如何在工程中定义一段 NoInit RAM 内存 STM32 - Embedded IDE - GCC - 如何将编译得到的.bin固件添加CRC32校验…

HTTP协议——理解相关概念、模拟实现浏览器访问自定义服务器

文章目录HTTP协议理解相关概念HTTP相关背景知识认识URLHTTP协议在网络通信的宏观认识urlencode & urldecodeHTTP请求和应答的格式模拟实现浏览器访问自定义服务器关于http requesthttp request的请求行——URI使用浏览器完成静态资源的访问常用的报头属性http response状态…

【服务器】英伟达M40显卡风冷方案心得

在之前的博文中&#xff0c;博主说到最近准备自己组装一台服务器&#xff0c;主要用于有限元仿真&#xff0c;其次兼顾一部分AI机器学习的工作&#xff0c;于是博主就入手了一张英伟达Tesla M40的12G显卡GPU。本来博主也纠结过是买M40还是M60&#xff0c;后来在网上看到说M60看…

Java中的锁升级机制

目录 核心思想 Java对象头&#xff08;Object Header&#xff09;与Mark Word 锁升级的详细步骤 1. 无锁&#xff08;No Lock&#xff09; 2. 偏向锁&#xff08;Biased Locking&#xff09; 3. 轻量级锁&#xff08;Lightweight Lock&#xff09; 4. 重量级锁&#xff…