You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 20:57:01