
Spring Security 新版本配置这几年让不少从 Spring Boot 2 时代过来的开发者在升级时栽了跟头。以前老项目里最常见的写法就是让配置类继承WebSecurityConfigurerAdapter然后重写configure(HttpSecurity http)里面用antMatchers(...).permitAll()一把梭从 Spring Security 5.7 开始这种写法就不断告警到 Spring Security 6 直接移除。你现在打开新项目会发现整个配置思路已经换成了组件化写法不再有 Adapter不再有.and()链式拼接而是通过HttpSecurity上的方法组合出一个SecurityFilterChainBean。这篇内容就是针对 Spring Security 新版本配置的一线实操总结包含我升级过程中踩过的坑、调整过的方案以及现在最常用的配置骨架。适合正准备从旧版迁移或者刚接触 Spring Boot 3 Spring Security 6 的朋友参考读完拿去做项目改造基本够用。1. 先盘清楚新版本配置到底改了什么1.1 从 WebSecurityConfigurerAdapter 到组件化配置先说一个很多人没想明白的问题为什么 Spring Security 一定要把WebSecurityConfigurerAdapter干掉旧版里一次只允许一个配置适配器生效。项目稍大一点你想对不同的 URL 目录应用不同规则只能靠多个WebSecurityConfigurerAdapter的Order去控制。用起来很绕而且扩展点都藏在重写方法里新手看半天也不知道哪个方法被哪个框架回调了。新版本的做法是把“适配器”这个概念彻底去掉改成直接暴露SecurityFilterChain和过滤器链注册机制。本质上你现在要做的事情和以前是一样的核心还是构建一条过滤器链。只是注册方式从“继承 重写”变成了“声明 Bean 方法调用”。下面是一份最精简的新版核心配置你对比一下旧写法就能看出差别Configuration EnableWebSecurity public class SecurityConfig { Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/public/**).permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } }注意三点配置类不需要继承任何类Configuration加上一个返回SecurityFilterChain的Bean方法就行。authorizeHttpRequests替代了旧的authorizeRequests里面用的是requestMatchers旧版antMatchers和mvcMatchers已经淡出。方法内部直接用 lambda 配置不需要.and()来回切换上下文。1.2 为什么.and()没了以及 Lambda DSL 的好处很多旧代码里能看到一大串.and().and().and()结构例如http.authorizeRequests() .antMatchers(/public/**).permitAll() .and() .formLogin() .loginPage(/login) .and() .logout() .logoutUrl(/logout);这种写法的问题在于.and()只是把对象切回HttpSecurity一旦中间某一步传错了配置器编译器基本帮不上忙。新版本的 Lambda DSL 会让每个配置模块的上下文保持清晰配置formLogin、csrf、sessionManagement时各自独立成块代码可读性高了一个量级IDE 自动补全也更友好。不过这里要提醒一句网上不少老教程虽然标题是“新版本”代码却还停留在.and()时代甚至把authorizeRequests和authorizeHttpRequests混在一起写。这种代码在新版里会直接编译失败提示找不到antMatchers()。我看到太多帖子把这种情况归结为“Spring Security 太难了”其实只是 API 换了个位置。1.3 默认策略收紧CSRF、CORS、Session 策略的变化新版本除了 API 换了写法还有一个容易忽略的点默认策略比旧版严格了很多。CSRF跨站请求伪造防护默认开启。如果你的服务是前后端分离的无状态接口且不使用浏览器 Cookie 做身份凭证就需要显式关闭 CSRF否则所有 POST、PUT、DELETE 请求都会被拦截。默认登录页/login依然自带但如果你希望做完全自定义的 JSON 登录需要把默认的formLogin关掉并添加自己的认证过滤器。在 Spring Security 6 里对无状态场景的推荐方式是配置SessionCreationPolicy.STATELESS避免框架默认创建 Session。这里的核心认知是新版本希望开发者在写每一行配置前主动想清楚自己的应用形态是“传统服务端渲染页面”还是“前后端分离接口服务”。如果是前者很多默认行为可以直接用如果是后者你要显式关掉 CSRF、配置 CORS并且不要让框架维护 Session。我在项目里给团队定了个很简单的小口诀csrf要看凭证存放位置session要看服务端要不要维护状态cors要看浏览器接口是否跨域。这三个前提搞不清楚配置文档抄得再多也会埋坑。2. 搭建新版核心配置一个可以落地的 SecurityFilterChain2.1 最简配置怎么写才安全网上能找到的新版“最简配置”基本上都是这种风格http.csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .anyRequest().permitAll());这段代码的问题是它把认证和授权全关了算什么安全配置适合新建一个临时测试工程第一次确认 Spring Boot 能跑起来但如果你直接把它当成项目骨架那还不如不加 Security 依赖。我建议的最小可用配置是这样的保留密码加密校验能力默认所有请求都必须经过认证只放开健康检查和登录入口然后根据实际需求逐步加规则。这样配置上线后即使忘了某个细节没有一个大口子直接暴露在外面。Configuration EnableWebSecurity public class SecurityConfig { Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/health, /error, /api/auth/login).permitAll() .anyRequest().authenticated() ) .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(Customizer.withDefaults()); return http.build(); } }把一个请求“先全拦住再按需放行”作为底线比一开始就permitAll()一大片安全得多。后面要接 Swagger 文档、静态资源或者前端页面再单独把对应路径加进permitAll列表里风险就可控了。2.2 内存用户与密码解析器组合没有接数据库之前最快的用户配置方式是InMemoryUserDetailsManager。但这里有几个细节必须注意。首先是密码绝对不能存明文。Spring Security 新版本里默认的PasswordEncoder是一个DelegatingPasswordEncoder它会把密码按{id}密文格式存储例如{bcrypt}$2a$10$xxxx。你在代码里创建内存用户时一定要先调用passwordEncoder.encode(明文)。Bean UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { UserDetails admin User.builder() .username(admin) .password(passwordEncoder.encode(Admin123)) .roles(ADMIN, USER) .build(); UserDetails viewer User.builder() .username(viewer) .password(passwordEncoder.encode(Viewer123)) .roles(USER) .build(); return new InMemoryUserDetailsManager(admin, viewer); } Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }这里有一个很常见的报错你只自定义了UserDetailsService但忘了声明PasswordEncoder启动时会报There is no PasswordEncoder mapped for the id null。这个问题的根源是默认的密码解析器拿到一个没有{id}前缀的密码不敢确定该用哪种算法去解密所以直接报错。新版本里我建议统一用一个BCryptPasswordEncoder作为全局密码编码器配合DelegatingPasswordEncoder的灵活性。如果你有旧系统的MD5、SHA-256等历史密码需要兼容可以在PasswordEncoderFactories.createDelegatingPasswordEncoder()基础上扩展但新密码一律用 bcrypt。这样既保证兼容又不会把新数据存成弱算法。2.3 基于数据库的真实用户服务内存用户只适合原型阶段真实项目还是要接入数据库。新版 Spring Security 里定义一个UserDetailsServiceBean框架就会在认证流程中自动使用它Service public class DbUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public DbUserDetailsService(UserRepository userRepository) { this.userRepository userRepository; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userRepository.findByUsername(username) .orElseThrow(() - new UsernameNotFoundException(user not found: username)); return org.springframework.security.core.userdetails.User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles().split(,)) .disabled(!user.isEnabled()) .build(); } }记得把数据库用户表里的password字段存成加密后的结果不是明文。很多团队喜欢把roles字段用逗号分隔拼在一个字段里这种方式在用户量不大、角色关系不复杂的系统里确实省事但查询时要注意做权限变更后的缓存刷新否则用户改了角色要等登录态过期才生效。如果你需要把“数据库密码校验失败”“用户被锁定”等不同异常区分开处理可以自定义AuthenticationProvider在里面注入UserDetailsService和PasswordEncoder。但在大多数场景下DaoAuthenticationProvider已经内置了这些功能直接让框架自动装配即可。从配置角度来说新版代码里并不需要手动写一堆 Provider 逻辑。只有当你需要接入第三方登录、短信验证码或者动态 token 时才需要自定义AuthenticationProvider并注册到AuthenticationManager中。2.4 多过滤器链让管理后台和用户接口走不同规则这是新版本里值得充分利用的能力你可以声明多个SecurityFilterChainBean并通过Order控制优先级。旧版里要实现“同一个应用用户端接口和管理后台接口使用不同安全规则”要借助多个 Adapter 的 Order很容易踩坑。新版里干脆把每条过滤器链的匹配规则直接写在requestMatchers上Configuration EnableWebSecurity public class MultiChainSecurityConfig { Bean Order(1) SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher(/admin/**) .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/admin/login).permitAll() .anyRequest().hasRole(ADMIN) ) .formLogin(Customizer.withDefaults()); return http.build(); } Bean Order(2) SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher(/api/**) .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/public/**).permitAll() .anyRequest().authenticated() ) .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(Customizer.withDefaults()); return http.build(); } }这里最容易犯的错误是securityMatcher的作用范围没有覆盖所有请求导致某些路径落到了最底层的默认过滤器链上结果被 401 或 403 拦下来。如果你同时声明了多条过滤器链最好再加一个兜底的默认链确保所有未匹配的请求有明确的安全策略。我在生产项目里就亲眼看过一次这种配置事故用户端接口和管理端接口分了两条链结果OPTIONS预检请求没匹配到任何配置被默认规则直接挡掉前端控制台全是跨域报错排查半天才定位到规则重叠的问题。3. 新版本实际操作过程登录、鉴权、OAuth2 那些容易踩坑的地方3.1 前后端分离下的 JSON 登录接口怎么接很多团队从旧版过渡时问得最多的一句话是能不能不用默认的表单登录页自己写一个/auth/login接口接收 JSON 用户名密码默认的UsernamePasswordAuthenticationFilter只会从请求参数里获取用户名和密码不会解析 JSON。所以你需要做两件事关闭默认的表单登录过滤器。在过滤器链合适的位置添加一个自定义 JSON 登录过滤器或者直接绕过过滤器链在业务 Controller 里手动调用AuthenticationManager。我更推荐后一种“手动认证”方案。在 Controller 里注入AuthenticationManager写一个登录方法PostMapping(/auth/login) public ResponseEntityLoginResponse login(RequestBody LoginRequest request) { Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.username(), request.password()) ); SecurityContextHolder.getContext().setAuthentication(authentication); // 然后按自己项目的规范生成 token 返回给前端 }这样的好处是登录逻辑完全可控路径可以自己定义返回结构也能统一。得到Authentication对象后因为是无状态应用通常会把用户信息和过期时间封装成 Token。配置方面只需要确保/auth/login路径被permitAll放行并且框架不会因为没经过UsernamePasswordAuthenticationFilter而拒绝你的业务请求。一个小细节手动认证时如果没有调用SecurityContextHolder.getContext().setAuthentication(...)后续一旦走到任何依赖当前登录用户的方法级权限处理都会拿不到用户信息。即使你是用 Token 方案也应该在校验 Token 后设置一次 SecurityContext保证AuthenticationPrincipal等注解能正常工作。3.2 方法级鉴权PreAuthorize 和 EnableMethodSecurity 怎么配新版 Spring Security 中方法级安全已经独立成一个专门的注解配置类上要加EnableMethodSecurity而不是旧的EnableGlobalMethodSecurity。这个细节很容易被忽略因为很多旧教程标题写着 Spring Security 6代码里却还在用EnableGlobalMethodSecurity跑起来也不报错但方法上的PreAuthorize就是不生效。正确姿势是在通过Configuration配置的类上加注解Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { // ... }之后在 Controller 或 Service 方法上使用PreAuthorize(hasRole(ADMIN)) GetMapping(/admin/users) public ListUserVO listUsers() { return userService.listAll(); } PreAuthorize(hasAuthority(user:update)) PutMapping(/users/{id}) public void updateUser(PathVariable Long id, RequestBody UserUpdateRequest request) { userService.update(id, request); }区分hasRole和hasAuthority也很重要。如果你在用户服务里给用户设置的是roles(ADMIN)那么框架会默认给它加ROLE_前缀方法注解里就要写hasRole(ADMIN)。如果你设置的是authorities(user:update)这种细粒度权限码就要写hasAuthority(user:update)。两者混放在实际项目里非常常见运维排查权限问题时很多“为什么用户明明有权限但接口返回 403”的案例最后都查到是角色前缀没对上。EnableMethodSecurity里还有几个可以开关的选项比如jsr250Enabled true可以启用RolesAllowedprePostEnabled默认也是开启的。日常项目直接用默认配置就好不用刻意把每个注解体系都打开。3.3 Spring Boot 3 整合 OAuth2 资源服务器时别再纠结 hasScope热词里提到“spring security oauth2 没有 hasScope 方法了吗”这个问题我在很多群里被问过。其实你去翻官方文档或源码会发现oauth2ResourceServer()配置器上并没有一个叫hasScope的全局方法。很多老文章里的写法是从旧的授权服务器扩展点直接抄过来的到了新版本自然编译不过。正确做法是JWT 方式接入资源服务器时在authorizeHttpRequests里判断 Scope。http.oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) )) .authorizeHttpRequests(auth - auth .requestMatchers(/api/orders/**).hasAuthority(SCOPE_order:read) .anyRequest().authenticated() );因为在 Spring Security 的默认实现中从 JWT 的scope或scp声明解析出来的权限会以SCOPE_作为前缀变成一个个 authority。你在授权规则里直接用hasAuthority(SCOPE_order:read)判断即可。看到SCOPE_前缀就明白这是从 JWT Scope 映射出来的权限而不是数据库里给用户单独配置的权限。如果你的授权服务器在 JWT 里存放的是自定义字段比如roles: [admin]那你需要提供一个JwtAuthenticationConverter的 Bean把这个字段解析成ROLE_admin权限否则 Spring Security 默认只会处理scope/scp字段。之前有朋友接到一个第三方单点登录系统JWT 里权限字段叫authorities结果在网关层全部 403最后就是自定义了一个转换器才解决。**自定义转换器的实现很简单Bean JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter converter new JwtGrantedAuthoritiesConverter(); converter.setJwtClaimName(authorities); converter.setAuthorityPrefix(ROLE_); JwtAuthenticationConverter jwtAuthenticationConverter new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter); return jwtAuthenticationConverter; }这段代码的作用就是把 JWT 中名为authorities的声明取出并统一加上ROLE_前缀。这样你在PreAuthorize(hasRole(ADMIN))里写的角色才能匹配上。3.4 登录状态与跨域CORS 到底该配在哪一端前后端分离开发时跨域问题经常被丢给后端。新版 Spring Security 里如果你只依赖 Spring MVC 的CrossOrigin或全局CorsFilter同时又用了 Spring Security有时候 CORS 会被过滤器链的优先级挡住。建议在 Security 配置里统一管理Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors(cors - cors.configurationSource(corsConfigurationSource())) // 其他配置 return http.build(); } Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOriginPatterns(List.of(http://localhost:8080, https://*.example.com)); config.setAllowedMethods(List.of(GET, POST, PUT, DELETE, OPTIONS)); config.setAllowedHeaders(List.of(*)); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }这段配置里我用了setAllowedOriginPatterns而不是setAllowedOrigins。原因是当allowCredentials为 true 时setAllowedOrigins不支持通配符*如果你允许前端带上 Cookie 或 Authorization 头就必须用精确源或者AllowedOriginPatterns。很多初学者会在这里遇到“貌似 CORS 配置了但还是报跨域错误”的情况基本都是因为这个细节。跨域预检请求OPTIONS是由 CORS 机制处理的配置了上面的CorsConfigurationSource后Spring Security 会正确放行预检不需要你在authorizeHttpRequests里单独把OPTIONS全部permitAll。如果你看到接口单独用 Postman 调没问题浏览器一调就挂十有八九是 CORS 配置没生效或没走到 Security 的 CORS 过滤器前而不是后端业务接口拒绝跨域。4. 新版本配置实战排坑我至少遇到过这些异常4.1 启动 500 报错无法获取 AuthenticationManager在 Spring Security 6 中如果你在 Controller 里直接注入AuthenticationManager而项目里又没有显式声明这个 Bean启动可能会失败。常见报错提示找不到AuthenticationManager。解决办法是在配置类中显式暴露它Configuration public class AuthManagerConfig { private final AuthenticationConfiguration authenticationConfiguration; public AuthManagerConfig(AuthenticationConfiguration authenticationConfiguration) { this.authenticationConfiguration authenticationConfiguration; } Bean AuthenticationManager authenticationManager() throws Exception { return authenticationConfiguration.getAuthenticationManager(); } }AuthenticationConfiguration会自动感知你在项目中配置的UserDetailsService、PasswordEncoder以及自定义的AuthenticationProvider最终生成的AuthenticationManager就能用于手动认证。不要自己在配置类里 new 一个ProviderManager那样反而容易漏掉全局的 UserDetailsService。4.2 登录成功后一直拿不到用户信息经常遇到的现象是调用登录接口成功Token 也正常返回了但下一个接口把 Token 带过去后端处理时Authentication为 null。这种情况一般不是过滤器链写错而是没有在每次请求到达 Controller 之前根据 Token 还原登录态。你需要一个自定义过滤器放在UsernamePasswordAuthenticationFilter之前读取 TokenComponent public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null SecurityContextHolder.getContext().getAuthentication() null) { // 这里解析 token得到用户身份 // 构造 UsernamePasswordAuthenticationToken并 setAuthenticated(true) SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }然后在 Security 配置中注册http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);很多项目在接入 Token 登录时登录接口自己写了一套签发逻辑却漏掉了“每次请求都解析 Token 并恢复 SecurityContext”这个环节。只要漏了这一层后续所有靠 SecurityContext 判断用户身份的逻辑全部失效。记住自动登录状态恢复的本质就是在过滤器里替框架把“用户凭证”找回来。4.3 授权规则顺序导致的 403authorizeHttpRequests里的规则是按从上到下顺序匹配的先匹配到的规则先生效。常见的错误是把anyRequest().authenticated()写在中间结果后面的permitAll()全部不生效。比较稳妥的顺序是先放行完全公开的接口和静态资源再做方法级之外的粗粒度角色判断最后用anyRequest()兜底。http.authorizeHttpRequests(auth - auth .requestMatchers(/public/**, /assets/**, /error).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .anyRequest().authenticated() );我一直跟团队强调授权规则不要写得太多太细。系统复杂到一定规模后把所有权限判断都堆在安全配置类里面很难维护。建议在配置类里只做“公共接口放行”和“大块 URL 目录的角色隔离”真正细粒度的数据权限放到 Service 层用PreAuthorize去处理这样定位问题会快很多。4.4 常用问题与排查路径速查现象优先排查点常见原因接口返回 401Token 过滤器是否执行、permitAll路径是否正确Token 解析失败或未放行公开接口接口返回 403用户权限前缀、角色是否匹配没有ROLE_前缀或规则顺序不对登录接口一直走默认登录页是否关闭了formLogin自定义 JSON 登录还需关闭默认表单密码错误但没提示PasswordEncoder是否统一多个密码编码器或存了明文跨域请求报错CORS 配置、OPTIONS预检没有走 Security 的 CorsConfigurationSourceSecurityContext 为空过滤器顺序自定义过滤器没有注册或顺序颠倒PreAuthorize不生效配置类是否加了EnableMethodSecurity使用了旧注解每次排查这些问题时我习惯先看一眼请求到底经过了哪些过滤器。可以在日志级别里把org.springframework.security调成DEBUG过滤链的执行情况会被完整打印出来。实际追踪一遍过滤器执行顺序比自己凭空猜配置位置高效得多。我处理的绝大多数 Security 疑难杂症都是靠这个手段定位到具体过滤器节点的。5. 最后说几句配置思路上的体会从WebSecurityConfigurerAdapter到SecurityFilterChain表面上是换了一套 API背后其实是 Spring Security 团队推动了很多年的设计目标让安全配置显式化、模块化避免代码被隐藏的继承逻辑控制。我在实际项目里最深的体会是配置类不要写成一个巨无霸。无论SecurityFilterChain还是各种Bean都按模块拆开比如密码策略一个类、CORS 一个类、OAuth2 一个类。新版组件化配置本身就适合这种做法但很多人还是习惯把代码全堆在一个SecurityConfig里半年后没人能改得动。另一个很实用的做法是每次升级 Spring Boot 版本前先用 Spring Security 官方迁移文档对照一遍自己项目里用到的 API因为很多“新版本不再支持”的提示并不会在启动时立刻报错而是跑到某个接口时才出现诡异问题。依赖管理尽量用 Spring Boot 的 BOM 统一控制版本不要单独指定某个 Spring Security 版本跟 Boot 大版本错位。最后分享一个小技巧如果你在用 Spring Boot 3.x并且项目里引入了EnableWebSecurity但没有任何SecurityFilterChainBean系统会使用默认的BackButton...这类自动兜底配置。很多人想先快速跑通业务就随手加一行EnableWebSecurity结果所有请求都被默认认证拦住了还以为是自己路径写错。实际上你不做任何 Security 配置时 Spring Boot 也会给应用加上默认账号密码地址就在启动日志里自动生成的那个Using generated security password。这个默认账号密码不是随便生成的而是框架给你留的最后一道保险。搞清楚这套逻辑你对新版本配置的掌握就能比网上大多数照抄教程的开发者更扎实。