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
相关产品推荐
相关产品推荐

