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

Spring Security FilterChain失效但@PreAuthorize可用问题求助

问题原因排查与修复

核心问题:自定义过滤器未加入Spring Security过滤器链

你遇到的问题根源在于自定义的JwtAuthenticationProcessingFilter和CustomJsonUsernamePasswordAuthenticationFilter没有被添加到Spring Security的过滤器链中,导致权限校验时机错误:

  1. 当在SecurityFilterChain中配置hasAnyRole("MANAGER")时,Spring Security的权限校验会在请求到达自定义JWT过滤器之前执行,此时SecurityContext中还没有用户的Authentication信息,因此权限校验失败,返回403。
  2. 改为permitAll()后,Spring Security放行所有请求,请求会先经过Spring MVC的过滤器链(此时自定义JWT过滤器执行,将Authentication存入SecurityContext),再进入Controller层,@PreAuthorize注解此时能获取到已设置的Authentication,因此权限校验正常。

其他细节验证

  • 你的JwtAuthenticationProcessingFilter是OncePerRequestFilter的实现,作为Spring Bean会被自动注册到Spring MVC的过滤器链中,但这个链的执行顺序在Spring Security过滤器链之后,所以无法为Spring Security的FilterChain权限校验提供Authentication。
  • 你在saveAuthentication方法中用.roles(myAdmin.getRole().name())构建UserDetails,Spring Security会自动为角色添加ROLE_前缀,所以hasAnyRole("MANAGER")的写法是正确的,和@PreAuthorize("hasRole('ROLE_MANAGER')")等价。

修复方案

在SecurityConfig的filterChain方法中,将自定义过滤器添加到Spring Security的过滤器链中,确保JWT校验在权限校验之前执行:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .csrf(cs -> cs.disable())
            .sessionManagement(s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .formLogin(f->f.disable())
            .httpBasic(h->h.disable())
            // 添加自定义登录过滤器,放在UsernamePasswordAuthenticationFilter之前
            .addFilterBefore(customJsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            // 添加JWT校验过滤器,放在登录过滤器之后、权限校验之前
            .addFilterAfter(jwtAuthenticationProcessingFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class);
    http
            .authorizeHttpRequests(auth->
                    auth.requestMatchers("/login", "/admins/**").permitAll()
                        .requestMatchers("/lectures/**", "/instructors/**").hasAnyRole("MANAGER")
                        .anyRequest().authenticated()
            );
    return http.build();
}

额外建议

  • 若仅使用FilterChain权限校验,可移除@EnableGlobalMethodSecurity;若保留,需确保两种校验方式的规则一致。
  • 可在过滤器和权限校验逻辑中添加日志,验证执行顺序是否符合预期。

附:相关代码(原代码整理)

SecurityConfig.java

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    private final LoginService loginService;
    private final JwtService jwtService;
    private final AdminRepository adminRepository;
    private final ObjectMapper objectMapper;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(cs -> cs.disable())
                .sessionManagement(s->s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .formLogin(f->f.disable())
                .httpBasic(h->h.disable());
        http
                .authorizeHttpRequests(auth->{
//                    auth.requestMatchers("/lectures/**").hasAnyRole("MANAGER")
//                    auth.requestMatchers("/instructors/**").hasAnyRole("MANAGER")
//                    auth.requestMatchers("/instructors/**").authenticated()
//                                    .requestMatchers("/lectures/**").permitAll()
////                        .requestMatchers("/instructors/**").hasAnyRole("MANAGER")
//                            .requestMatchers("/admins/**").permitAll()
//                            .requestMatchers("/login").permitAll()
//auth.requestMatchers("/instructors/**").hasRole("MANAGER")
                    auth.anyRequest().permitAll()

                    ;
                });
        return http.build();
    }
    @Bean
    public AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(loginService);
        return new ProviderManager(provider);
    }


    @Bean
    public LoginSuccessHandler loginSuccessHandler() {
        return new LoginSuccessHandler(jwtService, adminRepository);
    }

    @Bean
    public LoginFailureHandler loginFailureHandler() {
        return new LoginFailureHandler();
    }


    @Bean
    public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter() {
        CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordLoginFilter
                = new CustomJsonUsernamePasswordAuthenticationFilter(objectMapper);
        customJsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
        customJsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
        customJsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
        return customJsonUsernamePasswordLoginFilter;
    }

    @Bean
    public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
        JwtAuthenticationProcessingFilter jwtAuthenticationFilter = new JwtAuthenticationProcessingFilter(jwtService, adminRepository);
        return jwtAuthenticationFilter;
    }
}

JwtAuthenticationProcessingFilter.java

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {

    private static final String NO_CHECK_URL = "/login";

    private final JwtService jwtService;
    private final AdminRepository adminRepository;

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getRequestURI().equals(NO_CHECK_URL)) {
            filterChain.doFilter(request, response);
            return;
        }

        String refreshToken = jwtService.extractRefreshToken(request)
                .filter(jwtService::isTokenValid)
                .orElse(null);

        if (refreshToken != null) {
            checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
            return;
        }

        if (refreshToken == null) {
            checkAccessTokenAndAuthentication(request, response, filterChain);
        }
    }

    public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
        adminRepository.findByRefreshToken(refreshToken)
                .ifPresent(admin -> {
                    String reIssuedRefreshToken = reIssueRefreshToken(admin);
                    jwtService.sendAccessAndRefreshToken(response, jwtService.createAccessToken(admin.getEmail()),
                            reIssuedRefreshToken);
                });
    }

    private String reIssueRefreshToken(Admin admin) {
        String reIssuedRefreshToken = jwtService.createRefreshToken();
        admin.updateRefreshToken(reIssuedRefreshToken);
        adminRepository.saveAndFlush(admin);
        return reIssuedRefreshToken;
    }

    public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
                                                  FilterChain filterChain) throws ServletException, IOException {
        log.info("checkAccessTokenAndAuthentication() 호출");
        jwtService.extractAccessToken(request)
                .filter(jwtService::isTokenValid)
                .ifPresent(accessToken -> jwtService.extractEmail(accessToken)
                        .ifPresent(email -> adminRepository.findByEmail(email)
                                .ifPresent(this::saveAuthentication)));
        log.info("saveAuthentication() 호출");
        filterChain.doFilter(request, response);
    }

    public void saveAuthentication(Admin myAdmin) {
        String password = myAdmin.getPassword();

        UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
                .username(myAdmin.getEmail())
                .password(password)
                .roles(myAdmin.getRole().name())
                .build();

        Authentication authentication =
                new UsernamePasswordAuthenticationToken(userDetailsUser, null,
                        authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

InstructorController.java

@RequiredArgsConstructor
@RequestMapping("/instructors")
@RestController
public class InstructorController {

    private final InstructorService instructorService;

    @PostMapping
    @PreAuthorize("hasRole('ROLE_MANAGER')")//it works
    public ResponseEntity createInstructor(@Valid @RequestBody InstructorPostDto instructorPostDto) {
        InstructorResponseDto createdInstructor = instructorService.createInstructor(instructorPostDto);
        return new ResponseEntity<>(createdInstructor, HttpStatus.CREATED);
    }
}

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.7'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.hh99'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // security 관련 의존성
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // jwt 관련 의존성
    implementation 'com.auth0:java-jwt:4.2.1'
}

tasks.named('bootBuildImage') {
    builder = 'paketobuildpacks/builder-jammy-base:latest'
}

tasks.named('test') {
    useJUnitPlatform()
}

内容的提问来源于stack exchange,提问作者Simba

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 10:09:51