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

Spring Boot双JWT认证过滤器配置引发403访问禁止问题

Spring Boot双过滤链JWT认证403问题排查方向

我正在开发一个Spring Boot应用,需要两条安全过滤链和两个JWT认证过滤器。完成现有实现后,使用Postman携带已认证的Bearer Token对/api/affiliates发起GET请求时,返回403禁止访问状态,而非预期的OK状态,以下是相关代码及排查方向:

实体类:Affiliate.java

@Entity
@Data
public class Affiliate implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String firstName;
    private String lastName;
    private String sex;

    @Column(unique = true)
    private String phoneNumber;

    @Column(unique = true)
    private String email;

    private String password;

    @Column(name = "balance_in_naira")
    private int balance;
    private String referralCode;

    @Enumerated(value = EnumType.STRING)
    private Role role = Role.AFFILIATE;

    @OneToMany
    private List<Student> students;

    private boolean enabled;

    public void increaseBalance(int referralBonus) {
        balance += referralBonus;
    }

    public void setPassword(String password) {
        this.password = new BCryptPasswordEncoder().encode(password);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + getRole().name()));
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

安全配置:SecurityConfig.java

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfiguration {
    private final StudentJwtAuthorizationFilter studentJwtAuthorizationFilter;
    private final StudentDetailsService studentDetailsService;
    private final AffiliateDetailsService affiliateDetailsService;
    private final AffiliateJwtAuthorizationFilter affiliateJwtAuthorizationFilter;

    @Bean
    @Order(1)
    public SecurityFilterChain affiliateFilterChain(HttpSecurity http) throws Exception {
        return http
                   .securityMatcher("/api/affiliates", "/api/affiliates/**")
                   .cors().disable()
                   .csrf().disable()
                   .formLogin().disable()
                   .httpBasic().disable()
                   .authorizeHttpRequests()
                       .requestMatchers(HttpMethod.POST, "/api/affiliates").permitAll()
                       .requestMatchers("/api/affiliates/confirm", "/api/affiliates/auth", "/api/affiliates/forgot-password", "/api/affiliates/forgot-password/**").permitAll()
                       .requestMatchers("/api/affiliates", "/api/affiliates/referrals").hasRole(Role.AFFILIATE.name())
                       .anyRequest().authenticated()
                       .and()
                   .authenticationProvider(affiliateAuthenticationProvider())
                   .addFilterBefore(affiliateJwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
                   .build();
    }

    @Bean
    public SecurityFilterChain studentFilterChain(HttpSecurity http) throws Exception {
        return http
                   .securityMatcher("/api/students", "/api/students/**")
                   .cors().disable()
                   .csrf().disable()
                   .httpBasic().disable()
                   .formLogin().disable()
                   .authorizeHttpRequests()
                       .requestMatchers(HttpMethod.POST, "/api/students").permitAll()
                       .requestMatchers("/api/students/auth", "/api/students/forgot-password", "/api/students/forgot-password/**").permitAll()
                        .requestMatchers("/api/students/confirm").permitAll()
                       .requestMatchers("/api/students/complete-registration").hasRole(Role.STUDENT.name())
                       .requestMatchers(HttpMethod.GET, "/api/students").hasRole(Role.STUDENT.name())
                       .requestMatchers(HttpMethod.PUT, "/api/students").hasRole(Role.STUDENT.name())
                       .requestMatchers(HttpMethod.PATCH, "/api/students/new-password").hasRole(Role.STUDENT.name())
                       .requestMatchers(HttpMethod.DELETE, "/api/students").hasRole(Role.STUDENT.name())
                       .anyRequest().authenticated()
                       .and()
                   .authenticationProvider(studentAuthenticationProvider())
                   .addFilterBefore(studentJwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
                   .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider studentAuthenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(studentDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationProvider affiliateAuthenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(affiliateDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(List<AuthenticationProvider> authenticationProviders) throws Exception {
        return new ProviderManager(authenticationProviders);
    }
}

AffiliateJwtAuthorizationFilter.java

@Component
@AllArgsConstructor
public class AffiliateJwtAuthorizationFilter extends OncePerRequestFilter {
    private final AffiliateDetailsService detailsService;
    private final JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authValue = request.getHeader("Authorization");
        if (authValue != null && authValue.startsWith("Bearer ")) {
            String token = authValue.substring(7);
            Affiliate affiliate = detailsService.loadUserByUsername(jwtService.extractUsername(token));
            if (!jwtService.isExpired(token)) {
                UsernamePasswordAuthenticationToken auth =  new UsernamePasswordAuthenticationToken(affiliate, null, affiliate.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);

                response.setHeader("Authorization", request.getHeader("Authorization"));
            }
        }

        filterChain.doFilter(request, response);
    }
}

StudentJwtAuthorizationFilter.java

@Component //make this a bean
@AllArgsConstructor
public class StudentJwtAuthorizationFilter extends OncePerRequestFilter {
    private final StudentDetailsService detailsService;
    private final JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authValue = request.getHeader("Authorization");
        if (authValue != null && authValue.startsWith("Bearer ")) {
            String token = authValue.substring(7);
            Student student = detailsService.loadUserByUsername(jwtService.extractUsername(token));
            if (!jwtService.isExpired(token)) {
                UsernamePasswordAuthenticationToken auth =  new UsernamePasswordAuthenticationToken(student, null, student.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);

               response.setHeader("Authorization", request.getHeader("Authorization")); //Check whether this is
            }
        }

        filterChain.doFilter(request, response);
    }
}

控制器:AffiliateController.java

@RestController
@RequestMapping(path = "/api/affiliates")
@AllArgsConstructor
public class AffiliateController {
    private final AffiliateService affiliateService;

    @GetMapping
    public AffiliateDto get() {
        return affiliateService.get();
    }
}

排查方向:

  • JWT Token有效性验证
    • 确认jwtService.isExpired(token)逻辑是否正确,是否错误判断token过期时间
    • 检查jwtService.extractUsername(token)提取的用户名是否与数据库中Affiliate的email字段完全一致
  • 权限配置校验
    • 虽然hasRole(Role.AFFILIATE.name())会自动拼接ROLE_前缀,与你返回的ROLE_AFFILIATE权限匹配,但仍需确认Role枚举的AFFILIATE值是否正确
    • 验证securityMatcher("/api/affiliates", "/api/affiliates/**")是否覆盖了GET /api/affiliates请求
  • 过滤器逻辑检查
    • 添加日志打印,确认是否进入了设置Authentication的代码块;如果token无效(比如过期),是否导致SecurityContext未被正确设置
    • 检查SecurityContextHolder.getContext().setAuthentication(auth)是否生效,后续请求中Authentication是否被意外清空
    • 尝试注释掉response.setHeader("Authorization", request.getHeader("Authorization")),确认这行代码是否干扰了请求头传递
  • 用户状态确认
    • 检查数据库中对应Affiliate的enabled字段是否为true,因为UserDetails的isEnabled()会返回该值,状态为false会导致认证失败
  • UserDetailsService与认证提供者检查
    • 确认AffiliateDetailsService.loadUserByUsername()是否正确返回对应的Affiliate对象,不存在查询空指针或返回null的情况
    • 验证affiliateAuthenticationProvider是否正确关联了affiliateDetailsService
  • 日志追踪
    • 开启Spring Security的DEBUG日志:
      logging.level.org.springframework.security=DEBUG
      
      查看认证流程日志,确认是否有认证成功记录,以及授权阶段的权限检查细节

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 22:03:06