Spring Security JWT认证配置引发HTTP 403禁止访问问题排查
我正在开发基于Spring Boot的应用,使用Spring Security处理认证与授权,采用JWT实现令牌生成和无状态会话管理。尽管完成配置,但所有端点(含已配置无需认证的端点)均持续返回HTTP 403禁止访问错误,尝试多种方案未解决。
核心配置代码
SecurityConfig
package com.khanfar.project2.Security.config; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; 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; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthFilter JwtAuthFilter ; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf().disable() .cors().and() .authorizeHttpRequests() .requestMatchers("/api/v1/**").permitAll() .anyRequest().authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(JwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .authenticationProvider(authenticationProvider); return http.build(); } }
JwtAuthFilter
package com.khanfar.project2.Security.config; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import static org.springframework.http.HttpHeaders.AUTHORIZATION; @Component @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { private final JwtService jwtService ; private final UserDetailsService userDetailsService ; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { final String userEmail ; final String authHeader = request.getHeader(AUTHORIZATION) ; final String Token ; if (authHeader==null || !authHeader.startsWith("Bearer ")){ filterChain.doFilter(request , response); return; } Token = authHeader.substring(7); userEmail = jwtService.extractUsername (Token) ; if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); if (jwtService.isTokenValid(Token , userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails , null , userDetails.getAuthorities() ) ; authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request , response); } }
JwtService
package com.khanfar.project2.Security.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @Service public class JwtService { private static final String SECRET_KEY = "TAREQ KHANFAR"; public String extractUsername(String token) { return extractClaim(token , Claims::getSubject); } private Claims extractAllClaims(String token) { return Jwts.parserBuilder() .setSigningKey(getSignInKey()) .build() .parseClaimsJwt(token) .getBody(); } public String generateToken(UserDetails userDetails) { return generateToken(new HashMap<>() , userDetails) ; } public String generateToken (Map<String , Object> map , UserDetails userDetails ) { return Jwts.builder().setClaims(map) .setSubject(userDetails.getUsername() ) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis()+1000 * 60 * 24 )) .signWith(getSignInKey() , SignatureAlgorithm.HS256) .compact(); } public boolean isTokenValid(String token , UserDetails userDetails ) { final String username = extractUsername(token) ; return username.equals(userDetails.getUsername()) && !isTokenExpired(token) ; } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private Date extractExpiration(String token) { return extractClaim(token , Claims::getExpiration); } public <T> T extractClaim(String token , Function<Claims , T > claimsTFunction) { final Claims claims = extractAllClaims(token) ; return claimsTFunction.apply(claims) ; } private Key getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); return Keys.hmacShaKeyFor(keyBytes); } }
ApplicationConfig
package com.khanfar.project2.Security.config; import com.khanfar.project2.Security.user.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @RequiredArgsConstructor @Configuration public class ApplicationConfig { private final UserRepository userRepository ; @Bean public UserDetailsService userDetailsService() { return username -> userRepository.findByEmail(username) .orElseThrow(()->new UsernameNotFoundException("User not found .")); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } @Bean public AuthenticationManager authenticationManager (AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
AuthenticationService
package com.khanfar.project2.Security.auth; import com.khanfar.project2.Entity.Role; import com.khanfar.project2.Security.config.JwtService; import com.khanfar.project2.Security.user.User; import com.khanfar.project2.Security.user.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class AuthenticationService { private final UserRepository userRepository ; private final PasswordEncoder passwordEncoder ; private final JwtService jwtService ; private final AuthenticationManager authenticationManager ; public AuthenticationResponse register(RegisterRequest registerRequest) { var user = User.builder() .name(registerRequest.getName()) .email(registerRequest.getEmail()) .password(passwordEncoder.encode(registerRequest.getPassword())) .role(Role.USER) .build(); userRepository.save(user); var token = jwtService.generateToken(user); return new AuthenticationResponse().builder(). Token(token) .build(); } public AuthenticationResponse authenticateRequest(AuthenticationRequest request) { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getEmail() ,request.getPassword()) ); var user = userRepository.findByEmail(request.getEmail()).orElseThrow(); var token = jwtService.generateToken(user); return new AuthenticationResponse().builder(). Token(token) .build(); } }
AuthenticationController
package com.khanfar.project2.Security.auth; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; 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; @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService authenticationService; @PostMapping("/register") public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest registerRequest) { System.out.println(registerRequest.getEmail()); return ResponseEntity.ok(authenticationService.register(registerRequest)); } @PostMapping("/authenticate") public ResponseEntity<AuthenticationResponse> authenticateRequest(@RequestBody AuthenticationRequest Request) { return ResponseEntity.ok(authenticationService.authenticateRequest(Request)); } }
配置说明
在SecurityConfig中,已禁用CSRF,设置会话管理策略为SessionCreationPolicy.STATELESS,并配置以"/api/v1/**"开头的路由允许匿名访问。
调试日志
2023-07-01T22:15:52.695+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.security.web.FilterChainProxy : Securing GET /api/v1/demo 2023-07-01T22:15:52.695+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext 2023-07-01T22:15:52.695+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.s.w.session.SessionManagementFilter : Request requested invalid session id FFD79F7D00BB82527193D66022479C54 2023-07-01T22:15:52.697+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access 2023-07-01T22:15:52.698+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.security.web.FilterChainProxy : Securing GET /error 2023-07-01T22:15:52.701+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext 2023-07-01T22:15:52.702+03:00 DEBUG 26652 --- [nio-8081-exec-1] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
日志显示尽管设置了无状态,应用仍尝试使用会话,请求被Http403ForbiddenEntryPoint拒绝。
问题排查与解决方案
1. JWT密钥解析错误
当前SECRET_KEY为明文字符串"TAREQ KHANFAR",但代码中用Decoders.BASE64.decode(SECRET_KEY)解码,该字符串并非有效的Base64编码,会导致密钥生成失败,进而使JWT校验失效。
修复方案:
修改getSignInKey方法,直接使用字符串的字节数组生成密钥(需确保密钥长度符合HS256要求,至少32字节,若当前密钥过短,建议更换为更长的字符串):
import java.nio.charset.StandardCharsets; private Key getSignInKey() { byte[] keyBytes = SECRET_KEY.getBytes(StandardCharsets.UTF_8); return Keys.hmacShaKeyFor(keyBytes); }
2. 会话管理配置不完整
虽然设置了SessionCreationPolicy.STATELESS,但需额外禁用会话固定保护,并确保响应头不会触发会话创建:
// 在SecurityConfig的securityFilterChain方法中添加 http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .sessionFixation().none() .and() .headers().frameOptions().disable();
3. 权限路径匹配问题
显式指定无需认证的具体路径,避免路径匹配规则冲突:
.authorizeHttpRequests() .requestMatchers("/api/v1/auth/**", "/api/v1/demo").permitAll() .anyRequest().authenticated()
4. JWT过滤器异常处理缺失
JWT解析失败时直接放行请求,会导致匿名访问被拒绝,需添加异常捕获并返回401错误:
@Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { try { // 原有逻辑 } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Invalid JWT token"); return; } filterChain.doFilter(request, response); }
5. CORS配置缺失
启用CORS但未正确配置时,跨域请求可能被拦截,添加CORS配置Bean:
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("*")); // 根据实际环境调整 configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setExposedHeaders(List.of(AUTHORIZATION)); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; }
然后在SecurityConfig中关联该配置:
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
内容的提问来源于stack exchange,提问作者Tareq Khanfar

