SpringSecurity6解决requestMatchers().permitAll()后依然执行自定义过滤器的问题

文章详细解释了SpringBoot如何处理Filter和SpringSecurity的集成,指出了当Filter被注册为Bean时可能会导致过滤器链执行多次的问题。解决方案是避免将自定义过滤器作为Bean,而是直接在Security配置类中实例化并添加到过滤器链。此外,文章还讨论了`permitAll()`在权限校验中的作用,指出它不影响认证过程,并引用了一条GitHubissue来说明这个问题。解决方案是通过使用`WebSecurityCustomizer`来忽略特定的请求匹配,从而避免登录接口误触发token校验过滤器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一个原因

Spring容器会自动识别被注册成为Bean对象的Filter过滤器(即继承自Filter对象的类),将这个Bean对象自动注册到SpringBoot的过滤器链中。

如果你的过滤器类使用注解注册成为了一个Bean对象,那么他就已经加到了SpringBoot的过滤器链中,所以就算你的SpringSecurity配置中设置了permitAll,可能还会去走SpringBoot的过滤器链。

第一个原因解决

不要将自己要加入到Security过滤器链的过滤器注册成为Bean对象,而是想办法传值给Security的配置类配置过滤器

例如我没有给自定义过滤器加注解注册成为Bean对象,但是在Security类中new自己的自定义过滤器,将他给Security配置过滤器

