Spring Boot带有效JWT访问POST安全端点仍返回403 Forbidden
Spring Boot JWT认证/api/game/save端点返回403 Forbidden问题解决
问题描述
开发基于Spring Boot的项目时,已实现JWT认证机制,但/api/game/save端点在Authorization头携带有效JWT的情况下,仍返回403 Forbidden响应。
现有代码
安全配置
package com.clicker.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/register", "/api/auth/login").permitAll() .requestMatchers("/api/game/**").authenticated() .anyRequest().authenticated()); http.sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); return http.build(); } }
JWT工具类(JwtUtil)
package com.clicker.component; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.stereotype.Component; import java.util.Date; import static io.jsonwebtoken.Jwts.builder; @Component public class JwtUtil { private static final String SECRET_KEY = "secret_key"; // 实际使用请替换为安全密钥 private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 10; // 10小时 public String generateToken(String username) { return builder() .setSubject(username) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } public String extractUsername(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } }
控制器端点
package com.clicker.controller; import com.clicker.service.GameDataService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/game") public class GameDataController { @Autowired private GameDataService gameDataService; @PostMapping("/save") public ResponseEntity<?> saveGameData(@RequestHeader("Authorization") String token, @RequestBody String gameDataJson) { return gameDataService.saveGameData(token, gameDataJson); } @PostMapping("/load") public ResponseEntity<?> loadGameData(@RequestHeader("Authorization") String token) { return gameDataService.loadGameData(token); } }
服务类
package com.clicker.service; import com.clicker.component.JwtUtil; import com.clicker.entity.GameData; import com.clicker.entity.User; import com.clicker.repository.GameDataRepository; import com.clicker.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @Service public class GameDataService { @Autowired private GameDataRepository gameDataRepository; @Autowired private UserRepository userRepository; @Autowired private JwtUtil jwtUtil; public ResponseEntity<?> saveGameData(String token, String gameDataJson) { String username = jwtUtil.extractUsername(token.substring(7)); User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("User not found!")); GameData gameData = gameDataRepository.findByUser(user) .orElse(new GameData()); gameData.setUser(user); gameData.setData(gameDataJson); gameDataRepository.save(gameData); return ResponseEntity.ok("Game data saved successfully!"); } public ResponseEntity<?> loadGameData(String token) { String username = jwtUtil.extractUsername(token.substring(7)); User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("User not found!")); return gameDataRepository.findByUser(user) .map(gameData -> ResponseEntity.ok(gameData.getData())) .orElse(ResponseEntity.ok("{\n" + " \"playerStats\": { \"solarEnergy\": 0, \"multiplyCostData\": 200, \"solarEnergyPerClick\": 1, \"solarEnergyPerSecond\": 0},\n" + " \"planets\": {\n" + " \"mercury\": { \"unlocked\": false, \"cost\": 500 },\n" + " \"venus\": { \"unlocked\": false, \"cost\": 2500 },\n" + " \"earth\": { \"unlocked\": false, \"cost\": 12500 },\n" + " \"moon\": { \"unlocked\": false, \"cost\": 62500 }\n" + " }\n" + "}")); } public boolean validateToken(String token) { return jwtUtil.validateToken(token); } }
application.properties
spring.application.name=Solar-Planet-Clicker spring.datasource.url=jdbc:mysql://localhost:3306/solar_planet_clicker spring.datasource.username=root spring.datasource.password= spring.jpa.hibernate.ddl-auto=update spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
问题根源
当前SecurityConfig仅配置了请求的认证规则,但未添加JWT认证过滤器,Spring Security无法解析Authorization头中的JWT令牌,无法识别用户身份,因此直接返回403 Forbidden。
解决方案
1. 创建JWT认证过滤器
该过滤器负责从请求头提取JWT、验证令牌,并将认证信息注入Spring Security上下文。
package com.clicker.configuration; import com.clicker.component.JwtUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { this.jwtUtil = jwtUtil; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String token = null; String username = null; // 提取Bearer令牌 if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); username = jwtUtil.extractUsername(token); } // 验证令牌并设置认证上下文 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(token)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
2. 修改SecurityConfig,添加JWT过滤器
将JWT过滤器添加到Spring Security的过滤器链中,放在UsernamePasswordAuthenticationFilter之前。
package com.clicker.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/register", "/api/auth/login").permitAll() .requestMatchers("/api/game/**").authenticated() .anyRequest().authenticated()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 注入JWT过滤器 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
3. 实现UserDetailsService接口
Spring Security需要通过该接口加载用户信息,确保认证流程正常运行。
package com.clicker.service; import com.clicker.entity.User; import com.clicker.repository.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; @Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); // 若用户有角色权限,可替换为实际权限列表 return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); } }
4. 优化控制器与服务类
移除手动获取Authorization头的代码,直接从Spring Security上下文获取认证用户信息,更符合框架规范。
修改后的控制器
package com.clicker.controller; import com.clicker.service.GameDataService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/game") public class GameDataController { @Autowired private GameDataService gameDataService; @PostMapping("/save") public ResponseEntity<?> saveGameData(@RequestBody String gameDataJson) { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return gameDataService.saveGameData(userDetails.getUsername(), gameDataJson); } @PostMapping("/load") public ResponseEntity<?> loadGameData() { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return gameDataService.loadGameData(userDetails.getUsername()); } }
修改后的服务类
package com.clicker.service; import com.clicker.entity.GameData; import com.clicker.entity.User; import com.clicker.repository.GameDataRepository; import com.clicker.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @Service public class GameDataService { @Autowired private GameDataRepository gameDataRepository; @Autowired private UserRepository userRepository; public ResponseEntity<?> saveGameData(String username, String gameDataJson) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在!")); GameData gameData = gameDataRepository.findByUser(user) .orElse(new GameData()); gameData.setUser(user); gameData.setData(gameDataJson); gameDataRepository.save(gameData); return ResponseEntity.ok("游戏数据保存成功!"); } public ResponseEntity<?> loadGameData(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("用户不存在!")); return gameDataRepository.findByUser(user) .map(gameData -> ResponseEntity.ok(gameData.getData())) .orElse(ResponseEntity.ok("{\n" + " \"playerStats\": { \"solarEnergy\": 0, \"multiplyCostData\": 200, \"solarEnergyPerClick\": 1, \"solarEnergyPerSecond\": 0},\n" + " \"planets\": {\n" + " \"mercury\": { \"unlocked\": false, \"cost\": 500 },\n" + " \"venus\": { \"unlocked\": false, \"cost\": 2500 },\n" + " \"earth\": { \"unlocked\": false, \"cost\": 12500 },\n" + " \"moon\": { \"unlocked\": false, \"cost\": 62500 }\n" + " }\n" + "}")); } }
验证
完成上述修改后,重新启动项目,携带有效JWT令牌请求/api/game/save端点,即可正常通过认证并处理请求。
内容的提问来源于stack exchange,提问作者Bojidar Kaloyanov
相关产品推荐
相关产品推荐

