Spring Security 6公开接口调用返回403 Forbidden问题求助
问题描述
我在使用Spring Security 6时遇到权限问题:已在SecurityConfig类中将/api/auth/signup端点配置为permitAll(无需认证),但发起POST请求http://localhost:8087/api/auth/signup/时,仍收到403 Forbidden响应。以下是我的相关代码:
SecurityConfig代码
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class SecurityConfig { private final UserDetailsServiceImpl userDetailsService; private final JwtAuthFilter jwtAuthFilter; public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthFilter jwtAuthFilter) { this.userDetailsService = userDetailsService; this.jwtAuthFilter = jwtAuthFilter; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain( HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { return http.cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Set permissions on endpoints .authorizeHttpRequests( auth -> auth // public endpoints .requestMatchers(HttpMethod.POST, "/api/auth/signup/**") .permitAll() .requestMatchers(HttpMethod.POST, "/api/auth/login/**") .permitAll() .requestMatchers(HttpMethod.GET, "/authentication-docs/**") .permitAll() // private endpoints .anyRequest() .authenticated()) .authenticationManager(authenticationManager) .formLogin(form -> form.loginPage("/login")) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); return authenticationManagerBuilder.build(); } }
UserDetailsServiceImpl代码
@Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository repository; public UserDetailsServiceImpl(UserRepository repository) { this.repository = repository; } @Override public UserDetails loadUserByUsername(String email) { UserEntity userEntity = repository .findByUsername(email) .orElseThrow( () -> new NotFoundException(String.format("User does not exist, email: %s", email))); List<SimpleGrantedAuthority> grantedRoles = new ArrayList<>(); grantedRoles.add(new SimpleGrantedAuthority(userEntity.getRole().name())); return org.springframework.security.core.userdetails.User.builder() .username(userEntity.getUsername()) .password(userEntity.getPassword()) .authorities(grantedRoles) .build(); } }
AuthController代码
@RestController @RequestMapping(path = "/api/auth", produces = MediaType.APPLICATION_JSON_VALUE) public class AuthController { private final AuthenticationManager authenticationManager; private final UserService userService; public AuthController(AuthenticationManager authenticationManager, UserService userService) { this.authenticationManager = authenticationManager; this.userService = userService; } @PostMapping("/signup") public UserDto signup(@Valid @RequestBody UserRequest requestDto) { return userService.signup(requestDto); } @PostMapping("/login") public ResponseEntity<UserResponse> login(@Valid @RequestBody UserRequest request) { try { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); } catch (BadCredentialsException e) { throw e; } String token = JwtHelper.generateToken(request.getUsername()); return ResponseEntity.ok(new UserResponse(request.getUsername(), token)); } }
问题分析与修复方案
1. 请求路径匹配不一致
你的请求URL是/api/auth/signup/(带末尾斜杠),但Controller映射的是/signup(无斜杠),Spring Security默认路径匹配规则会将两者视为不同路径,导致permitAll配置未生效。
修复:
修改SecurityConfig中的路径配置,同时兼容带斜杠和不带斜杠的请求:
.requestMatchers(HttpMethod.POST, "/api/auth/signup", "/api/auth/signup/**") .permitAll()
2. 重复启用方法安全注解
Spring Security 6中@EnableGlobalMethodSecurity已被弃用,你同时启用@EnableMethodSecurity和@EnableGlobalMethodSecurity会导致配置冲突,干扰权限判断。
修复:
删除@EnableGlobalMethodSecurity注解,保留@EnableMethodSecurity(prePostEnabled = true)即可:
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { // ... 其他代码 }
3. JwtAuthFilter拦截了公共端点
你将JwtAuthFilter添加在UsernamePasswordAuthenticationFilter之前,如果Filter内部没有跳过公共端点的逻辑,会对permitAll的请求执行认证检查,导致403。
修复:
在JwtAuthFilter的doFilterInternal方法中添加跳过逻辑:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestUri = request.getRequestURI(); // 跳过无需认证的端点 if (requestUri.startsWith("/api/auth/signup") || requestUri.startsWith("/api/auth/login")) { filterChain.doFilter(request, response); return; } // 原有JWT认证逻辑 // ... }
4. 冗余的表单登录配置
你配置了.formLogin(form -> form.loginPage("/login")),但这是针对传统表单登录的配置,与无状态JWT认证冲突,会干扰权限判定逻辑。
修复:
移除表单登录配置:
// 删除此行代码 .formLogin(form -> form.loginPage("/login"))
最终修改后的SecurityConfig
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { private final UserDetailsServiceImpl userDetailsService; private final JwtAuthFilter jwtAuthFilter; public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthFilter jwtAuthFilter) { this.userDetailsService = userDetailsService; this.jwtAuthFilter = jwtAuthFilter; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain( HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { return http.cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests( auth -> auth .requestMatchers(HttpMethod.POST, "/api/auth/signup/**", "/api/auth/login/**") .permitAll() .requestMatchers(HttpMethod.GET, "/authentication-docs/**") .permitAll() .anyRequest() .authenticated()) .authenticationManager(authenticationManager) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); return authenticationManagerBuilder.build(); } }
内容的提问来源于stack exchange,提问作者obela06