/**
 * @Author Yan
 * @Date 2023/06/04 8:37
 * @Version 1.0
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private UserMapper userMapper;

    public JwtAuthenticationFilter authenticationJwtTokenFilter() {
        return new JwtAuthenticationFilter(userMapper,userDetailsService);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // 提供自定义loadUserByUsername
        authProvider.setUserDetailsService(userDetailsService);
        // 指定密码编辑器
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用basic明文验证
                .httpBasic().disable()
                // 禁用默认登录页
                .formLogin().disable()
                // 禁用默认登出页
                .logout().disable()
                // 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
//                .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(invalidAuthenticationEntryPoint))
                // 前后端分离是无状态的,不需要session了,直接禁用。
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((authorizeRequests -> {
                    authorizeRequests
                            // 允许所有OPTIONS请求
                            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                            // 允许直接访问授权登录接口
                            .requestMatchers(HttpMethod.POST,"/coc/user/login").permitAll()
                            // 允许 SpringMVC 的默认错误地址匿名访问
                            .requestMatchers("/error").permitAll()
                            // 除上面外的所有请求全部需要鉴权认证
                            .anyRequest().authenticated();
                }))
                .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .authenticationProvider(authenticationProvider());
        return http.build();

    }
}

这样过滤器就不会多次执行了

第二个原因

第二个原因是我在SpringSecurity的github找到的一篇issue中看到的,原贴点此

有一个人是这样说的

原文如下,大意就是permitAll对权限校验的fillter没有影响,Security先认证后授权,permitAll是授权部分

permitAll() has no effect on authentication filters. Spring Security processes authentication first and then authorization, and permitAll() is an authorization matter.

Things essentially happen in this order:

  1. Write Secure Headers, like X-XSS-Protection
  2. Create an Authentication statement (that's what the authentication filters are for)
  3. Decide if that Authentication is enough to allow the request (permitAll() always says "yes")

Step 2 is where a JWT filter would go since that's how you'd figure out what privileges (scopes for example) that token has. That question has to be asked before an authorization decision (permitAll()) can be made.

Note that just because a request is public, that doesn't mean the response won't be rendered differently if there is a user in context. Thus, the separation of these two concerns is important.

It's already been stated that WebSecurity is how to completely ignore certain endpoints. Note, though, that this means that Spring Security won't secure that request in any way (no Step 1, 2, or 3).

 第二个原因解决

在这篇2017年的issue中,一条2019年的回复提供了一个方法解决这个问题,这个方法是当时的一个配置项,但是到Security6中这个配置项更改了,我去官方api文档中找相似的api终于找到关于这个配置的说明

Callback interface for customizing WebSecurity. Beans of this type will automatically be used by WebSecurityConfiguration to customize WebSecurity. 

用于自定义WebSecurity的回调接口。WebSecurityConfiguration将自动使用此类型的Bean来自定义WebSecurity。

示例用法

 @Bean
 public WebSecurityCustomizer ignoringCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/ignore1", "/ignore2");
 }

 我将它用到了我的项目中,以下是我完整的SecurityConfig配置

package com.greenjiao.coc.config;

import com.greenjiao.coc.filter.JwtAuthenticationFilter;
import com.greenjiao.coc.mapper.UserMapper;
import com.greenjiao.coc.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Author Yan
 * @Date 2023/06/04 8:37
 * @Version 1.0
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private UserMapper userMapper;

    public JwtAuthenticationFilter authenticationJwtTokenFilter() {
        return new JwtAuthenticationFilter(userMapper,userDetailsService);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        // 提供自定义loadUserByUsername
        authProvider.setUserDetailsService(userDetailsService);
        // 指定密码编辑器
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用basic明文验证
                .httpBasic().disable()
                // 禁用默认登录页
                .formLogin().disable()
                // 禁用默认登出页
                .logout().disable()
                // 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
//                .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(invalidAuthenticationEntryPoint))
                // 前后端分离是无状态的,不需要session了,直接禁用。
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((authorizeRequests -> {
                    authorizeRequests
                            // 允许所有OPTIONS请求
                            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                            // 允许直接访问授权登录接口
                            .requestMatchers(HttpMethod.POST,"/coc/user/login").permitAll()
                            // 允许 SpringMVC 的默认错误地址匿名访问
                            .requestMatchers("/error").permitAll()
                            // 除上面外的所有请求全部需要鉴权认证
                            .anyRequest().authenticated();
                }))
                .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .authenticationProvider(authenticationProvider());
        return http.build();

    }
    @Bean
    public WebSecurityCustomizer ignoringCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/coc/user/login");
    }
}

项目中添加了这个配置后,忽略的路径会执行认证流程,并且不会再执行后续的过滤器了,就解决了我登录接口依旧走token校验过滤器的问题。

Security6的配置也更改了,最初我是看的这位的博客参考新配置

Spring Security 6 配置方法,废弃 WebSecurityConfigurerAdapter_markvivv的博客-CSDN博客

### 实现Spring Security 6与JWT集成 #### 配置依赖项 为了使项目能够支持Spring Security 6以及JWT功能,需要引入必要的库。这通常涉及到在`pom.xml`文件中添加如下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <scope>runtime</scope> <version>${jwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <scope>runtime</scope> <version>${jwt.version}</version> </dependency> ``` 这些依赖提供了构建和解析JSON Web Tokens所需的功能[^3]。 #### 创建自定义过滤器 创建一个名为`JwtAuthenticationFilter`的类用于拦截请求并从中提取JWT令牌。该过滤器会读取HTTP头部中的`Authorization`字段,并尝试对其进行验证。如果验证通过,则设置相应的身份认证信息给当前线程的安全上下文中。 ```java public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = resolveToken(request); try { if (token != null && validateToken(token)) { Authentication auth = getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (Exception ex){ logger.error("Could not set user authentication in security context", ex); } filterChain.doFilter(request,response); } private String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } // Validate and parse the token... } ``` 此部分逻辑实现了从HTTP请求头中获取JWT令牌,并将其转换成有效的用户认证对象[^1]。 #### 设置安全配置 接下来,在应用程序的主要入口处定义一个新的配置类来指定哪些URL路径应该受到保护,以及如何处理未经认证或未授权访问的情况。这里可以利用Java Config的方式来进行声明式的安全管理。 ```java @EnableWebSecurity(debug=true) @Configuration @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationEntryPoint unauthorizedHandler; @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable).cors(CorsRegistry::configureCors); http.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)); http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); http.authorizeHttpRequests((authz) -> authz.requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated()); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();} @Bean public JwtAuthenticationFilter jwtAuthenticationFilter(){ return new JwtAuthenticationFilter(); } } ``` 上述代码片段展示了如何禁用CSRF防护(因为API通常是无状态的),允许跨域资源共享(CORS),并且指定了特定端点无需经过身份验证即可访问。同时设置了全局异常处理器以应对未经授权的访问尝试。最后一步是注册之前编写的JWT认证过滤器实例[^4]。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值