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

如何拒绝访问未添加@PreAuthorize注解的Spring控制器?

问题背景

环境信息

  • Java 17
  • Spring Boot 3.0.4
  • Spring Security 6.0.2
  • Jetty 11 服务器
  • 启动日志:Running with Spring Boot v3.0.4, Spring v6.0.6

配置信息

application.properties 配置:

spring.security.filter.dispatcher-types=request,async,error,forward,include

安全配置类代码:

@Configuration
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
@EnableWebSecurity
public class SecConfiguration {

    private final HouseUserDetailsService customUserDetailsService;
    private final HouseShiftConfiguration dashShiftConfig;

    private final DBI dbi;
    private final AuditLogService auditLogService;

    @Autowired
    private CustomSecurityconfiguration customSecurityConfiguration;

    @Autowired
    public SecConfiguration(HouseUserDetailsService customUserDetailsService, DBI dbi, HouseShiftConfiguration dashShiftConfig, AuditLogService auditLogService) {
        this.customUserDetailsService = customUserDetailsService;
        this.dbi = dbi;
        this.dashShiftConfig = dashShiftConfig;
        this.auditLogService = auditLogService;
    }

    @Bean
    public HouseRedirectAuthenticationSuccessHandler houseRedirectAuthenticationSuccessHandler() {
        return new HouseRedirectAuthenticationSuccessHandler(dbi);
    }
  
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception {
       return authConfiguration.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
        requestCache.setMatchingRequestParameterName("continue");

        CsrfTokenRequestAttributeHandler csrfRequestHandler = new CsrfTokenRequestAttributeHandler();
        csrfRequestHandler.setCsrfRequestAttributeName("_csrf");// set the name of the attribute the CsrfToken will be populated on

        XorCsrfTokenRequestAttributeHandler xorCsrfRequestHandler = new XorCsrfTokenRequestAttributeHandler();
        xorCsrfRequestHandler.setCsrfRequestAttributeName("_csrf");// set the name of the attribute the CsrfToken will be populated on
        
        //csrf white list, when you don't want to use csrf, put your url here.
        String[] csrfWhiteList = {
                "/emailSender/**"
        };

        http
            .csrf((csrf) -> csrf
                .csrfTokenRequestHandler(csrfRequestHandler)
            )
            .csrf((xorCsrf) -> xorCsrf
                .csrfTokenRequestHandler(xorCsrfRequestHandler)
            )
            .csrf()
                .ignoringRequestMatchers(csrfWhiteList) 
                .and() 
            .rememberMe()
                .rememberMeServices(dashShiftConfig.tokenBasedRememberMeServicesCore())
                .and()
            .authorizeHttpRequests(auth -> auth
                .dispatcherTypeMatchers(DispatcherType.FORWARD/*, DispatcherType.INCLUDE, DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST*/).permitAll()
                .requestMatchers("/accessDenied", "/security_check", "/img/*", "/js/*", "/css/*", "/?*", "/", "/login*").permitAll()
                .anyRequest().denyAll())           
            .formLogin()
                .loginPage("/")
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/security_check")
                .failureUrl("/?r=fail")
                .failureHandler(HouseAuthFailureHandler())
                .successHandler(HouseRedirectAuthenticationSuccessHandler())
                .and()    
            .logout()
                .logoutUrl("/logout")
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessHandler(customLogoutHandler())
                .deleteCookies("JSESSIONID","rm")
                .invalidateHttpSession(true)
                .and()
            .sessionManagement()
                .invalidSessionUrl("/?r=invalidSessionUrlLogin")
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler());

        http
            .headers()
                .frameOptions().disable()
                .addHeaderWriter(new StaticHeadersWriter("X-FRAME-OPTIONS", "ALLOW-FROM https://www.secure-service.com"));

        http
            .headers()
                .xssProtection(xssProtection -> xssProtection.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
                .contentSecurityPolicy("form-action 'self'");

        http
            .sessionManagement((sessions) -> sessions
                .requireExplicitAuthenticationStrategy(false)
            )
            .sessionManagement()
                .maximumSessions(1)
                .sessionRegistry(sessionRegistry())
                .expiredUrl("/?r=sessionExpiredDuplicateLogin");
      
        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
         
        authProvider.setUserDetailsService(this.customUserDetailsService);
        authProvider.setPasswordEncoder(customSecurityConfiguration.passwordEncoder());
     
        return authProvider;
    }
}

