
每次看到新项目里有人从网上抄了一段Spring Security配置然后被各种报错折腾到怀疑人生我是真的很想拍一拍他的肩膀说一句“兄弟你大概率是把Spring Boot 2时代的写法硬套到了Spring Boot 3上。”Spring Security这玩意儿单看官方文档会觉得它抽象得不行但只要你抓住了它在新版本里的设计主线——从“继承适配器”变成“声明式过滤器链”——入门和迁移的难度会瞬间下降一个量级。这篇文章我打算直接围绕Spring Security在Spring Boot 3环境下的落地来写重点对比那些由于版本升级导致“昨天还能跑今天全部报错”的配置差异。无论是新手第一次搭登录认证还是老手正在做Spring Boot 2到3的迁移你都能在这篇里找到可以直接复制、能跑通的方案也能理解为什么这么写、为什么以前那么写会废。1. Spring Security到底在解决什么问题1.1 从“认证”和“授权”说起很多初学者看到Spring Security第一反应是“它是不是一个登录框架”这么理解其实偏了。它本质上是应用安全框架核心解决两件事你是谁认证Authentication你能干什么授权Authorization。登录只是认证的一种表现会话管理、密码加密、请求过滤、方法级权限控制统统在它的管辖范围内。拿一个实际场景举例。假设你写了一个后台管理系统普通用户只能看自己的订单管理员才能删除订单、查看用户列表。如果你在每个业务方法里都手写一遍“校验当前登录人有没有权限”代码会爆炸而且极容易漏掉某个接口。Spring Security做的事情就是用一套统一的机制把所有需要鉴权的入口统统拦截下来通过过滤器链一层层判断最终决定“放行”还是“拒绝”。在Spring Boot 3环境下这套机制默认就开着。你只要引入了spring-boot-starter-security依赖随便启动一个Web项目访问任意接口浏览器就会跳到一个默认的登录页账号是user密码在启动日志里。很多同学第一次见这个现象都是懵的“我什么都没配怎么就要密码了”这是因为Spring Security的自动化配置生效了它在你毫无感知的情况下给整个应用加上了防护。1.2 过滤器链是理解Spring Security的地基Spring Security最核心的机制叫过滤器链SecurityFilterChain。每个Web请求进来后会依次穿过多个过滤器每个过滤器检查一类事情。举几个常见的过滤器职责UsernamePasswordAuthenticationFilter处理表单登录请求校验用户名和密码是否匹配。BasicAuthenticationFilter处理HTTP Basic认证。CsrfFilter处理跨站请求伪造防护。AuthorizationFilter基于当前请求的URL和当前用户权限决定允不允许访问。ExceptionTranslationFilter把前面的异常翻译成HTTP响应或登录重定向。以前Spring Security的做法是让你继承一个叫WebSecurityConfigurerAdapter的抽象类然后重写里面的configure方法让你在方法体内堆配置。从Spring Security 5.4开始官方逐步抛弃这个类到Spring Security 6也就是Spring Boot 3用的版本直接移除取而代之的是声明一个SecurityFilterChain的Bean。这个变化是Spring Boot 3引入后大家普遍不适应的根源。你上网搜资料能搜到大量旧教程打开一看是extends WebSecurityConfigurerAdapter粘到项目里发现全是红线因为类根本不存在了。如果你要快速从“看不懂配置”过渡到“能按需定制”核心思路就一条现在写配置不再是用继承来覆盖而是用依赖注入来声明。你告诉Spring Security“我要一个这样的过滤器链”至于链上挂多少过滤器、顺序怎么排框架自己处理你只管你想开放的请求、你不想开放的请求、你用哪种登录方式。2. Spring Boot 3不是版本号变了是配置思维重写了2.1 你以前写的配置为什么全部失效Spring Boot 3.0是基于Spring Framework 6和Spring Security 6打造的而Spring Security 6做了一次非常大的接口清理。最直观的表现是以前常见的antMatchers()方法没了现在叫requestMatchers()and()方法没了改成lambda表达式配置WebSecurityConfigurerAdapter没了改成SecurityFilterChain Bean。我见过一个比较典型的旧代码是这样的Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/public/**).permitAll() .anyRequest().authenticated() .and() .formLogin(); } }这段代码在Spring Boot 2时代是标准答案。但到了Spring Boot 3里EnableWebSecurity虽然还在但WebSecurityConfigurerAdapter已经删了antMatchers也没了authorizeRequests被authorizeHttpRequests取代.and()链式调用也不再被推荐。如果项目里大量使用旧写法升级的时候编译都无法通过只能逐段改。但好消息是新写法其实更简洁而且语义更清晰改完之后整个配置会更容易读懂。2.2 lambda DSL配置解析Spring Security 6推荐的写法是直接用SecurityFilterChain作为Bean配合lambda风格的HttpSecurity配置。HttpSecurity从一个“可以被不断配置的对象”变成“配置的入口”最后通过.build()生成过滤器链但Bean方法里不需要显式调用buildSpring容器会替你完成。说直白一点你现在写的是“描述我要什么”而不是“命令框架怎么做”。这就好比以前你煮面要自己烧水、放面、调料现在你只需要跟厨房说“来一碗红烧牛肉面”后厨自己安排。看一个Spring Boot 3下最基础的配置Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/public/**).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .permitAll() ) .logout(logout - logout.logoutSuccessUrl(/login?logout)); return http.build(); } }你看到没有authorizeHttpRequests这个方法接收一个lambda这个lambda的参数是你自己命名的auth它是AuthorizeHttpRequestsConfigurer的实例。你在lambda里继续调用它的方法不需要再写.and()回到原来的链上。这个设计的好处是每个配置区域都是独立的lambda块不会再出现“链式调用写着写着忘了现在到底在配置谁”的混乱。IDE的自动提示也可以告诉你在这个区块里你能调哪些方法。2.3 密码策略和用户体系的变化Spring Security 6还有一个让不少人中招的点密码格式强制要求。以前PasswordEncoder你随便写个NoOpPasswordEncoder.getInstance()就是明文密码虽然不安全但demo能跑。到了Spring Security 6默认的密码解析器是DelegatingPasswordEncoder它要求数据库里的密码必须携带{noop}、{bcrypt}这类前缀否则直接报错。具体来说如果你用内存用户Bean public UserDetailsService users() { UserDetails user User.withUsername(admin) .password({noop}123456) .roles(ADMIN) .build(); return new InMemoryUserDetailsManager(user); }如果密码不带{noop}前缀登录时Spring Security会认为“你告诉我你是明文密码但我不知道用什么算法去校验”然后抛异常。到了实际生产项目你肯定不用明文密码基本都是BCrypt加密。用BCrypt的话前缀是{bcrypt}但你也可以不写前缀直接让DelegatingPasswordEncoder默认按BCrypt处理。不过一旦你设置了自定义PasswordEncoderBean这个前缀机制的行为会发生变化我建议直接显式声明一个BCryptPasswordEncoderBean然后在存储密码时存它加密后的结果即可。3. 十分钟搭一个能跑的Spring Security项目3.1 依赖引入和最小配置新建一个Spring Boot 3的项目引入依赖时只需要添加一个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency这一步做完应用已经有了默认安全机制。想快速验证的话直接启动应用随便访问一个接口浏览器会跳到http://localhost:8080/login。这是Spring Security自带的登录页虽然丑但功能是全的。但是一般项目中我们要自己定制登录页、放行静态资源、配置多个角色。下面给出一份最小但完整的配置适应大多数管理系统场景Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/css/**, /js/**, /images/**).permitAll() .requestMatchers(/login, /register, /public/**).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .requestMatchers(/user/**).hasRole(USER) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .loginProcessingUrl(/login) .defaultSuccessUrl(/index, true) .failureUrl(/login?errortrue) .permitAll() ) .logout(logout - logout .logoutUrl(/logout) .logoutSuccessUrl(/login?logouttrue) .deleteCookies(JSESSIONID) ); return http.build(); } }csrf(csrf - csrf.disable())这行很多新手问为什么要关掉。CSRF防护是用来防御跨站请求伪造的它要求POST请求带一个随机的token。但在前后端分离或纯后端渲染的系统中如果你不打算在表单里维护这个token就需要关掉它否则你在测试POST请求时会疯狂遇到403。后面我会专门讲CSRF到底怎么处理。3.2 内存用户与角色授权没有数据库时可以用内存用户快速测试。一个完整的用户存储配置大概长这样Bean public UserDetailsService users() { UserDetails admin User.withUsername(admin) .password(passwordEncoder().encode(admin123)) .roles(ADMIN) .build(); UserDetails user User.withUsername(user) .password(passwordEncoder().encode(user123)) .roles(USER) .build(); return new InMemoryUserDetailsManager(admin, user); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }这里有几个细节值得展开一下。User.withUsername()返回的是User.UserBuilder这个builder还会提供.roles()、.authorities()等方法。.roles(ADMIN)底层其实是把权限字符串变成了ROLE_ADMIN所以你在配置里用hasRole(ADMIN)时它会自动拼接ROLE_前缀去和用户权限比对。如果你不想走前缀逻辑想直接控制权限名可以用.authorities(order:read)然后在接口配置处用hasAuthority(order:read)。这是很多做细粒度权限控制的项目选择的方式。我自己的习惯是粗粒度的页面或接口权限用角色细粒度的数据级权限用权限标识。3.3 登录流程与默认页面当你配置了loginPage(/login)后你需要自己提供一个/login的Controller接口和对应的模板页面。如果不提供Spring Security会继续用内置登录页。有一个容易被忽略的点loginPage(/login)本身是permitAll()的但这个/login路径对应的视图解析是归你管的。也就是说Spring Security只负责把它当作登录入口控制器层还是你自己写。一个极简Controller可以是Controller public class LoginController { GetMapping(/login) public String login() { return login; } }然后在模板里放一个表单这个表单的action指向/loginmethod必须是post字段名必须是username和password。因为UsernamePasswordAuthenticationFilter默认就从这两个参数名取值。如果你改了前端字段名比如前端传的是account那么需要在配置里指定.usernameParameter(account) .passwordParameter(pwd)我之前在一个项目里就踩过这个坑前端密码字段叫pass后端没配结果登录永远失败排查了半天最后发现是参数名对不上。4. 把旧项目从Spring Boot 2迁到Spring Boot 3的实战记录4.1 迁移前需要知道的依赖变化做版本升级之前我强烈建议先搞清楚你现在用的是哪一版Spring Boot、哪一版Spring Security。Spring Boot 2.x内部用的是Spring Security 5.xSpring Boot 3.x内部用的是Spring Security 6.x。这个对应关系决定了你的代码要走多少改动。依赖层面有一些明显变化。比如spring-boot-starter-security坐标本身没变但传递进来的spring-security-core等版本全部升到6.x。如果你用了Spring Security OAuth2相关的旧依赖比如spring-security-oauth2-client需要检查是否需要切换到spring-boot-starter-oauth2-client因为Spring Security 6对OAuth2的支持集成得更紧密很多旧包已经停止维护。操作的顺序建议是先升级Spring Boot版本再修编译错误最后做运行时测试。不要想着一步到位因为你所有基于旧版本的配置都要重写一次性改完很可能出现“编译过了但启动循环依赖”的隐患。4.2 逐项改造配置前面已经说了最大的改动是SecurityConfig从继承改为Bean声明。做一个对照表格也许更直观我列出几个最常见的迁移点Spring Boot 2 / Spring Security 5Spring Boot 3 / Spring Security 6extends WebSecurityConfigurerAdapterBean SecurityFilterChain filterChain(HttpSecurity http)http.authorizeRequests()http.authorizeHttpRequests().antMatchers(/path).permitAll().requestMatchers(/path).permitAll().and().formLogin().formLogin(form - form.xxx())在lambda里写子配置EnableGlobalMethodSecurityEnableMethodSecurityhttp.rememberMe()默认基于持久化Token默认基于TokenBasedRememberMeServices需要显式配置key用实际项目来演示。旧项目里你可能写过这样的代码Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/api/public/**).permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage(/custom-login) .permitAll(); }迁移后是Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated()) .formLogin(form - form .loginPage(/custom-login) .permitAll()); return http.build(); }可以看到改动集中在“符号层面”类没了改成方法返回.and()删掉换成lambda块方法名变了。但如果你面对的是一个大型项目配置可能散落多处不只是SecurityConfig一个类。比如方法级安全注解也需要逐项调整。4.3 方法级安全用注解武装起来光靠URL拦截不够时你需要在方法级别控制权限比如“只有管理员才能执行这个Service方法”。Spring Security通过EnableMethodSecurity开启这一能力然后在需要保护的方法上使用注解。Spring Boot 2时代用的是EnableGlobalMethodSecuritySpring Boot 3里改成EnableMethodSecurityConfiguration EnableMethodSecurity public class MethodSecurityConfig { }常用注解有这些PreAuthorize(hasRole(ADMIN))方法执行前校验。PostAuthorize方法执行后校验适合根据返回值判断。Secured(ROLE_ADMIN)一个比较老的注解在Spring Security 6中默认不生效需要额外开启securedEnabled true。PreFilter/PostFilter对集合类型的参数或返回值做过滤实际中不常用。配合今天讲的安全上下文你可以在Service方法里直接写PreAuthorize(hasRole(ADMIN)) public void deleteUser(Long userId) { userRepository.deleteById(userId); }还有一种场景管理员只能删除自己创建的文章。这需要结合当前登录用户判断写法可以这样PreAuthorize(hasRole(ADMIN) and #article.owner authentication.name) public void deleteArticle(Param(article) Article article) { articleRepository.delete(article); }#article是方法参数引用authentication.name是从SecurityContext里取当前登录用户名。这些表达式虽然看起来像魔法字符串但它是Spring的SpEL表达式IDE也能给出一定提示花一点时间学习语调很值。5. 我踩过的一些坑以及排查建议5.1 一直在登录页转圈或所有请求都被重定向到login这个现象非常经典。你访问/user/info结果被踢回/login哪怕登录成功了访问受保护资源还是被重定向回登录页。我开始排查这类问题时一般按下面的思路来。首先确认你访问的路径是否被permitAll覆盖如果/user/info需要认证登录成功后又跳到这个路径但会话里没有正确的Authentication就会出现循环。其次检查登录成功后的默认跳转地址defaultSuccessUrl(/index, true)这个方法的第二个参数true表示忽略来源地址强制跳到/index。如果你没有设置这个登录成功后Spring Security会尝试跳回用户原来想访问的路径如果那个路径又触发认证就会形成环路。另一个高频原因是静态资源没有放行。浏览器加载页面时会请求/css/...、/js/...这些路径如果它们也被拦截页面样式加载不出来而你看到的只是“页面跳到登录页”。排查时可以打开浏览器开发者工具的Network面板看哪些请求返回302。5.2 CSRF和CORS到底怎么配CSRF和CORS这两个词长得像但完全不是一回事。我见过很多人在配置里因为接口403就盲目把CSRF关掉结果Postman里测试是通了安全防护也就被撤了。在Spring Security 6里CSRF防护默认是开启的。如果你做的是后端渲染模板的传统项目需要在模板里给所有POST表单加上input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} /这个_csrf变量由Spring Security自动注入到Model中。如果是前后端分离前端通过请求头X-CSRF-TOKEN携带token后端也可以支持。但纯前后端分离且使用Token认证比如JWT的项目把CSRF关掉是合理的因为CSRF攻击的本质是“浏览器自动携带Cookie”而Token JWT一般存在前端内存或自定义Header里不会自动携带所以不受CSRF影响。CORS则是另一个维度的问题你允许哪些域名跨域访问。需要单独配置Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(https://example.com); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }然后在HttpSecurity里接入http.cors(cors - cors.configurationSource(corsConfigurationSource()));如果发现前端明明配了跨域过滤器却依然被拦截多半是CORS和CSRF的顺序问题。在Spring Security中CORS处理需要先于CSRF执行实际上框架默认就是先CORS后CSRF问题往往出在你自己加的过滤器顺序不对或者没走Spring Security的过滤器链。5.3 自定义认证与JWT的整合思路很多项目在完成了表单登录之后就开始琢磨“我不想用Session我想用JWT”。这是一个合理的演进方向。在Spring Security 6里JWT认证的思路并不是替换掉UsernamePasswordAuthenticationFilter而是在它后面再插入一个认证过滤器。流程大概是这样的用户通过/auth/login接口提交用户名密码。服务端校验通过后签发一个JWT字符串返回给前端。前端后续请求在Header里带Authorization: Bearer token。Spring Security的过滤器链上新增一个过滤器专门解析这个Header。解析成功后就往SecurityContext里放一个Authentication对象之后接口鉴权就跟Session模式没有区别。实现这个自定义过滤器时核心代码可以长这样只做演示生产环境需要更多健壮性处理public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader request.getHeader(Authorization); if (authHeader ! null authHeader.startsWith(Bearer )) { String token authHeader.substring(7); // 解析token得到用户名和权限列表然后构造Authentication对象 // 把Authentication放进SecurityContextHolder } chain.doFilter(request, response); } }然后在SecurityConfig里用addFilterBefore把这个过滤器挂在UsernamePasswordAuthenticationFilter之前http.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);整合时有一个很重要的体验如果是简单场景不建议一上来就上JWT。Session方案是Spring Security默认支持的开箱即用JWT方案的可维护成本其实比大多数人想象的高比如密钥管理、过期时间、刷新策略、注销问题。我见过一个临时项目为了“无状态”上JWT最后光是处理“用户改密码后老token还有效”的问题就花了半天。对你目前阶段来说先跑通表单登录理解过滤器链再自己手写一个JWT过滤器才是最稳的路径。如果确实不确定是否需要JWT建议先默认Session等真的出现横向扩展、多端登录需求时再迁移也不晚认知清晰之后这个迁移速度会很快。表格整理一下常见问题的定位方向现象可能原因排查点所有请求都跳登录页会话未建立或路径未放行检查请求头Cookie调整requestMatchersPOST请求403CSRF未处理模板加_csrf隐藏域或按场景关闭CSRF登录成功但拿不到用户信息SecurityContext未持有Authentication检查自定义过滤器是否执行确认Filter注册顺序自定义过滤器不生效没有加入HttpSecurity过滤器链用addFilterBefore/addFilterAfter手动注册密码报“There is no PasswordEncoder mapped”密码缺{noop}等前缀或未配置PasswordEncoder统一使用BCryptPasswordEncoder方法级PreAuthorize不生效没加EnableMethodSecurity配置类上显式开启6. 从入门到生产的一点个人体会最后说点实在的。Spring Security学习曲线陡很大程度上是因为它的抽象层级高、概念密度大但真正写起来大多数人用到的配置也就那么几个过滤链、密码加密、用户加载、方法注解。你只要手写一个带数据库用户和自定义登录页的小项目这些概念瞬间就能串起来——别只是看文档一定要亲手搭一个把默认登录页换成自己的把内存用户改成数据库用户再把一个接口用注解锁起来跑通一次后面就顺了。另外我强烈建议你去翻一翻Spring Security的源码不需要深读就看SecurityFilterChain是怎么被构建出来的、HttpSecurity的那些方法分别往链里塞了什么过滤器。看一次之后再遇到什么奇怪问题你的直觉会准很多。如果你正在做Spring Boot 2到3的升级别指望一个晚上搞定尤其是老项目里塞满了各种过滤器、HandlerInterceptor、自定义注解的话一步步编译、一步步修多留几次测试验证的时间。这也是为什么我坚持让你先用小项目练手的原因——等你在小项目里把那些配置差异摸清了再回老项目动手心态会稳很多。