
1. 项目概述1.1 Spring Security到底是什么我估计很多刚接触Spring Security的人跟我当初一样打开官方文档看到一屏的Filter、AuthenticationManager、SecurityContext这些术语那感觉就像面对着一个黑洞。我先用人话把它说清楚Spring Security本质上就是一道放在你Web应用前面的安检门所有的HTTP请求进来之后都得先过这道门。它负责回答两个最核心的问题——你是谁和你能干什么用学术点的说法就是认证Authentication和授权Authorization。认证解决的是识别身份比如你登录时提交的用户名密码它得确认你确实是张三而不是李四授权解决的是权限控制比如确认你是张三之后得判断你有没有资格访问/admin这个接口。这两个核心问题解决了其实大部分Web应用的安保需求也就解决了。Spring Security厉害的地方在于它把这两个问题背后的复杂逻辑——包括会话管理、密码加密、CSRF防御、记住我功能、跨域配置等等——全都封装成了一套可插拔的组件你只需要通过配置把这些组件串起来就行了。我见过很多Java开发者在项目里直接用拦截器Interceptor配合HandlerInterceptor来做登录校验刚开始确实简单但随着业务复杂化你会发现自己在重复造轮子密码加密方案各写各的登录会话失效逻辑漏洞百出接口权限散落在业务代码里。Spring Security至少在行业标准层面帮你把这些问题都规范了。1.2 谁需要读这篇文章这篇文章不是给那种已经能熟练改造Spring Security底层过滤器的老手看的我面向的是下面这些人群刚进入Java后端开发岗位不久、接手了公司项目但发现项目里配了一套看不懂的安全机制的新人准备自己写个人项目但不知道登录校验怎么做得规范的学生以及项目升级到Spring Boot 3后还在用老代码硬凑发现启动直接报错的开发者。文章会花大篇幅讲Spring Boot 3升级过程中配置代码的迁移问题这是目前最容易让Spring Security新手和老手都头疼的地方。因为Spring Security在5.x到6.x之间做了几次比较大的重构尤其是WebSecurityConfigurerAdapter这个类被彻底移除了如果你在百度上看到的大部分教程都是2021年以前的你照着抄在Spring Boot 3里大概率是启动不了的。2. 核心概念拆解与新旧版本差异2.1 必须搞清楚的三个核心组件在动手写代码之前我建议你先花十分钟理解这几个概念不然你后面配置代码里的每个方法都会看着一头雾水。第一个是SecurityFilterChain。它是整个安全机制的载体你可以理解成是一个安检流程清单。Spring Boot启动的时候会创建一个名字叫springSecurityFilterChain的Servlet过滤器这个过滤器把Spring Security里所有的过滤器按顺序组织起来形成一个链HTTP请求按顺序穿过这条链上的每一个过滤器只要任何一个过滤器不放行请求就被拦截了。你要配置的哪些路径需要登录才能访问、登录页面长什么样、放行哪些静态资源最终都是往这条链上挂过滤器或者调整链上过滤器的行为。第二个是AuthenticationManager。它是专门负责校验身份的组件。你提交用户名密码上来Spring Security通过AuthenticationManager去找你配置的UserDetailsService这个服务负责根据用户名从数据库里查用户信息然后把查出来的用户信息和提交的密码做比对。密码匹配通过说明身份有效。第三个是SecurityContext。它保存的是当前请求到底是谁发起的这样的上下文信息。一个用户认证成功之后Spring Security会把认证结果放到SecurityContext里后续的请求到达你的Controller时你可以通过SecurityContextHolder.getContext().getAuthentication()拿到当前用户的信息。Spring Session的并发控制和会话失效底层也是围绕这个上下文来管理的。这三个概念搞清楚之后你再看配置代码就会轻松很多因为你会知道你在改的是谁的行为。2.2 Spring Boot 3中配置迁移的核心变化现在来说说热词里提到的Spring Boot 3配置迁移问题。这是目前搜索Spring Security相关教程时最大的痛点也是我要重点讲的部分。在Spring Boot 2时代我们写安全配置最常见的方式是继承WebSecurityConfigurerAdapter这个类然后重写它的configure(HttpSecurity http)方法。我印象中2019年到2021年之间市面上几乎99%的Spring Security教程都是这个写法。网上搜索出来的代码样例基本是Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/**).permitAll(); } }这个写法在Spring Security 5.x时代是可以正常工作的。但Spring Boot 3基于Spring Security 6.x这个类已经被彻底移除了。你如果在新项目里沿用这个写法启动时会直接抛java.lang.AbstractMethodError或者因为找不到WebSecurityConfigurerAdapter类而编译失败。新的推荐写法是直接声明一个SecurityFilterChain的Bean。基于组件的注册方式整体的思路是把配置行为变成组件的定义。Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/admin/**).hasRole(ADMIN) .requestMatchers(/login).permitAll() .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .loginProcessingUrl(/doLogin) .defaultSuccessUrl(/index) .permitAll() ) .logout(logout - logout.logoutUrl(/logout)); return http.build(); } }除了这个最核心的变化配置迁移中还涉及几个特别容易翻车的小点。第一antMatchers()变成了requestMatchers()。antMatchers在Spring Security 6里还能用但已经标了弃用而且官方明确建议切换到requestMatchers同时路径匹配规则从Ant风格逐步转向了PathPattern风格写通配符的时候要注意写法上的差异。第二方法名变了。authorizeRequests()和permitAll()这些老方法在6.x中仍然保留但在代码提示里会出现过期标记。新的链式调用风格是authorizeHttpRequests()配合Lambda表达式代码的配置块更清晰。我在迁移项目的时候发现直接用新风格改起来其实很快就是一个方法名替换的活儿。第三默认行为变了。Spring Boot 3里的Spring Security默认几乎把常见攻击的防御都打开了CSRF默认开启登录表单默认生成X-Frame-Options默认拒绝iframe嵌入。这也是好事但如果你在写前后端分离项目遇到POST请求被403挡回来八成就是CSRF没关或者没配置好。第四依赖坐标也变了。老的spring-boot-starter-security坐标不变但如果你直接依赖spring-security-config或spring-security-web这些底层模块需要注意版本要匹配Spring Boot 3的版本管理最好直接用BOM来统一版本。3. 实操从零搭建一个Spring Boot 3安全项目3.1 环境准备与依赖引入动手之前先把环境准备好。我的建议是直接创建Spring Boot 3.x项目JDK版本至少17以上。这里给出我实测可用的版本组合方便你参考。组件版本建议JDK17或21Spring Boot3.2.x或3.3.xSpring Security由Spring Boot BOM统一管理6.2.x构建工具Maven或Gradle均可引入依赖时如果你只需要最基础的登录认证能力先引入这三个就够了spring-boot-starter-web、spring-boot-starter-security还有一个用来连接数据库的spring-boot-starter-jdbc或者spring-boot-starter-data-jpa。不过为了保持文章聚焦先不引入数据库相关的东西用内存用户测试等把流程跑通了再接数据库。3.2 最简配置与认证逻辑实现新建一个Configuration类我给它起名叫SecurityConfiguration。夫妻先配置一个SecurityFilterChain的Bean这是新版的核心。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; Configuration EnableWebSecurity public class SecurityConfiguration { Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize - authorize .requestMatchers(/, /home, /css/**, /js/**).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .requestMatchers(/user/**).hasRole(USER) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .permitAll() ) .logout(logout - logout .logoutUrl(/logout) .logoutSuccessUrl(/) ); return http.build(); } }注意这里我用/admin/**和/user/**做了两个不同的权限层级一个需要ADMIN角色一个需要USER角色。这样配置之后所有未登录用户访问受保护资源时会被引导到/login页面。这个页面需要你自己准备。顺便说一句requestMatchers方法可以接收多个路径参数也可以接收AntPathRequestMatcher对象灵活度很高。接着我们需要声明用户信息。在没接数据库的时候用InMemoryUserDetailsManager来定义两个测试账号是最方便的。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; Configuration public class UserConfig { Bean public UserDetailsService userDetailsService() { UserDetails admin User.withUsername(admin) .password({noop}admin123) .roles(ADMIN, USER) .build(); UserDetails user User.withUsername(user) .password({noop}user123) .roles(USER) .build(); return new InMemoryUserDetailsManager(admin, user); } }这里有一个非常关键的细节我先说一下后面还会反复提到{noop}前缀表示这个密码是明文存储没有任何加密。Spring Security 5之后默认要求密码必须有某种编码格式如果你直接在.password()里写纯字符串而不用{noop}或者没给PasswordEncoder启动时会报错提示No PasswordEncoder mapped for the id null。生产环境当然不能这么干这里只是为了先把流程跑通。3.3 登录页面与Controller配置接下来我们加一个简单的Controller来验证效果同时配置登录页。先用Thymeleaf模板引擎来渲染页面。加入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependencyController代码import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; Controller public class PageController { GetMapping(/) public String home() { return home; } GetMapping(/admin) public String admin() { return admin; } GetMapping(/user) public String user() { return user; } GetMapping(/login) public String login() { return login; } }在src/main/resources/templates/目录下建home.html、admin.html、user.html和login.html四个页面。login页面里放一个表单注意表单的提交地址和字段名称必须跟Spring Security默认约定一致提交地址是/login用户名和密码字段名分别是username和password。如果字段名不同你需要自定义认证参数我建议先按默认来。!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head title登录/title /head body h1登录/h1 form action/login methodpost div label用户名/label input typetext nameusername/ /div div label密码/label input typepassword namepassword/ /div button typesubmit登录/button /form /body /html一个很容易让新手困惑的点是我明明没有写处理/loginPOST请求的Controller方法为什么这个表单提交能生效答案就是Spring Security内部的登录过滤器自动处理了这个地址的POST请求。当用户提交表单时过滤器拿到用户名和密码调用AuthenticationManager认证认证成功后再把用户信息写入SecurityContext然后重定向到defaultSuccessUrl指定的地址。这就是框架的魅力——很多逻辑你不需要写但要理解它的存在。做完这些启动项目访问http://localhost:8080/可以看到公开的home页面。访问/admin时会被重定向到登录页因为该路径需要ADMIN角色。登录成功之后跳回原来想访问的页面。整个过程就是一个完整的认证加授权闭环。4. 实战难点密码加密与用户权限数据加载4.1 PasswordEncoder的正确用法刚才演示的时候为了快速跑通流程用了{noop}明文密码。实际项目里密码绝不可能这么存数据库泄露一次就全完了。Spring Security提供了一个PasswordEncoder接口业内最常用的是BCryptPasswordEncoder。BCrypt算法加盐且计算耗时可控抵抗暴力破解的能力很强。在Spring Security 6中你可以直接声明一个PasswordEncoder的Bean。注意一旦你声明了它之前UserDetailsService里的密码就必须用它来编码。import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }在初始化测试用户时可以改成这样。不过为了后面的实操我还是手动编码一下直接把用户数据放到里面。UserDetails admin User.withUsername(admin) .password(passwordEncoder().encode(admin123)) .roles(ADMIN, USER) .build();如果你使用UserDetailsService从数据库查询用户查出来的密码也一定是数据库中存储的BCrypt密文。登录时Spring Security会自动调用passwordEncoder.matches(rawPassword, encodedPassword)来做校验你不需要在Service层手动比对密码。这一点也很重要如果你自己写LoginService又调了一次matches可能会因为密码已经被比对过了导致两套逻辑冲突。4.2 自定义UserDetailsService的接入方式内存用户只适合演示真实项目里都是从数据库读用户。我们定义一个实现UserDetailsService接口的Service重写loadUserByUsername方法。import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; Service public class DatabaseUserDetailsService implements UserDetailsService { // 假设这里注入了UserMapper或者UserRepository private final UserRepository userRepository; public DatabaseUserDetailsService(UserRepository userRepository) { this.userRepository userRepository; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity user userRepository.findByUsername(username) .orElseThrow(() - new UsernameNotFoundException(用户不存在: username)); return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles().split(,)) .build(); } }Spring Security在登录时发现容器里有一个UserDetailsService的实现类就会自动用这个Service来加载用户信息链接数据库直接验证用户身份不再需要你手动调用。这也是为什么很多人配置完数据库用户表之后登录逻辑一句代码都不用改就能生效的原因。这里容易出现的一个问题是权限设计。很多人把权限和角色混为一谈实际上Spring Security里角色Role是特殊的权限角色名称自动带有ROLE_前缀在代码中使用时写hasRole(ADMIN)而不是hasAuthority(ROLE_ADMIN)。如果你数据库中存的是不带前缀的权限标识可以用hasAuthority等方法做权限级别的控制。5. 前后端分离场景下的JSON登录与鉴权配置5.1 禁用表单登录改用JSON登录接口现在很多项目是前后端分离的前端登录页面是Vue或React后端只提供JSON接口。这种情况下默认的formLogin默认返回HTML重定向就不合适了需要配置JSON登录。做法主要有两种一是写一个登录Controller接收JSON体手动调用AuthenticationManager来完成认证二是自定义登录成功和失败处理器让Spring Security内部的过滤器返回JSON数据。我倾向于用第一种逻辑更直观也更好排查问题。import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; RestController RequestMapping(/api/auth) public class AuthController { private final AuthenticationManager authenticationManager; public AuthController(AuthenticationManager authenticationManager) { this.authenticationManager authenticationManager; } PostMapping(/login) public MapString, Object login(RequestBody LoginRequest request) { UsernamePasswordAuthenticationToken token new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword()); Authentication authentication authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authentication); MapString, Object result new HashMap(); result.put(code, 0); result.put(message, 登录成功); result.put(username, authentication.getName()); return result; } }这个方案的优点是简单直白登录成功之后认证信息就在当前会话中生效了配合基于Session的认证机制完全够用。缺点是没有充分发挥Spring Security过滤器链已经内置好的登录即处理的优势需要自己写Controller层的逻辑。如果你希望接口只接受JSON请求拒绝表单提交可以在HttpSecurity里关闭formLoginhttp.formLogin(form - form.disable());同时关闭CSRF因为前后端分离项目如果不用Session存储Token而是用JWT之类的无状态令牌那么CSRF攻击的场景就会变化这种情况下需要关闭默认开启的CSRF防御不然POST请求会被403拦截http.csrf(csrf - csrf.disable());5.2 JWT无状态鉴权的快速集成思路如果项目要做成纯无状态接口服务用JWT来替代Session是最常见的方案。JWT本质上就是把用户信息和过期时间用签名算法加密成一个字符串服务端不保存会话状态每次请求时前端把Token放在请求头Authorization: Bearer xxx里服务端解密验证Token有效之后直接认定请求者的身份。在Spring Security里接入JWT标准套路是写一个过滤器继承OncePerRequestFilter在这个过滤器里解析请求头中的JWT如果Token有效就把用户的认证信息设置到SecurityContext中。然后在过滤器链上把这个过滤器放在UsernamePasswordAuthenticationFilter之前。这里有个小细节是登录成功之后你自己生成Token千万不要在过滤器里既解析Token又校验密码那就是职责混乱了。我的建议是新建一个JwtAuthenticationFilter并且配置它在前。JWT的解析如果失败直接放行让后面的过滤器决定用户到底是匿名还是被拦截。import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.List; public class JwtAuthenticationFilter extends OncePerRequestFilter { private final String secretKey; public JwtAuthenticationFilter(String secretKey) { this.secretKey secretKey; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); try { Claims claims Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); String username claims.getSubject(); ListSimpleGrantedAuthority authorities List.of(new SimpleGrantedAuthority(ROLE_ADMIN)); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception ignored) { // Token无效或过期SecurityContext保持未认证状态即可 } } filterChain.doFilter(request, response); } }然后在配置类里把过滤器塞进去就可以了http.addFilterBefore(new JwtAuthenticationFilter(secretKey), UsernamePasswordAuthenticationFilter.class);5.3 会话并发控制与记住我功能如果你还在用Session方案有个比较隐蔽但是很实用的配置是会话并发控制。默认情况下Spring Security允许多个端同时登录同一个账号。有些业务场景不允许比如同一个账号只能在一处登录这时候配置一下sessionManagement就行。http.sessionManagement(session - session .maximumSessions(1) .expiredUrl(/login?expiredtrue) );配置了maximumSessions(1)之后用同一个账号在新设备登录时旧设备访问任何受保护资源都会被弹出登录页面或者被踢下线。记住我功能也是个小细节。注册了这个功能之后用户勾选记住我关闭浏览器重新打开之后登录状态还在。它的实现原理是发送一个加密过的Cookie里面包含用户名和过期时间下一次请求访问时过滤器从这个Cookie恢复出用户信息。配置方式很简单http.rememberMe(remember - remember .key(uniqueAndSecretKey) .tokenValiditySeconds(86400 * 7) );一个注意点是key必须持久化稳定如果应用重启后key变了之前发的所有记住我Cookie都会失效用户全都需要重新登录。6. 真实踩坑Spring Boot 3迁移中的常见问题6.1 antMatchers消失与路径匹配差异这个坑是目前最常见的一个。网上老教程里是的authorizeRequests().antMatchers(/admin/**)在Spring Boot 3里把项目一启动就直接报错或者代码里疯狂划黄线提示过时。解决方案是把authorizeRequests()改成authorizeHttpRequests()把antMatchers()改成requestMatchers()。代码风格建议用新版Lambda写法。还有一个更隐蔽的地方是路径匹配规则的细微差别。Spring Security 6默认的路径解析器是PathPattern它跟老的AntPathMatcher在匹配规则上有区别。比如/admin/*在老风格里匹配一层路径新的PathPattern也差不多是这个语义但叠加通配符时建议实际测试一下最好把配置里的路径统一到/**和/*这两种常见写法上能避开绝大多数差异问题。6.2 CSRF导致的403问题前后端分离项目中排查问题花费时间最多的可能就是请求被无缘无故403了。明明用户名密码都正确接口就是调不通。这个问题十有八九是CSRF防护在拦截。Spring Security 6默认开启CSRF防护POST、PUT、DELETE这些有副作用的请求都会被要求携带CSRF Token。你用表单开发的时候Spring Security会自动在模板渲染时把CSRF Token放到请求里Thymeleaf不用你额外处理。但如果你用Postman测接口、用Vue发Ajax请求Token就不存在了请求直接403。如果项目仍然走Session认证且没有同源策略限制那我建议保留CSRF防御然后在请求头里带上Token。如果项目是无状态JWT方案直接关闭CSRF也说得过去因为JWT放在消息头里天然防护了CSRF攻击接外部请求时不依赖浏览器自动携带Cookie。注意权衡。6.3 迁移到Spring Boot 3后登录验证码失效还有一个我从多个项目里见过的现象升级到Spring Boot 3之后原本能正常显示的验证码图片登录流程突然进不去登录页或者验证码服务接口直接返回401。原因是在Spring Security 6中默认未认证的请求进入登录页动态生成的验证码图片接口往往位于放行列表之外导致请求被打回。只要在过滤链配置里把验证码生成接口放进permitAll()列表即可。/captcha,/code/image这些路径全部放行认证之后接口访问就不受影响了。6.4 启动报错PasswordEncoder must not be null或循环依赖如果启动时报PasswordEncoder相关错误多半是UserDetailsService加载用户时密码没有指定编码器。检查两个点容器里有没有声明PasswordEncoder的Bean用户密码字段是否有{noop}前缀或是否用BCrypt编码过。如果报循环依赖错误尤其是自动配置了数据源和SecurityConfig之间互相依赖一般检查一下是否在UserDetailsService的Bean里引用了当前配置类中的方法导致两个Bean互相依赖。建议把PasswordEncoder单独放在一个配置类里避免和安全配置类耦合。7. 项目小结与个人经验7.1 为什么推荐使用新写法用Spring Boot 3写安全配置刚上手的新发明最好不要再去网上翻老教程。官方文档现在已经是组件注册的写法而且WebSecurityConfigurerAdapter这种继承方式本身就是对框架的一种侵入式使用它让你覆盖父类方法父类方法的逻辑对使用者不透明而新的SecurityFilterChainBean方式更符合组合优于继承的思想配置就是声明扩展就是往链路里加过滤器排查问题时链路也更清晰。7.2 快速上手路径建议如果你是想快速在项目中用起来我建议按这个顺序来先把最简的SecurityFilterChain配置跑通页面表单登录然后替换成数据库用户服务和BCrypt密码接着给REST接口加JSON认证最后根据需要再接JWT或者引入OAuth2。每一步验证一次不要试图一次把全部功能都堆上去不然出了问题排查起来会无从下手。7.3 从0到1的最终参考配置我最后把所有内容浓缩一下给出一个可以继续扩展的最终参考配置。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; Configuration EnableWebSecurity public class SecurityConfig { Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**, /, /home, /css/**, /js/**, /captcha/**).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .requestMatchers(/user/**).hasRole(USER) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .permitAll() ) .logout(logout - logout .logoutUrl(/logout) .logoutSuccessUrl(/) ) .sessionManagement(session - session .maximumSessions(1) .expiredUrl(/login?expiredtrue) ); return http.build(); } }我个人在实际操作中的体会是Spring Security入门的第一道坎不是API的用法本身而是心态。它的过滤器链确实抽象配置项也很多但只要你抓住一个核心链路——请求进来经过过滤器过滤器里调用认证管理器认证管理器加载用户信息并判断密码是否匹配匹配之后把结果塞进安全上下文后续请求就能拿到用户身份——那么不管后面的JWT、OAuth2、方法级权限怎么变你就都不会乱了。先把这条链用最小配置跑通剩下的都是往链上挂东西。