问题描述

按照Spring Security迁移指南建议(原指南提到:建议Spring Security保护所有调度类型,6.0版本已修改默认行为,需调整授权规则覆盖所有调度类型),修改授权规则为:

.authorizeHttpRequests(auth -> auth
    .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
    .requestMatchers("/accessDenied", "/security_check", "/img/*", "/js/*", "/css/*", "/?*", "/", "/login*").permitAll()
    .anyRequest().denyAll())

之后无法访问带有@PreAuthorize注解的控制器,提示Access Denied。

需求目标

  1. 仅允许访问添加了@PreAuthorize约束的Spring控制器;
  2. 拒绝访问未添加@PreAuthorize约束的控制器;
  3. 允许外部服务(如Plesk)调用特定控制器(如CRON任务相关接口);
  4. 理解request、async、error、forward、include各DispatcherType对Web应用的控制作用。

解决方案及说明

一、解决@PreAuthorize接口访问被拒绝的问题

当前授权规则里的.anyRequest().denyAll()会直接拦截所有未被前面规则放行的请求,包括带有@PreAuthorize注解的接口——因为方法级注解的权限校验是在过滤器链之后执行的,过滤器链先把请求拦了,方法级校验根本没机会生效。

调整授权规则,把.anyRequest().denyAll()改成.anyRequest().authenticated(),同时覆盖所有DispatcherType:

.authorizeHttpRequests(auth -> auth
    // 放行内部转发请求,避免跳转被拦截
    .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
    // 放行静态资源、登录相关接口
    .requestMatchers("/accessDenied", "/security_check", "/img/*", "/js/*", "/css/*", "/?*", "/", "/login*").permitAll()
    // 放行外部调用的CRON任务接口
    .requestMatchers("/cron/**").permitAll()
    // 所有其他请求先要求认证,之后交给@PreAuthorize做细粒度校验
    .anyRequest().authenticated())

这样设置后,过滤器链只会拦截未认证的请求,已认证的请求会进入方法层面,由@PreAuthorize判断是否有权限访问。

二、实现仅允许带@PreAuthorize的控制器被访问

要拒绝未添加@PreAuthorize的控制器,可通过AOP切面实现:

@Aspect
@Component
public class UnauthorizedControllerInterceptor {

    @Pointcut("execution(* com.yourpackage.controller..*(..))")
    public void controllerMethods() {}

    @Before("controllerMethods()")
    public void checkPreAuthorize(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        if (!method.isAnnotationPresent(PreAuthorize.class)) {
            throw new AccessDeniedException("此接口未配置权限规则,禁止访问");
        }
    }
}

所有控制器方法如果没有@PreAuthorize注解,会直接抛出拒绝访问异常。

三、允许外部调用特定控制器

在授权规则里通过requestMatchers直接放行对应接口,比如CRON任务接口路径是/cron/**,就添加:

.requestMatchers("/cron/**").permitAll()

如果需要更严格的控制,可限制请求来源IP:

.requestMatchers("/cron/**").hasIpAddress("192.168.1.100")

四、各DispatcherType的作用说明

  • REQUEST:标准HTTP请求,比如浏览器直接发起的GET/POST请求,是最常见的调度类型。
  • ASYNC:异步请求,比如使用Callable、DeferredResult处理的异步响应请求。
  • ERROR:应用抛出异常后,被ErrorPageFilter转发到错误处理控制器的请求。
  • FORWARD:通过RequestDispatcher.forward()发起的内部转发请求,比如控制器之间的跳转。
  • INCLUDE:通过RequestDispatcher.include()发起的包含请求,比如在页面中嵌入其他资源的请求。

Spring Security 6.0默认会对所有这些调度类型的请求应用安全规则,所以需要明确配置哪些调度类型需要放行或拦截。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 20:25:00