
做了几年 Java 后端Spring Security 是最让我又爱又恨的一个框架。爱是因为它确实把认证授权这套通用逻辑封装得很完整恨是因为一旦配置不对光是 401、403 还有那个无限重定向就能折腾一晚上。不过话说回来只要把它的过滤器链和认证授权模型真正吃透后面不管是写企业后台、写多商户平台还是接第三方登录都会顺手非常多。这篇文章我按自己的实际使用经验来写不搞那种从入门到放弃的教科书式铺陈。我会先拆一下框架的运作逻辑再带你把一套基于 Spring Boot 的认证授权从零搭起来最后把我在项目中真正遇到过的几个典型问题和排查思路一起列出来。不管你是准备面试还是准备上手做项目这篇文章应该都能给你一些直接能用的东西。1. 为什么每个 Java 后端都应该吃透这套框架如果你只是想在本地写个小 demo那 Spring Security 确实显得有点“重”。但只要你做的系统要上线、要接真实用户认证授权就是绕不过去的安全底线。Spring Security 本质上解决的是两个问题你是谁以及你能干什么。前者是认证后者是授权。很多初学者喜欢自己在拦截器里写一套 Session 判断和权限校验说实话小项目确实能跑但一旦权限模型变得复杂比如有角色继承、有数据范围限制、有第三方接入自己维护的那套代码很快就会变成一堆补丁。Spring Security 的真正价值在于把这些问题标准化了。它提供了一套完整的过滤器链机制每一个请求从进来到响应会经过一系列有序的过滤器每个过滤器只关心一件事情。比如登录表单处理、Session 校验、记住我、匿名身份识别、权限投票等都对应着不同的过滤器。这意味着你不需要从零开始设计一套安全体系只需要在它给的框架里做配置和扩展。另外一点是对接 Spring Boot 实在太顺。Spring Boot 的自动配置会把 Spring Security 默认接入上下文你只要引入依赖所有接口立刻处于被保护状态连登录页都是自动生成的。对于企业级项目来说这种开箱即用的安全感很重要。哪怕你不是专门做安全的也必须掌握它的核心模型因为你会发现很多框架的文档、很多开源项目都在用 Spring Security 做权限基础比如 Spring Cloud 的网关鉴权、OAuth2 客户端、资源服务器等。它不只是一个小工具而是整个 Java 服务端安全体系的基石。这套体系适合谁去深入我建议三类人一定别绕开第一类是准备 Java 后端面试的Spring Security 在简历里出现的频率很高面试官几乎一定会问认证流程和过滤器链第二类是正在做 Spring Boot 业务系统的尤其是后台管理、Saas、电商这类需要复杂权限控制的第三类是自己维护过权限代码并且已经感到吃力的把 Spring Security 的模型理解透之后你会发现自己之前写的好多“权限工具类”其实都是这个框架里边已经实现好的东西。2. 核心运作机制拆解2.1 过滤器链是如何一环扣一环的Spring Security 的核心是一个过滤器链它在业务请求到达 Controller 之前先经过一串过滤器。常见的关键过滤器按照顺序大致是SecurityContextPersistenceFilter负责从 Session 里读取 SecurityContext 并放到当前线程、UsernamePasswordAuthenticationFilter处理表单登录、BasicAuthenticationFilter处理 HTTP Basic 认证、RememberMeAuthenticationFilter处理记住我、AnonymousAuthenticationFilter给未认证用户一个匿名身份、FilterSecurityInterceptor做最后的授权判断。每一环都是有顺序的顺序错了会出很隐晦的问题。这里面最容易误解的一点是匿名用户并不是“没有身份”而是会被赋予一个匿名的 Authentication 对象。这在权限配置里很关键比如你要放行某些接口但又不想让匿名用户拥有某些角色配置的时候就要区分 permitAll 和 anonymous。我用过不少项目配置成 permitAll 之后以为万事大吉结果匿名用户照样能访问到那些只该给登录用户看的数据就是因为没搞清楚匿名身份这个底层逻辑。鉴权判断发生在 FilterSecurityInterceptor 这一环它会在请求进入 Controller 之前把当前请求需要的权限和当前用户拥有的权限做一个匹配。如果用户还没认证就会抛出 AccessDeniedException随后被 ExceptionTranslationFilter 捕获再决定是跳转登录页还是返回 403。如果你自己写过全局异常处理会发现 Security 的异常经常到不了 RestControllerAdvice原因就是异常在过滤器链这一层已经被拦下来处理了。2.2 认证流程的核心组件和协作方式认证流程里最重要的几个组件是 AuthenticationManager、ProviderManager 和 AuthenticationProvider。实际调用关系大致是这样的UsernamePasswordAuthenticationFilter 从请求里拿到用户名密码后封装成一个 UsernamePasswordAuthenticationToken这个 Token 此时是未认证状态。它会被交给 AuthenticationManager而 AuthenticationManager 是一个门面接口真正干活的是它的实现类 ProviderManager。ProviderManager 会遍历自己维护的一批 AuthenticationProvider逐个询问“你能不能处理这种 Token”能处理的就去执行认证逻辑。最常见的 DaoAuthenticationProvider 会调用 UserDetailsService 去数据库查用户拿到 UserDetails 之后再用 PasswordEncoder 比对密码。密码比对通过后返回一个已认证的 Authentication 对象也就是把 Token 里的 authenticated 字段从 false 改成 true同时把权限列表填进去。最后这个对象被放进 SecurityContext再写入 Session。整个过程听起来环数很多但每个组件都只负责一件事所以排查问题的时候非常方便。比如你发现用户一直登录失败可以先确定是 UserDetailsService 没查到人还是密码匹配失败再往下定位。很多初学者会混淆 UserDetails 和 Authentication 的区别。UserDetails 是从数据源通常是数据库查出来的用户信息是“原始素材”Authentication 是认证的结果状态存放在 SecurityContext 中。Authentication 里通常封装了 principal对应用户信息、credentials一般是密码或者令牌认证成功后会被清空、authorities权限列表。理解这两层数据结构之后你才会明白为什么改密码之后有时候要重新登录才生效因为 Session 中的 Authentication 还保留着旧的信息。2.3 授权模型里容易被忽略的细节授权部分的核心模型是 GrantedAuthority它是一个字符串形式的权限标识在框架里通常用角色 Role 和权限 Authority 两种方式混用。我实践中比较推荐的是基于权限的细粒度控制比如 READ_USER、WRITE_ORDER然后通过角色把这些权限打包。这样比单纯用角色做判断灵活很多尤其是当你需要“这个角色除了某项权限以外全部拥有”这种场景时基于权限的模型很容易扩展。框架在请求级授权上默认是按投票器模式处理的也就是 AccessDecisionManager 下面挂了一批 AccessDecisionVoter。每一张“选票”会对“是否允许当前请求”给出允许、拒绝或弃权。最常用的投票器是 RoleVoter 和 AuthenticatedVoter。默认的投票策略是如果一个投票器投了拒绝票就直接拒绝如果有多个允许票但有一个拒绝票也还是拒绝。这个细节有时候会被忽略导致你配置了多个权限规则其中一个不匹配却能影响最后的决定。理解了投票机制你就能解释为什么两个看似不相关的配置会互相影响。2.4 方法级权限的底层逻辑除了请求级授权Spring Security 还支持方法级别的安全控制通过 EnableGlobalMethodSecurity 或新版 EnableMethodSecurity 开启。开启后在任意 Bean 方法上加上 PreAuthorize、PostAuthorize、Secured 等注解就能在方法调用前或调用后进行权限校验。它的底层用的是 AOP切面会在方法调用前检查当前 SecurityContext 里的 Authentication 是否满足 SpEL 表达式。我看过不少项目请求级配置放行了一大批接口然后在 Service 层用 PreAuthorize 做精确控制效果其实挺好。唯一的坑是如果方法是在同一个类内部调用的AOP 代理不会生效注解会直接跳过。比如在 UserService 里一个方法调用了同一个类的另一个带权限注解的方法权限检查是不会被触发的。这个属于 Spring AOP 的经典问题遇到的时候先想想是不是自调用导致的。3. 从零构建一套可落地的认证授权体系3.1 项目依赖与初始准备下面我用 Spring Boot 2.7 作为基础来演示因为目前很多企业项目还在用这个版本虽然 3.x 已经出来了但核心差异并不大稍后我会标注不同点。先引入核心依赖Spring Security 的 starter 和 Web 应用相关依赖就够了。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency这里有个容易踩坑的地方。Spring Boot 的自动配置会在 classpath 中找不到 Spring Security 相关配置时生成一个默认用户用户名是 user密码在启动日志里。很多新手会困惑为什么没有配用户却可以登录其实这是 Spring Boot 的保护机制提示你已经开始使用安全框架了。真正做项目时我们会写一个 SecurityFilterChain 配置 Bean把它替换掉。我第一次用的时候没有关闭 CSRFPostman 调试登录接口时发现除了 GET 请求其他请求全部 403排查了半天才发现是 CSRF 防护在拦截。后来我在教程里一律建议如果你做的是纯后端接口服务或者前后端已经分离CSRF 防护可以直接关掉或者用 Token 方式处理不然联调阶段会很难受。当然如果是服务端渲染的页面应用CSRF 防护建议保留。3.2 核心配置类的编写方式Spring Boot 2.7 及以后版本推荐通过 SecurityFilterChain Bean 来做配置而旧版的 WebSecurityConfigurerAdapter 已经废弃了。这个变化一定要跟上因为新项目再写 WebSecurityConfigurerAdapter 不仅报警告还会在很多细节上有行为差异。下面是一个配置骨架Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(auth - auth .antMatchers(/api/auth/**, /h2-console/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .antMatchers(/api/user/**).hasAnyRole(USER, ADMIN) .anyRequest().authenticated() ) .formLogin(form - form .loginProcessingUrl(/api/auth/login) .successHandler((req, res, auth) - { res.setContentType(application/json;charsetUTF-8); res.getWriter().write({\code\:200,\msg\:\login success\}); }) .failureHandler((req, res, e) - { res.setContentType(application/json;charsetUTF-8); res.getWriter().write({\code\:401,\msg\:\login failed\}); }) ) .logout(out - out .logoutUrl(/api/auth/logout) .logoutSuccessHandler((req, res, auth) - { res.setContentType(application/json;charsetUTF-8); res.getWriter().write({\code\:200,\msg\:\logout success\}); })) .sessionManagement(session - session .maximumSessions(1) .expiredUrl(/login)); return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public UserDetailsService userDetailsService(DataSource dataSource) { JdbcUserDetailsManager users new JdbcUserDetailsManager(dataSource); return users; } }在 Spring Boot 3.x 里antMatchers 换成了 requestMatchers用法上差不太多但老代码直接迁移的话会编译不过。这个细节面试的时候经常有人被问我也栽过一次所以提一下。上面这套配置完成之后你实际上已经有了一套可以跑起来的登录、注销、Session 管理逻辑。successHandler 和 failureHandler 写成 JSON 输出是为了方便前后端分离如果你的页面是服务端渲染其实用默认的跳转逻辑就够了。3.3 用户的存储与密码加密策略用户信息的存储最简单的方案是 JdbcUserDetailsManager它底层依赖一组固定的表结构默认表名是 users 和 authorities。你要是自己定义表可以通过 SQL 来初始化这组表。生产环境我更推荐自己实现 UserDetailsService 接口因为业务用户表往往和默认表结构差别很大而且一般还需要关联部门、角色、状态等字段。密码加密这件事非常重要。真实项目里绝对不能明文存密码哪怕哈希过一次也不够安全。Spring Security 的 PasswordEncoder 接口本身就是策略模式你可以选择 BCrypt、Argon2、SCrypt 等实现。我实践中用得最多的是 BCryptPasswordEncoder因为它的计算开销适中算法自带盐值而且每次生成的哈希值都不一样。这点常被误解有人以为两次加密结果不同是系统 bug其实这是 BCrypt 的盐值机制在起作用验证明文密码时依然可以正确匹配。实现自己的 UserDetailsService 时有一个隐藏的细节需要处理框架内部调用 loadUserByUsername 方法时如果返回了 UserDetailsDaoAuthenticationProvider 会自动用 PasswordEncoder 对前端传的明文密码和 UserDetails 中的加密密码做比对。你的 UserDetails 里那个 getPassword 必须返回数据库中加密后的密文而不是原始明文。我见过有人在这个地方把明文密码放进去导致每次校验都能成功相当于登录校验形同虚设。Service public class DbUserDetailsService implements UserDetailsService { private final UserMapper userMapper; public DbUserDetailsService(UserMapper userMapper) { this.userMapper userMapper; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user userMapper.selectByUsername(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } ListGrantedAuthority authorities new ArrayList(); // 这里建议把角色和权限分别查出来按 ROLE_ 前缀标识角色其他标识权限 for (String roleCode : userMapper.selectRoleCodes(user.getId())) { authorities.add(new SimpleGrantedAuthority(ROLE_ roleCode)); } for (String permCode : userMapper.selectPermCodes(user.getId())) { authorities.add(new SimpleGrantedAuthority(permCode)); } return new User(user.getUsername(), user.getPassword(), user.getEnabled(), true, true, true, authorities); } }User 类的构造函数里后面几个 boolean 参数分别是 enabled、accountNonExpired、credentialsNonExpired、accountNonLocked。这四个状态任何一个为 false登录时都会被拒绝。如果用户被管理员禁用后还能正常登录先排查这个 enabled 是不是正确地从数据库映射过来了。3.4 会话管理、登录态控制和并发处理登录认证成功之后用户的 Authentication 会存到 Session 中后续请求通过 Session 里恢复出的 SecurityContext 来识别身份。默认实现是 SecurityContextPersistenceFilter 从 Session 读取并写入上下文响应结束后再写回。这里有个性能上的细节每个请求结束时都会把 SecurityContext 序列化到 Session如果你的登录用户表字段很多Session 里存的东西就会很臃肿。你可以考虑实现 SecurityContextRepository 接口把上下文放到 Redis 里这样既支持分布式会话又降低 Session 体积。并发登录控制是一个很容易被忽略的点。配置文件里我写了 maximumSessions(1)它可以把同一账号的同时在线数量限制为 1此时系统默认的策略是新登录会把旧登录踢下线。在某些场景下你可能希望的是相反行为新登录失败提示“该账号已在别处登录”。这可以通过 maxSessionsPreventsLogin(true) 来设置。两种策略没有绝对的好与坏主要是看业务需求但很多人不知道有这两个选项结果只能自己在登录接口里写 Redis 记录来做限制实际上 Spring Security 已经提供了现成的开关。对于前后端分离项目表单登录默认的登录页面和跳转逻辑可能不适合你你可以通过登录接口返回 Token 或 Session 信息给前端前端以后在请求头里带着凭证即可。Session 方案虽然传统但依然可靠使用的时候注意设置合理的超时时间太短会让用户频繁登录太长会有安全风险。更现代的做法是接入 JWT 做无状态认证这需要自定义过滤器或使用 Spring Security OAuth2 Resource Server我暂时不展开但上面这套 Session 机制理解好了切到 JWT 也是很容易的。3.5 基于注解的方法级权限控制方法级权限配合业务系统特别舒服比如一个订单列表接口可能需要按不同角色返回不同数据Controller 层不好写太多 if else就可以把这些逻辑下沉到 Service 层并用注解保护。先开启注解支持Configuration EnableMethodSecurity public class MethodSecurityConfig { }然后我们就可以在方法上写注解了Service public class OrderService { PreAuthorize(hasRole(ADMIN) or hasAuthority(ORDER_VIEW)) public ListOrder page(int pageNum, int pageSize) { return orderMapper.page(pageNum, pageSize); } PreAuthorize(hasRole(ADMIN) and #order.userId authentication.principal.id) public void update(Order order) { orderMapper.updateById(order); } }这里最实用的一个点是 SpEL 表达式里可以拿参数和方法返回值。hasRole、hasAuthority、hasPermission 这些表达式在官方文档里都列得很清楚但像 #order.userId authentication.principal.id 这种写法是自己灵活扩展的关键。它可以在一个注解里同时完成权限和数据归属校验省掉业务代码里的额外手工判断。我在多商户和后台管理项目里经常这样用它来实现“数据行级权限”的一部分比如只允许操作自己的数据。还有两点容易忽略。第一是 PreAuthorize 在对象创建时通过 AOP 代理生效如果你把调用写成类内部自调用注解会被绕过。第二是方法级别注解和请求级权限是叠加的请求级放行了不代表方法级就一定能过必须两个层面都通过才最终放行。这个设计看起来有点冗余但在大型系统里反而成了双保险。4. 认证授权之外的常见扩展场景4.1 记住我功能的实现与原理“记住我”是一个看起来简单但内部有点门道的功能。如果只是把用户名塞到 Cookie 里那等于是裸奔。Spring Security 的记住我默认使用了 PersistentTokenBasedRememberMeServices它会生成一个随机的 token存到数据库的表里。用户下次访问时会携带令牌过滤器链中的 RememberMeAuthenticationFilter 会去校验校验通过则恢复登录状态。配置方式是在 HttpSecurity 里调用 rememberMe()并设置 token 仓库。我做过一次踩坑记录当时记住了功能开启后用户重启浏览器再访问明明 Cookie 还在却一直跳到登录页。后面发现是因为我把 rememberMe 的 key 改了旧令牌校验不通过。key 本质上是一个加密盐值用来签名令牌改了以后旧数据全部失效。如果你要长期运行的系统这个 key 最好配置成固定的变量而不是随机的临时值否则每次重启服务所有用户的“记住我”都会失效。4.2 CSRF 防护到底应该怎么处理CSRF 防护是 Spring Security 默认开启的功能它通过每次请求校验一个 token 来防止跨站请求伪造。对于传统的服务端渲染页面这是一道很重要的防线。但对于纯 REST API尤其是基于 Token 或 Session 的接口服务CSRF 防护经常会给开发带来不小麻烦。因为无状态 API 本来就不依赖 Cookie 做身份识别攻击面已经显著缩小所以很多团队会选择关闭。我的建议是分场景看待。如果做的是内部系统、接口服务、App 后端直接关闭 CSRF 基本没有太大问题。如果做的是面向 C 端且有 Cookie 会话的网站那一定要保留或者用其他机制替代。关闭方式在 HttpSecurity 配置里调用 csrf().disable()。曾经有人在生产环境没关 CSRF结果队友在 Postman 里调试 POST 接口时一直调不通其实只要在请求头里加一个 X-CSRF-TOKEN 就能解决但团队如果不了解这个机制会浪费很多时间。4.3 跨域 CORS 与安全过滤器顺序前后端分离项目里必然遇到跨域问题。Spring Security 和 Spring MVC 各有自己的 CORS 处理逻辑如果两个层面的配置不一致会出现一个现象预检请求 Options 通过了实际请求却被拦截。原因是 Spring Security 的过滤器链中如果没有配置 CorsFilter或者配置的顺序不对请求根本走不到 MVC 的跨域处理中。推荐做法是在 HttpSecurity 里直接配置 cors()并且自定义 CorsConfigurationSource。这样做的好处是安全过滤器链内统一处理预检和实际请求不会被来源拦截。一个常见的报错是“Expected CORS token X-... to be parsed”这类问题通常就是你在 Security 层没有放行预检请求导致的。我一般在和前端联调的时候会在浏览器控制台里看预检响应头缺什么头就是哪里没配对比瞎猜快得多。4.4 分布式会话语境下怎么扩展当你的服务从单机变成多实例Session 默认存放在单机内存里就不够用了。Session 不共享会导致用户在一台机器上登录下一次请求被负载均衡到另一台机器却识别不了。两个常见思路一个是把 Session 放到 Redis 中Spring Session 这个项目就是干这个事的另一个是改成无状态 JWT 方案。前者能保留传统的用户友好度后端改动较小后者更利于接口的扩展和移动端接入。用 Spring Session 时只需要引入对应的依赖并且确定仓库类型。比如用 Redis 就是 spring-session-data-redis。引入之后不需要改太多代码Session 存取自动走 Redis。需要注意的坑是如果你的系统用了 WebSocketWebSocket 握手时会话验证也依赖同一个 Session 仓库上线前要测一下。另外Redis 版 Session 的序列化方式默认基于 JDK 序列化跨服务调用时要保证对象实现了 Serializable否则会报序列化异常。5. 常见问题与排障实录5.1 登录接口一直返回 403 而不是 401这个问题在前后端分离项目中出现的频率极高。明明密码错误或不存在的用户应该返回 401但接口上写的是 403。原因通常是 CSRF 保护未关闭或者匿名用户请求被打到了需要完整认证的接口而 Spring Security 对未认证访问返回的是 403 或者重定向到登录页。排查路径是先看请求有没有经过 Security 的过滤器链再看配置的 authorizeHttpRequests 到底把接口分到了哪一块。如果你在浏览器里点开请求发现响应头里有个 Location 指向 /login那说明认证失败后被引导去做表单登录了不走 JSON 返回。比较好的做法是配置 authenticationEntryPoint让未认证请求直接返回 JSON 而不是重定向。这样前端好处理一些而且接口风格的系统也更统一。http.exceptionHandling().authenticationEntryPoint((req, res, e) - { res.setStatus(401); res.setContentType(application/json;charsetUTF-8); res.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); });5.2 配置了 permitAll 但接口还是被拦截permitAll 不会执行任何权限校验这是很多人的认知。但现实是即使 permitAll请求也会先经过认证流程之前的那些过滤器。如果你的 Security 配置一直触发到 AnonymousAuthenticationFilter然后又被 ExceptionTranslationFilter 拦截那大概率是请求根本没有匹配到你期望的那条规则。尤其在使用 antMatchers 时路径匹配规则有优先级顺序写在前面的规则优先匹配。如果一个接口同时被多个规则覆盖最终执行的规则是最先匹配的那条而不是最精确的那条。另外我遇到过有人把静态资源目录直接 permitAll 了但项目里实际用的是非标准路径导致 CSS、JS 加载不出来被登录页拦了。解决方法是打开日志级别为 DEBUG看看每个请求到底进入哪些过滤器Security 在日志里会打印匹配规则跟着日志走基本都能定位。5.3 密码加密格式问题导致无法登录有时候用户表里保存的密码是 {noop}123456这看起来像是一个带前缀的编码格式实际上这是 Spring Security 的 DelegatingPasswordEncoder 支持的编码标记。不同前缀对应不同算法比如 {bcrypt}、{noop}、{pbkdf2}。如果你用 BCryptPasswordEncoder 作为唯一的 PasswordEncoder而数据库里存的密码前缀不是 {bcrypt}那校验就会失败。老项目里很多密码是最初用 MD5 或明文存的迁移到 Spring Security 时如果没有加上对应的编码器历史密码会全部失效。我见过一种顺手的迁移方案在 UserDetailsService 中先判断密码前缀如果不匹配当前算法就在认证成功后用一个更新逻辑自动把密码升级成新算法。不过这个操作一定要在认证确认通过之后再执行不能把迟到的新密码密文写回库里。5.4 过滤器链不生效自定义过滤器没执行自定义过滤器通常有两种方式继承 OncePerRequestFilter 写成普通 Filter 注册到 Servlet 容器或者继承 OncePerRequestFilter 注册到 Spring Security 过滤器链中。很多人把第一类过滤器加进来了却发现它在 Security 过滤器之前或之后执行不受 Security 的认证上下文影响。如果你需要拿到当前登录用户的信息你的过滤器就必须加到整个过滤器链内部靠后的位置比如在 UsernamePasswordAuthenticationFilter 之后调用 addFilterAfter。自定义过滤器里如果要获取用户信息不能直接 new 一个 Authentication 放到 SecurityContextHolder需要确保该上下文在整个链路中是线程绑定的。实际上 SecurityContextHolder 默认使用 ThreadLocalWeb 请求中通常没问题但如果你在子线程里异步处理上下文不会自动传递。遇到子线程取不到登录用户的问题可以考虑使用 DelegatingSecurityContextExecutor 包装线程池或者显式传参。5.5 常见面试追问速查Spring Security 相关的问题在面试里常常会连续追问。面试官如果问过滤器链的顺序你要能说出关键节点的名称和职责如果问认证流程你要能画出 AuthenticationManager 和 Provider 的关系如果问授权模型你要能解释 Authority 和 Role 的差异。还有一个高频问题登录成功后 Session 是何时创建的SecurityContext 是什么时候持久化的。答案是在认证成功后SecurityContextPersistenceFilter 在响应提交前将 SecurityContext 保存到 Session 中。你如果能把这里面的细节讲清楚面试官通常会觉得你不是背八股。我在实际和候选人对谈的时候发现很多人熟悉注解和配置但对底层模型说不清楚。我会建议这类朋友去把 Spring Security 源码里的过滤器链相关核心类翻一下不必所有细节都读把关键 6 个过滤器的走向看明白就够了比刷十道面经题都管用。6. 写在最后的个人体会说了这么多其实我想表达的是Spring Security 的学习曲线虽然有点陡但它值得投入时间。很多看起来“麻烦”的配置背后都是安全工程多年实践下来的最佳实践。你甚至可以把学到的模型用在其他语言和框架上因为认证授权的通用逻辑是相通的只是 API 不同。比如行级权限、接口防刷、分布式会话、安全响应头这些话题本质上都和 Spring Security 的安全上下文与过滤器机制息息相关理解了底层做扩展的时候会很有底。我自己带项目的时候有个习惯新模块的权限设计永远先画一张简单的表格列清楚哪些接口是匿名可访问的、哪些是需要登录的、哪些是特定角色才能操作的然后再去对应 Spring Security 的配置。这个习惯帮我避免了很多线上事故也推荐你试试。最后再顺便提一句如果你正在准备 Java 方向的技术考核建议把 Spring Security 里“认证流程、授权模型、过滤器链、Session 管理”这四个主题刻意练习到能讲清楚原理的程度收获会比单纯看文档大得多。这套体系对齐的其实不只是面试而是真实项目里的安全素养。