如何拒绝访问未添加@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。
需求目标
- 仅允许访问添加了
@PreAuthorize约束的Spring控制器; - 拒绝访问未添加
@PreAuthorize约束的控制器; - 允许外部服务(如Plesk)调用特定控制器(如CRON任务相关接口);
- 理解
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
相关产品推荐
相关产品推荐

