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

Spring Security中同时实现Form Login与JWT认证的技术咨询

Spring Boot同时实现表单登录与JWT API认证解决方案

问题背景

我开发了一个采用Thymeleaf作为前端、Spring Security表单登录做身份认证的Spring Boot应用,现在需要为特定API接口添加JWT认证来提升安全性。目前遇到的情况是:

  • 注释掉APISecurity内部类时,表单登录功能正常可用
  • 注释掉WebSecurity内部类时,Postman可以正常调用API完成登录

需要实现两种认证方式共存的配置方案。

当前配置代码

package com.example.exam.security;

import com.example.exam.repository.UserRepository;
import com.example.exam.security.jwt.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
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;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found."));
    }

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

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

    @Bean
    public static JwtRequestFilter authenticationJwtTokenFilter() {
        return new JwtRequestFilter();
    }

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

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source =
                new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }


    @Configuration
    @Order(1)
    public static class APISecurity {
        @Bean
        public SecurityFilterChain apiSecurityConfigure(final HttpSecurity http) throws Exception {
            return http.cors(withDefaults())
                    .csrf(AbstractHttpConfigurer::disable)
                    .authorizeHttpRequests((authorize) -> authorize
                            .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register", "api/v1/auth/test").permitAll()
                            .anyRequest().authenticated()
                    )
                    .httpBasic(withDefaults())
                    .sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                    .build();
        }
    }

    @Configuration
    public static class WebSecurity {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http.csrf(AbstractHttpConfigurer::disable)
                    .authorizeHttpRequests((requests) -> requests
                            .requestMatchers("/registration/**").permitAll()
                            .requestMatchers("/login/**").permitAll()
                            .requestMatchers("/admin/**").hasAnyRole("ADMIN")
                            .anyRequest().authenticated()
                    )
                    .formLogin((form) -> form
                            .loginPage("/login")
                            .loginProcessingUrl("/login")
                            .defaultSuccessUrl("/secure")
                            .permitAll()
                    )
                    .logout((logout) -> logout
                            .logoutUrl("/logout")
                            .logoutSuccessUrl("/")
                            .permitAll()
                    );
            return http.build();
        }
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/images/**", "/js/**", "/webjars/**");
    }
}

问题分析

当前配置的核心问题是两个SecurityFilterChain没有明确的请求范围划分:

  • 优先级更高(@Order(1))的APISecurity配置会拦截所有请求,它采用无状态(STATELESS)会话策略,覆盖了表单登录需要的有状态会话机制,导致表单登录无法正常工作。
  • WebSecurity配置因为优先级低,请求根本不会走到这里。

解决方案

给两个SecurityFilterChain分别指定匹配的请求路径,让API请求走JWT认证链,非API请求走表单登录链,具体修改如下:

1. 修改APISecurity配置

添加securityMatcher("/api/**"),明确该配置只处理API路径的请求:

@Configuration
@Order(1)
public static class APISecurity {
    @Bean
    public SecurityFilterChain apiSecurityConfigure(final HttpSecurity http) throws Exception {
        return http
                .securityMatcher("/api/**") // 新增:只匹配API路径
                .cors(withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register", "/api/v1/auth/test").permitAll()
                        .anyRequest().authenticated()
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

注:可以移除httpBasic(withDefaults()),因为API已经用JWT认证,不需要HTTP Basic认证。

2. 调整WebSecurity配置

保持原有逻辑,它会自动处理非API的请求(因为APISecurity已经拦截了/api/**路径):

@Configuration
public static class WebSecurity {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((requests) -> requests
                        .requestMatchers("/registration/**", "/login/**").permitAll()
                        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/login")
                        .defaultSuccessUrl("/secure")
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/")
                        .permitAll()
                );
        return http.build();
    }
}

验证方法

  • 表单登录验证:访问前端页面(如/login),输入账号密码后可正常跳转到/secure,会话保持正常。
  • API JWT验证:用Postman调用/api/v1/auth/login获取JWT令牌,然后在后续API请求的Header中携带Authorization: Bearer {token},即可正常访问需要认证的API接口。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 02:24:54