代码之家  ›  专栏  ›  技术社区  ›  Bojidar Kaloyanov

为什么即使有有效的JWT,Spring Boot也会对安全端点的POST请求返回403 Forbidden?

  •  0
  • Bojidar Kaloyanov  · 技术社区  · 4 月前

    我正在进行一个Spring Boot项目,在那里我实现了基于JWT的身份验证。我有一个/api/game/save端点,它应该接受带有有效JWT的POST请求。尽管在Authorization标头中包含了令牌,但我仍然收到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.JwtBuilder;
    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"; // Not my real secret key ofc
        private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 10 hours
    
    
        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
    

    当我使用有效的令牌和正文向/api/game/save发送POST请求时,我得到了403 Forbidden响应。

    1 回复  |  直到 4 月前
        1
  •  0
  •   Rob Sil    4 月前

    假设您在问题中提供的代码缺少Spring Security Filter,它将对请求进行身份验证。 您的安全过滤器链可能如下:

    @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, MyCustomJwtAuthenticationFilter jwtFilter) throws Exception {
            http.csrf(csrf -> csrf.disable())
                    .authorizeHttpRequests(auth -> auth
                            .requestMatchers("/api/auth/register", "/api/auth/login").permitAll()
                            .requestMatchers("/api/game/**").authenticated()
                            .anyRequest().authenticated()
                            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class));
    
            http.sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    
            return http.build();
        }
    

    强调你的注意力 .addFilterBefore ,在这里,您将使用您的过滤器来验证您的请求。

    实现JWT的示例: https://medium.com/@tericcabrel/implement-jwt-authentication-in-a-spring-boot-3-application-5839e4fd8fac

    了解更多关于Spring Filters的资源: https://docs.spring.io/spring-security/reference/servlet/architecture.html