Spring Security 认证授权核心机制与 Spring Boot 3 实战解析
1. 认识 Spring Security它到底是什么能替你解决哪些安全问题先说结论Spring Security 是 Spring 生态里的安全框架核心解决两件事——认证Authentication和授权Authorization。说白了就是回答两个问题“你是谁”和“你能干什么”。这个框架不是最近才火的新东西它从 Acegi 时代一路迭代到今天已经成了 Java 后端做安全控制的默认选择。你只要用 Spring Boot 搭 Web 应用只要涉及登录、权限、接口保护十有八九会用到它。很多人一开始觉得它难是因为它跟传统写业务代码的思路不太一样——你不需要在每个接口里写一堆 if 判断“有没有登录”而是通过一套过滤器链在请求进入 Controller 之前就把安全检查做完。我见过不少团队用了几年的 Spring Security出了 BUG 只会到处复制配置对底层机制一知半解。这篇文章我想从整体设计思路讲起再把热点问题逐个拆开Filter 是怎么注册进去的、OAuth2 里 hasScope 为什么不见了、Spring Boot 3.x 下到底该怎么配。适合刚接触 Spring Security 的初学者也适合用了很久但没深入过原理的开发者。2. 整体架构与核心思路拆解2.1 三大核心组件SecurityContext、Authentication、SecurityContextHolder讲 Spring Security 之前得先建立一个概念它所有的工作都围绕一条链来展开——“身份信息从哪来、放在哪、怎么被取用”。SecurityContext一个接口内部保存当前请求的 Authentication 对象。你可以把它理解成一个“信封”里面装的是当前用户是谁、有什么权限。Authentication代表当前用户的身份凭据和权限集合。它有三个关键信息principal用户主体通常是 UserDetails 对象或者用户名credentials凭证通常是密码认证完成后会被清空authorities已授予的权限集合。SecurityContextHolder一个静态工具类用 ThreadLocal 方式持有 SecurityContext。在同一个线程内你随时可以用SecurityContextHolder.getContext().getAuthentication()拿到当前用户信息。这三个组件的关系我用大白话解释一下SecurityContextHolder 像一个储物柜ThreadLocal 相当于给每个请求分一个独立的格子互不干扰格子里面放着 SecurityContext 信封信封里装着一张写着用户信息的纸也就是 Authentication。整个 Spring Security 的过滤链做的主要工作就是“往信封里塞纸”和“检查纸上写了什么”。2.2 认证流程的运行逻辑从请求进入过滤器链到身份落定如果你第一次接触 Spring Security最容易困惑的是“认证到底在哪里发生的”。这里的关键机制是Spring Security 默认不拦截所有请求做强制认证而是通过过滤器链来实现策略控制。一次带 Basic Auth 或表单登录的请求大致的流程是请求进入FilterChainProxy这是整条过滤器链的总入口按配置顺序执行链条上的各个 Filter比如SecurityContextHolderFilter从 Session 或请求中恢复上下文、UsernamePasswordAuthenticationFilter处理提交的用户名密码、AnonymousAuthenticationFilter给未认证的请求设置匿名身份等当执行到授权相关过滤器时如AuthorizationFilter会检查当前Authentication是否已认证、是否满足访问当前 URL 所需的权限如果未认证会抛出AuthenticationException或被重定向到登录页如果认证了但权限不足会抛出AccessDeniedException。这个过程有个很关键的点过滤器链在业务代码之前执行所以你的 Controller 里拿到的永远是一个已经经过安全处理的“当前用户状态”。这也是为什么 Spring Security 使用体验好——你压根不需要在 Controller 里写一堆重复的登录校验逻辑框架在入口处就把活干完了。2.3 为什么选择过滤器链而不是其他方案有些读过源码的人会问为什么 Spring Security 不直接用一个拦截器HandlerInterceptor或者 AOP 切面来做安全检查非要搞一条复杂的过滤器链我个人的理解是过滤器是 Servlet 标准里最早的、也最靠近容器底层的扩展机制它能覆盖的不只是 Spring MVC 的路径还包括静态资源、错误分发、以及非 Spring 管理的 Servlet。拦截器和 AOP 都晚了过滤器一步很多场景覆盖不到。再说了过滤器链天然支持“先检查再放行”的流水线模型对“认证→授权→后续处理”这种安全流程非常契合。另外Spring Security 的过滤器链不是一串任意排列的 Bean它通过SecurityFilterChain接口来组织你可以配置多个 SecurityFilterChain每个链可以匹配不同路径、应用不同规则。这种设计让多租户、前后端分离、接口和页面分别管控这样的场景变得很干净。3. 核心机制详解Spring Security Filter 是如何完成注册的这个点是官方文档和中文社区里讨论热度非常高的一个问题。很多人扒 Spring Boot 源码时会好奇我们并没有手动注册 Spring Security 的过滤器到 Servlet 容器它到底是怎么生效的3.1 DelegatingFilterProxy 与 FilterChainProxy 的关系先讲两个容易混淆的类DelegatingFilterProxy它是 Spring 提供的一个 Servlet 过滤器但本身不做安全逻辑只是一个“中间人”。它在 Servlet 容器中注册被调用时从 Spring 容器里找名字为springSecurityFilterChain的 Bean再把请求委托给它。FilterChainProxy这才是 Spring Security 真正的主角。它实现了Filter接口内部可以持有多个SecurityFilterChain每个链包含一组有序的过滤器。为什么要有这一层“代理套代理”的设计因为直接注册FilterChainProxy到 Servlet 容器是可以的但 Spring Security 需要解决“什么时候初始化、Bean 还没准备好怎么办”这类问题。DelegatingFilterProxy 机制让过滤器的生命周期和 Spring 容器的生命周期解耦了一些保证容器能先完成初始化再通过代理获取真正干活的 Bean。3.2 在 Spring Boot 3.x 中Filter 到底是怎么“自动”注册的Spring Boot 3.x 使用了新的自动配置机制SecurityFilterAutoConfiguration里面做了这么几件事注册一个名为springSecurityFilterChain的DelegatingFilterProxyRegistrationBean这个RegistrationBean会把DelegatingFilterProxy注册到 Servlet 容器中映射路径默认是/*DelegatingFilterProxy在首次被调用时会从 Spring 容器中获取名为springSecurityFilterChain的 Bean通常就是FilterChainProxyFilterChainProxy内部再按照配置好的SecurityFilterChain列表逐条匹配并执行过滤器链。所以如果你在 Spring Boot 项目里跑起来看一眼启动日志会发现多了一个名为springSecurityFilterChain的过滤器它其实就是FilterChainProxy的化身。如果不理解这层关系你会以为 Spring Security 用魔法给你做了所有事情魔法失灵的时候连排查方向都找不到。3.3 过滤器链的默认排序与自定义过滤器的插入位置默认的过滤器链上过滤器是有固定顺序的比如SecurityContextHolderFilter在前AuthorizationFilter在靠近末尾的位置。这种顺序设计是有道理的先得把请求身份信息构建好后面的授权过滤器才能检查。如果你需要自定义过滤器比如加一个验证请求头、记录审计日志可以用addFilterBefore()或addFilterAfter()指定位置。这里有个实操上的重要提醒插入过滤器时一定要搞清楚你要在哪个过滤器前后生效。我见过有人把自定义校验逻辑放在SecurityContextHolderFilter之前导致根本拿不到用户信息也有人把过滤器放在授权过滤器之后结果被拒绝的请求已经“响应”了逻辑压根不执行。3.4 多个 SecurityFilterChain 的匹配顺序Spring Security 允许配置多个SecurityFilterChain匹配规则是“先到先得”——按配置顺序优先匹配第一条能匹配上的 Chain之后的就不再执行。这在做 API 和后台管理区分时很好用第一链匹配/api/**使用无状态 JWT 认证放行登录接口第二链匹配/admin/**使用表单登录 CSRF 保护要求管理员角色第三链匹配所有路径默认全部放行或全部拒绝。但千万注意顺序的坑如果你把“匹配所有路径”的链放在最前面那后面的链全都会失效。这个点我在实际项目里踩过一次坑后来养成习惯具体的链放前面兜底的链放最后。4. Spring Security 6.x 与 OAuth2 相关变化的重点解读4.1 hasScope 方法去哪了OAuth2 授权范围与权限的纠葛最近网上有很多人问“Spring Security OAuth2 没有 hasScope 方法了吗”。这个问题背后其实是一个版本演进的故事。如果你用的是早期的 Spring Security OAuth2比如 Spring Boot 1.x/2.x 时代的spring-security-oauth2项目那时候资源服务器校验 Token 时可以直接用.access(oauth2.hasScope(read))这样的写法或者使用hasScope(read)。这是因为旧项目的EnableResourceServer和EnableOAuth2Sso注解提供了一套内置的 OAuth2 校验模型hasScope是这套模型里的一个表达式方法。但到了 Spring Security 5.x 之后尤其是 Spring Boot 2.x 后期官方把 OAuth2 的功能整合进了 Spring Security 核心里的spring-security-oauth2-resource-server原来的EnableResourceServer被废弃大量spring-security-oauth2的写法就“变味”了。你不再能在配置里直接调用hasScope()因为AuthorizationFilter的权限检查模型已经变成了 “基于 GrantedAuthority 通用模型”。换句话说系统不再区分“这是一个 scope”还是“这是一个 role”在权限上下文里它们统称为authority。这时如果你配的是 JWT 登录JwtGrantedAuthoritiesConverter会把 JWT 里的scope字段转换成带SCOPE_前缀的 authority。举例来说JWT 声明中包含scope: read那么转化后就变成了SCOPE_read。所以你的授权表达式应该写http.authorizeHttpRequests(auth - auth.anyRequest().hasAuthority(SCOPE_read))在方法安全上如果你用PreAuthorize也应该写成hasAuthority(SCOPE_read)。网上很多说“hasScope 没了”的帖子其实是因为他们没有重新理解“权限模型统一”这件事——scope 和 role 在 Spring Security 内部都被降维成了 authority但增加了前缀规则来区分来源。4.2 Spring Authorization Server 取代了旧的 OAuth2 工程再延伸一个相关变化原来的spring-security-oauth2-authorization-server是被拆出来的独立项目后来正式进入 Spring 官方维护路线。Spring Boot 3.x 之后如果你想自己搭一个授权服务器Authorization Server来签发 Token官方推荐用的就是 Spring Authorization Server而 Spring Security 本身专注做资源服务器Resource Server和客户端Client。这就意味着老的spring-security-oauth2依赖里很多类在 Spring Boot 3.x 里都已经不存在或迁移了。如果你在迁移老项目遇到SecurityFilterChain里oauth2ResourceServer()配置不生效、或者引入旧依赖后报 ClassNotFound 的错误第一反应应该是检查是不是引入了已被淘汰的 OAuth2 旧依赖。4.3 Spring Security 6.x 相比 5.x 的关键差异Spring Security 6.x 有几个对升级影响很大的变化配置方式彻底转向 Lambda DSL老的antMatchers()方法移除改用requestMatchers();默认禁止 CSRF对无状态 API 应用来说更省心但传统表单登录场景要注意配置密码编码策略升级默认PasswordEncoder是DelegatingPasswordEncoder存储格式是{bcrypt}...所以密码编码器的选择要明确授权模型简化authorizeRequests()改成了authorizeHttpRequests()角色校验变化因为 authority 模型统一要求角色检查时要注意 ROLE_ 前缀的匹配逻辑。这些差异不是语法层面随便改改那么简单背后是 Spring Security 在“去 OAuth2 特殊化、统一为通用权限模型”的过程中做的简化。理解了这个大方向你看到一堆 API 变化就会觉得很合理。5. 实操过程基于 Spring Boot 3 搭建一个可运行的 Security 示例理论讲再多不如跑一个项目来得直观。这一节我带大家从零配置一个 Spring Boot 3 Spring Security 6 的项目覆盖 JWT 登录、接口保护、角色授权这几个最常见的需求。依赖我用的版本是 Spring Boot 3.2.x对应的 Spring Security 6.2.x。5.1 引入依赖与基础配置先在pom.xml里加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency如果要用 JWT还需要引入jjwtdependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.12.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.12.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.12.5/version scoperuntime/scope /dependency只要引入spring-boot-starter-security哪怕你没写任何配置Spring Boot 也会自动注册上面说的过滤器链默认要求所有请求认证并生成一个随机密码。所以项目启动的时候控制台里那一段Using generated security password: xxx就是自动配置的默认密码。5.2 核心安全配置类SecurityFilterChain 的配置与参数含义写一个SecurityConfig配置类Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .requestMatchers(/api/user/**).hasAnyRole(USER, ADMIN) .anyRequest().authenticated() ) .exceptionHandling(ex - ex .authenticationEntryPoint((request, response, authException) - { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或Token无效\}); }) .accessDeniedHandler((request, response, accessDeniedException) - { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\message\:\没有权限访问\}); }) ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }逐个解释关键点csrf.disable()对于纯前后端分离的 REST API 来说CSRF 防护通常不需要因为 CSRF 攻击依赖浏览器的 Cookie 自动携带而 JWT 通常存在 Header 里。如果你做的是表单登录的传统 Web 应用不要图省事直接禁用它。sessionCreationPolicy(STATELESS)告诉 Spring Security 不使用 Session 保存登录状态。每次请求都要通过 Token 来认证。requestMatchers(/api/auth/login).permitAll()登录接口必须放行不然用户没法登录。hasRole(ADMIN)等价于检查有没有ROLE_ADMIN这个权限。addFilterBefore(...)把自定义的 JWT 过滤器放到UsernamePasswordAuthenticationFilter之前执行这样请求到达 Controller 之前就会完成 Token 解析和身份注入。5.3 自定义 JWT 过滤器与登录认证流程登录请求进来了Spring Security 不会自动帮你验证用户名密码需要你自己实现。一个常见做法是写一个 Controller 接收用户名密码调用AuthenticationManager做认证成功后签发 JWT。定义AuthenticationManagerBeanBean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); }然后在内部Spring Security 会使用DaoAuthenticationProvider完成从UserDetailsService加载用户、比对密码的逻辑。所以你要实现一个UserDetailsService从数据库或内存里加载用户信息Service public class UserDetailsServiceImpl implements UserDetailsService { Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 这里替换为从数据库中查询用户 if (!admin.equals(username)) { throw new UsernameNotFoundException(用户不存在); } return User.withUsername(admin) .password(passwordEncoder().encode(123456)) .roles(ADMIN) .build(); } }登录接口RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthenticationManager authenticationManager; Autowired private JwtUtil jwtUtil; PostMapping(/login) public MapString, String login(RequestBody LoginRequest request) { Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) ); String token jwtUtil.generateToken(authentication.getName()); return Map.of(token, token); } }这里有个很核心的细节authenticationManager.authenticate()抛出的异常由 Spring Security 决定是继续往下走还是直接中断。如果用户名密码不对会抛出BadCredentialsException你不捕获的话会返回 500 或 401根据你的异常处理来定。建议在全局异常处理器里专门捕获AuthenticationException和AccessDeniedException。JWT 过滤器核心逻辑Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtil jwtUtil; Autowired private UserDetailsService userDetailsService; 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 { String username jwtUtil.extractUsername(token); if (username ! null) { UserDetails userDetails userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } catch (Exception e) { // 解析失败则视为未认证不设置 SecurityContext后续过滤器会拦截 } } filterChain.doFilter(request, response); } }这里有个容易踩的坑过滤器内如果解析 Token 失败不要把异常直接抛出否则请求直接中断。更好的做法是不设置 SecurityContext让后面的授权过滤器看到“未认证”状态统一走AuthenticationEntryPoint返回 401。这样才能保证异常处理路径统一。5.4 密码加密与 UserDetailsService 的配合前面代码里用到了passwordEncoder()这个方法需要定义 BeanBean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }密码加密这个环节我见过很多初级项目直接明文存储这是非常危险的做法。BCrypt 是一种适合密码哈希的算法特点是自带随机盐、计算速度可控能抵抗暴力破解。Spring Security 的DaoAuthenticationProvider会自动调用PasswordEncoder.matches(rawPassword, encodedPassword)来比对密码所以你的UserDetailsService返回的密码必须是数据库里存的编码后的值。在实际项目里用户注册时应该用passwordEncoder.encode(rawPassword)存储密码登录时由框架自动完成校验不要自己写解密逻辑。5.5 方法级安全在 Controller 上使用 PreAuthorize 增强权限控制很多场景下URL 级别的权限控制不够细比如同一个接口A 用户可以访问B 用户不能访问。这时候方法级安全就派上用场了。在配置类上加了EnableMethodSecurity之后可以直接在方法上用注解GetMapping(/order/{id}) PreAuthorize(hasRole(ADMIN) or orderService.isOwner(#id, authentication.name)) public Order getOrder(PathVariable Long id) { return orderService.getOrder(id); }这里的orderService.isOwner(#id, authentication.name)是 Spring Security 的 SpEL 表达式可以调用 Spring Bean 的方法做自定义校验。这种写法比硬编码一堆 if 判断要优雅得多而且和 URL 级别的授权是叠加生效的。我个人的习惯是URL 级别配置粗粒度的入口规则方法级别处理细粒度的资源所有权校验两层结合既简洁又安全。6. 常见问题与排查技巧实录这部分我整理了自己和团队在项目里经常碰到的几个问题每个都是亲手排查过的直接给结论和排查思路。6.1 为什么有时候返回 403有时候返回 401这是初学者最容易困惑的问题。Spring Security 中401表示“未认证”——你可能根本没有登录或者 Token 无效。触发点是AuthenticationEntryPoint。403表示“已认证但没有权限”——你登录了但是访问的资源超出了你的角色/权限范围。触发点是AccessDeniedHandler。如果你发现“用户明明没登录却返回 403”多半是因为匿名用户也能通过认证链只是没有权限。解决办法是检查你的安全配置对于需要认证的路径不要用permitAll()应该用authenticated()。另外exceptionHandling里两个 Handler 都要配置否则默认行为可能会把异常渲染成错误页前后端分离时很烦。6.2 自定义 Filter 被调用了两次怎么排查我遇到过一次自定义的认证过滤器在两台实例上各执行了一次导致用户状态被覆盖。排查后发现是配置了多个SecurityFilterChain而我的自定义过滤器被同时添加到了多个链上导致一个请求路径匹配了多条链。还有一种可能性是DelegatingFilterProxy和 Spring Boot 自动注册的 Filter 都注册了一次两者叠加后看起来执行了多次。解决方法是确认你的过滤器 Bean 是否实现了Filter接口且同时被手动RegistrationBean注册了。Spring Boot 的自动注册机制对容器中所有FilterBean 都会生效如果你同时手动注册就会重复执行。6.3 使用 JWT 后登录状态突然消失如果你在无状态模式下登录后再次请求接口发现SecurityContextHolder.getContext().getAuthentication()是 null排查顺序如下确认 JWT 过滤器有没有被添加到过滤器链的正确位置确认过滤器有没有把Authentication设置到SecurityContextHolder确认每次请求都会经过过滤器有没有被permitAll()的路径跳过了确认OncePerRequestFilter的shouldNotFilter方法没有误伤。这里有个关键点Spring Security 6 默认在请求结束时会清空 SecurityContext如果你用的是有状态 Session框架会自动从 Session 中恢复如果是无状态模式必须每次请求重新解析 Token 并设置 SecurityContext。如果你发现“第一次请求有用户信息第二次没有了”大概率是 Token 解析或过滤器链被某个异常中断了。6.4 CORS 配置到底放在哪里才生效前后端分离的项目跨域问题绕不开。Spring Security 6 中配置 CORS 的正确姿势是http.cors(cors - cors.configurationSource(corsConfigurationSource()));然后在Configuration里提供一个CorsConfigurationSourceBeanBean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of(*)); configuration.setAllowedMethods(List.of(GET, POST, PUT, DELETE, OPTIONS)); configuration.setAllowedHeaders(List.of(*)); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, configuration); return source; }很多同学在 Spring MVC 里配置过CrossOrigin或WebMvcConfigurer但在 Spring Security 下光配置这些是不够的因为安全过滤器链上还需要开启cors()。否则预检请求OPTIONS会被安全链拦截导致页面报 CORS 错误。还有一个细节setAllowCredentials(true)时setAllowedOriginPatterns不能用*否则浏览器会拒绝请求。6.5 为什么角色带上了 ROLE_ 前缀还是没生效Spring Security 里如果你用.hasRole(ADMIN)框架内部会自动补全成ROLE_ADMIN然后和UserDetails里的 authorities 比较。所以你有两种写法给用户赋权限时用grantedAuthorities加ROLE_ADMIN或者用.roles(ADMIN)快捷方式。但有个容易忽略的点如果你使用 JWT 认证Token 解析后生成的GrantedAuthority列表里如果是ADMIN而不是ROLE_ADMIN那么hasRole(ADMIN)就匹配不上因为框架会自动找ROLE_ADMIN。解决办法是在 JWT 过滤器解析时做权限前缀映射ListGrantedAuthority authorities jwtUtil.extractRoles(token).stream() .map(role - new SimpleGrantedAuthority(ROLE_ role)) .collect(Collectors.toList());我把这个坑写出来是因为项目里有个同事排查了一整天最后发现是 JWT 里只放了role: ADMIN没有加前缀配置里用的又是hasRole导致一直 403。7. 与其他安全框架的对比与选型建议有不少人在社区里问“Shiro 和 Spring Security 选哪个”。我个人的态度很明确如果是新项目除非有特殊理由比如团队对 Shiro 极其熟悉否则优先选 Spring Security。原因有三生态整合度高官方对 OAuth2、OIDC、LDAP、JWT 等都有现成的支持Shiro 在这些领域往往需要自己拼装第三方库更新迭代稳定Spring Security 随 Spring Boot 一起发布版本兼容性由官方保证不会出现“框架升级后安全配置 API 失效”这种失控情况社区问题库丰富遇到问题搜解决方案基本都有人踩过坑排障成本低。Shiro 的优势在于学习曲线平缓、配置相对简单适合在 Spring 的早期项目或者非 Spring 项目中使用。我一开始学的是 Shiro后来切到 Spring Security 时确实经历了一段难受期但撑过去就觉得整套模型更严谨。如果团队项目已经是 Spring Boot 技术栈我建议不要混用两套安全框架否则多套过滤器链叠加后会产生各种诡异问题。8. 后续扩展方向从基础认证到 Spring Authorization Server如果你已经能独立完成上面的 JWT 登录示例下一步我建议往这几个方向深入Spring Authorization Server 授权码模式自己搭建一个授权服务器为第三方应用签发 Token涉及授权码模式、刷新令牌、客户端注册等概念OAuth2 Resource Server 与自定义 JWT 解析不自己解析 Token而是交给 JwtDecoder并配置 JWT claim 到 authority 的映射规则动态权限把 URL 和角色的映射关系存入数据库通过自定义AuthorizationManager实现运行时动态授权分布式会话与无状态 Token 的取舍微服务场景下 Session 和 JWT 的选型、Token 续期、黑名单方案设计。这些方向每一个展开都能写一篇长文但基础还是本文提到的这些组件。把 SecurityContext、过滤器链、认证与授权模型吃透后面学什么都顺。9. 最后分享一点我的实操体会写这篇文章时我回想了一下这几年用 Spring Security 的过程。最大的体会是不要一开始就追求把每个 Filter 的源码都看完但要抓住核心链路。你只需要理解“请求进来 → 过滤器链检查 → 身份落定 → 放行到 Controller”这条主线遇到问题再往细节里钻效率远高于一开始就死磕源码。另外排查安全问题时要保持一个习惯先确认请求走的是哪条 SecurityFilterChain、哪个过滤器在拦截。Spring Security 的日志里其实会输出过滤器链的执行顺序和匹配结果启动时把日志级别调到 DEBUG很多时候报错的原因在日志里已经写得明明白白。遇到 403 和奇怪的重定向先看 DEBUG 日志再去看代码这是我最常用的排查套路。如果你打算在真实项目里落地建议从最简单的场景开始先配好内存版的用户和角色把登录流程跑通再加数据库和 JWT最后再考虑复杂的 OAuth2 分布式授权。一步到位反而容易失控。希望这篇文章能帮你把 Spring Security 的骨架搭起来后面再遇到具体问题欢迎带着场景和日志来聊。