新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Security原理深挖:手搓过滤器链搞定认证与授权

发布时间:2026/9/26 11:37:48来源:尧图网络
Spring Security原理深挖:手搓过滤器链搞定认证与授权
1. 手搓之前先拆穿Spring Security的底裤如果你只用过Spring Security的配置类可能会觉得这东西像个黑盒一引入依赖登录页有了密码校验有了接口保护有了。出了问题之后报错日志一大堆根本不知道哪一层拦截了你。我最初学安全框架时也有这个感觉直到自己动手做了一个极简版本才彻底搞明白它到底在做什么。先说结论Spring Security本质上就是一段Servlet过滤器链只不过它把“认证”、“授权”、“防攻击”这些事拆成了很多个小Filter按顺序一个个执行。你配置的SecurityFilterChain说人话就是“你允许哪些过滤器参与、按什么顺序、放行哪些路径”。这套东西完全基于Servlet的Filter机制脱离Spring Boot也能跑所以想“手搓一个Spring Security”第一步就是理解过滤器链而不是去看那些花里胡哨的注解。1.1 Spring Security的本质是一串Filter不是魔法Servlet容器收到一个请求后会按照注册顺序依次调用Filter每个Filter可以选择放行、直接返回响应或者先处理再放行。Spring Security就是往这个链路里塞了十几个Filter比如UsernamePasswordAuthenticationFilter负责处理表单登录FilterSecurityInterceptor负责做最终的授权判断。手搓版本不需要那么多但要抓住最核心的三个职责从请求里提取身份凭证Token、用户名密码、Cookie等校验凭证并把身份信息放进上下文请求进入业务方法之前判断这个身份有没有权限这三件事对应到代码里就是一个认证过滤器、一个上下文持有器、一个权限拦截器。搞清楚这三样Spring Security的主干就摸透了。1.2 认证、授权、上下文三件套缺一不可很多人把认证和授权混在一起其实它们是两个阶段。认证是“你是谁”授权是“你能做什么”。两者之间需要传递一个载体也就是SecurityContext里的Authentication对象。类比一下认证就是刷门禁卡看你是不是公司员工授权是进到楼里之后看你的门禁权限能不能打开某个办公室。Spring Security用Authentication对象同时装这两个信息它里面有principal身份主体、credentials凭证一般存原始密码或Token、authorities权限列表。手写迷你版时我也照这个思路设计登录接口校验用户名密码之后构造一个AuthUser对象塞进ThreadLocal同时带一个权限集合后续的拦截器从ThreadLocal里取人判断权限。难点不在这些概念而在过滤器的顺序和放行规则这才是坑最多的地方。2. 手搓一个能用的认证过滤器我选择先实现一个最简的Token认证过滤器原因很简单它不依赖Session逻辑清晰适合讲原理。真实项目中用JWT也是一样的套路只是加密验证细节更多。2.1 从最简单的Token认证开始我的极简方案长这样客户端登录后服务端发一个随机UUID当Token存内存Map里后续请求在Header里带上Token。认证过滤器负责查这个Token查不到就代表未登录。public class TokenAuthenticationFilter implements Filter { private final TokenStore tokenStore; public TokenAuthenticationFilter(TokenStore tokenStore) { this.tokenStore tokenStore; } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest (HttpServletRequest) request; HttpServletResponse httpResponse (HttpServletResponse) response; String token httpRequest.getHeader(X-Auth-Token); if (token ! null tokenStore.contains(token)) { AuthUser user tokenStore.getUser(token); // 把用户放进上下文供后续使用 SecurityContextHolder.setCurrentUser(user); } chain.doFilter(request, response); // 请求结束后清理防止ThreadLocal泄漏 SecurityContextHolder.clear(); } }关键点是chain.doFilter执行完之后一定要清理当前线程的上下文。我曾经在线程池环境下忘了做清理结果高并发时A用户拿到了B用户的数据这种事故排查起来极其痛苦。2.2 把SecurityContext存起来还要解决跨线程问题ThreadLocal看起来简单但真实项目里容易踩雷。比如说你在一个Filter里放入了用户到了Service层想拿用户信息用的却是异步线程那ThreadLocal就取不到了。Spring Security的SecurityContextHolder默认也基于ThreadLocal但它提供了两种模式MODE_THREADLOCAL和MODE_INHERITABLETHREADLOCAL。后者可以让子线程继承父线程的上下文但也有并发覆盖的风险。实际用例中RPC调用、MQ消费这类场景我建议把用户ID显式传参不要依赖上下文传递。我在手搓版本里也故意做成单线程模型并留下一个清晰接口方便以后替换成别的实现public class SecurityContextHolder { private static final ThreadLocalAuthUser CONTEXT new ThreadLocal(); public static void setCurrentUser(AuthUser user) { CONTEXT.set(user); } public static AuthUser getCurrentUser() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }这个类虽然简单但它很好地反映了Spring Security的核心抽象。你甚至可以把这个类理解成“全局变量”只是这个全局变量是线程隔离的。在处理每个请求时过滤器负责往里面塞内容业务代码负责取内容最后过滤器再负责清空。2.3 放行规则与异常处理最容易出错的地方很多人写安全框架时第一个坑就是登录接口也被拦截了。这就像一个门禁系统把“进门本身”也当成需要门禁才能做的操作逻辑上说不通。所以放行规则必须放在过滤器链的最前面处理。我在TokenAuthenticationFilter里加了一个放行名单private final ListString permitAllPath List.of(/login, /public, /health); Override public void doFilter(...) { String path httpRequest.getRequestURI(); if (permitAllPath.stream().anyMatch(path::startsWith)) { chain.doFilter(request, response); return; } // 其余逻辑... }注意这里的坑permitAll不等于“不经过过滤器”。真正的Spring Security里放行只是跳过“认证要求”过滤器链还是会继续执行。所以我在手搓版里也保持这个语义只是跳过认证判断而不是直接把请求放给Servlet。异常处理也要提防。过滤器抛出AuthenticationException后不能直接就500应该转成401状态码而且要确保响应格式统一。我在过滤器外层包了一个try-catchtry { // 认证逻辑 } catch (AuthenticationFailedException e) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.setContentType(application/json;charsetUTF-8); httpResponse.getWriter().write({\message\:\未登录或登录已过期\}); return; }不这么做的后果是前端的axios统一报错捕获不到401反而拿到一个Html的500页面联调时非常难受。3. 授权模仿PreAuthorize的拦截逻辑认证做完接着是授权。Spring Security的授权有两种常用方式基于URL的和基于注解的。手搓版本里我选择实现一个简化版RequireRole注解因为它最直观地体现了“在方法执行前做检查”的思想。3.1 在拦截器里做权限判断要拦截方法注解第一反应是用Spring AOP。思路是写一个切面拦截所有带有RequireRole注解的方法在方法执行前取出当前用户再看用户权限列表里有没有要求的角色。Aspect Component public class PermissionAspect { Before(annotation(requireRole)) public void checkRole(JoinPoint joinPoint, RequireRole requireRole) { AuthUser currentUser SecurityContextHolder.getCurrentUser(); if (currentUser null) { throw new ForbiddenException(未登录); } String requiredRole requireRole.value(); boolean hasRole currentUser.getRoles().contains(requiredRole); if (!hasRole) { throw new ForbiddenException(缺少角色: requiredRole); } } }这个切面其实复刻了Spring Security的PreAuthorize的核心思路。PreAuthorize支持SpEL表达式能写hasRole(admin)这种复杂判断我们手搓版本不需要那么强大但原理是一样的在方法执行前拿到当前身份、判断权限、没有就抛异常。3.2 让注解真正生效还要考虑优先级只是加一个切面还不够需要明确这个切面应该在哪一层执行我建议放在Controller层之前或之后一段时间比如直接切在RestController的方法上。如果切在Service层弊端是权限检查会晚于参数校验一部分流程而且一个Controller方法调用多个Service方法时权限语义会分散。我自己的经验是权限注解放在Controller方法上因为它是面向接口粒度的正好对应“某个URL需要什么角色”。这样做也天然契合前后端交互模型一个请求就是一个动作。还有一个细节如果有多个校验类注解比如ValidateToken和RequireRole它们的执行顺序可能影响结果。可以通过Order来控制但更稳妥的办法是不要重复发明轮子直接在一个注解里表达“登录且拥有角色”。3.3 RBAC模型的精简实现完整版RBAC基于角色的访问控制涉及五张表用户表、角色表、权限表、用户角色关联表、角色权限关联表。手搓版本不用那么重的表结构用两张简化表就能跑用户表id、username、password、roles角色权限表role、permission在内存模型里我直接给AuthUser加一个roles属性登录时查询用户表把角色列表塞进去。判断权限时先找角色再通过角色找权限。public class AuthUser { private Long id; private String username; private SetString roles; private SetString permissions; // getter setter 略 }这样在切面里可以做两级判断if (requireRole.value() ! null) { // 检查角色 } if (requirePermission.value() ! null !currentUser.getPermissions().contains(requirePermission.value())) { throw new ForbiddenException(缺少权限: requirePermission.value()); }实际项目中权限粒度做到“接口”级就够了不用“按钮”级否则维护成本会指数级上升。这是我做了多个后台系统后的体会。4. 注册过滤链与适配Spring Boot 3过滤器写好了切面写好了怎么让它跑起来在Spring Boot里只需要把过滤器注册成Bean。但这里有个版本差异特别值得说就是Spring Boot 3的配置迁移问题。4.1 用FilterRegistrationBean接入Servlet容器最传统的做法是定义FilterRegistrationBean手动指定过滤器顺序和URL模式Configuration public class SecurityConfig { Bean public FilterRegistrationBeanTokenAuthenticationFilter tokenFilterRegistration(TokenStore tokenStore) { FilterRegistrationBeanTokenAuthenticationFilter registration new FilterRegistrationBean(); registration.setFilter(new TokenAuthenticationFilter(tokenStore)); registration.addUrlPatterns(/*); registration.setOrder(1); return registration; } }Order很关键。如果手搓过滤器是1Spring Security自身的过滤器链可能是-100那你自己过滤器的位置就在Spring Security之后导致一些请求还没到你的认证逻辑就被Spring Security拒了。如果你根本没引入Spring Security就没这个问题一旦同时存在要仔细核对顺序。我调试时发现最简单的方式是给过滤器一个较小的Order比如-200让它靠前执行。但注意太靠前可能导致静态资源全部走一遍认证逻辑虽然也能放行但性能上不划算。最好还是用路径匹配来控制。4.2 Spring Boot 3配置迁移的坑从WebSecurityConfigurerAdapter说起很多老项目从Spring Boot 2升级到Spring Boot 3最大的坑就是WebSecurityConfigurerAdapter过时。Spring Security 5.7.0起就标记过时6.0里直接移除。所以Spring Boot 3时代安全配置要写成一个SecurityFilterChain的Bean完全用Lambda DSL风格。这是老写法Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/login).permitAll() .anyRequest().authenticated() .and() .formLogin(); } }这是新写法Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/login, /public).permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } }我最早自己迁的时候照着老教程写发现antMatchers方法直接报错因为新版本改成了requestMatchers。and()链式也废弃了必须用Lambda风格配置每一块。这不仅是语法变化更是一种思维切换老版中每个配置块都依赖and()来拼接新版中每个配置项接收一个自定义器更清晰不容易把配置顺序搞乱。4.3 配置类里如何定义安全规则兼谈过滤器放行与security放行的区别你可能会问既然我已经手搓了过滤器还需要Spring Security的SecurityFilterChain吗如果是我为了演示会先关掉Spring Security只用自己的过滤器。但生产环境中同时存在的情况很常见比如你引入spring-boot-starter-security后Boot会自动配置默认的SecurityFilterChain它会把所有请求都拦下来。此时的解决思路不是去删依赖而是用Spring Security提供的能力来管理请求级权限同时让它“放行”你手搓过滤器需要处理的路径。这里有个常见误区在Spring Security里配置permitAll()只是说明Spring Security不做认证授权你的手搓过滤器还是会跑。反过来如果你手搓过滤器也没放行请求照样会被拦下来。两者是叠加关系。我实际调试中遇到过这种场景/api/public在Spring Security配置里permitAll了但浏览器请求还是被自己写的TokenAuthenticationFilter拦截。排查了半天才发现我自己的过滤器是按全局/*注册的它在Spring Security之前接管了所有请求。最后把那几个公共路径也加入手搓过滤器的放行列表才解决。所以这里需要想清楚你引入Spring Security是为了利用它成熟的FilterChain机制而不是为了对抗它。如果你要手搓一个学习版建议单独建一个demo工程不要跟Spring Security混在一起否则你会被“谁拦截了谁”的问题绕晕。5. 常见问题与排查技巧实录手搓安全框架的过程其实是一个排查问题的过程。下面整理几个我踩过的坑每个都很典型。5.1 过滤器没执行八成是Order问题明明注册了FilterRegistrationBean但请求就是不走过滤器。检查点有三个Bean是否被扫描到有没有加Configuration或ComponentURL Pattern是否匹配/*和/含义不同/*匹配所有内容/匹配根路径Order是否被其他过滤器覆盖实战里最常见的是项目里有多个过滤器你的Token过滤器Order是2结果排在它前面Order是1的过滤器直接抛异常返回了请求根本到不了你的过滤器。看日志时不要只看有没有你的Filter执行还要看有没有别的Filter短路了。5.2 CSRF和Session固定攻击的简化处理Spring Security默认开启CSRF保护会在表单提交时校验一个隐藏Token。手搓版不需要照搬全部机制但要理解真正的项目如果是前后端分离且使用JWTCSRF风险相对较小因为Token放在Header里攻击者很难构造请求头但如果用Cookie做认证就必须要防CSRF。我在手搓Demo里没有加CSRF但特意留了一个接口注释“生产环境必须处理CSRF和Session固定攻击”。对于Cookie方案最简单的防护是每次登录后更换SESSIONID避免攻击者提前种下一个已知Session。这就像你家里的锁被撬过一次后换一把新锁而不是继续用同一把。5.3 密码存储别再用明文BCrypt才是老朋友手搓安全框架时总有人图方便把密码存明文。我可以明确说正式项目这么做等于裸奔。Spring Security自带BCryptPasswordEncoder它是一种带随机盐的哈希算法同一密码每次加密结果都不一样而且计算成本可控暴力破解成本很高。如果你在手搓版本里也要做密码校验不要自己发明加密算法直接引用spring-security-cryptoBCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String hash encoder.encode(password123); boolean matches encoder.matches(password123, hash);这里有个细节BCryptPasswordEncoder生成的字符串长度固定60位每次都不一样。你在数据库建字段时长度要大于60位否则入库被截断等查询时发现匹配不上那是性能问题和数据问题的双重折磨。5.4 排查SecurityContext被覆盖的问题有一次Demo上线后有用户反馈A账号调接口返回的却是B账号的数据。最后排查出原因是由于我在异步线程里做业务逻辑父线程的SecurityContextHolder没被子线程继承子线程里重新从数据库查用户信息时查询条件写成了静态变量上的登录用户ID导致并发状态下互相覆盖。真实项目里有一个非常实用的技巧进入异步线程前先把用户ID用局部变量保存好传入子线程不要在子线程里依赖任何上下文。这虽然“不够安全框架”但非常安全工程。5.5 从日志视角看整个请求路径最后推荐一下排查思路手搓安全框架后你最好在过滤器里加一行日志输出请求路径、是否放行、当前用户。比如log.info( [AuthFilter] path{}, token{}, user{}, result{}, path, token, username, result);这样一来当一个请求被莫名其妙的401或403拦截时打开日志看自己的过滤器日志和Spring Security的过滤器日志基本就能定位是哪一个环节出了问题。合理的日志是全链路排查的基础这一点比写100行安全配置还重要。6. 手搓之后对Spring Security的理解就不一样了个人体会是手搓一遍之后你再回去看Spring Security的官方文档很多原来抽象的概念都会具体化。比如它为什么默认有十三条过滤器为什么顺序不能乱为什么SecurityContext要在请求结束后清理这些在我实现过一个迷你版后全都通了。后续想继续扩展的话可以按这几个方向做给这个迷你框架加上“记住我”功能或者支持Session方式认证又或者把权限判断从角色升级成细粒度的权限码。每加一个功能你就越接近Spring Security的一小块能力。最终你会发现Spring Security并没有想象中那么神秘它只是把很多安全细节打磨得极其扎实而已。如果你也在用Spring Boot 3做项目建议先把手头老旧的WebSecurityConfigurerAdapter配置迁到新风格感受一下Lambda DSL的清晰度再决定要不要继续手搓。毕竟理解原理的目的是更好地用手里的工具不是为了重复造轮子。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

DeepSeek-Harness:CLI与Web UI双入口实操Agent开发 2026/9/26 13:56:22

DeepSeek-Harness:CLI与Web UI双入口实操Agent开发

上一篇文章把 Harness 和 Agent 的区别掰扯清楚了,很多朋友看完还是觉得差点意思:概念懂了,下一步怎么跑起来?这次直接从 DeepSeek-Harness 最常用的两个入口讲起——CLI 和 Web UI。一个是纯命令行操作,适合脚本化、自…

阅读更多 →
iVentoy 批量装机实战:PXE 网络启动部署与自动化配置指南 2026/9/26 13:56:22

iVentoy 批量装机实战:PXE 网络启动部署与自动化配置指南

1. 为什么我最终选择了 iVentoy 做批量装机 机房上架新机器,最烦的从来不是硬件安装,而是装系统。十几台甚至几十台机器,一台一台插U盘、选启动项、点下一步,一天下来人直接废掉。我最早用的是传统 PXE 方案,配 DHCP、…

阅读更多 →
Matlab实现正则化逻辑回归:微芯片质检分类完整实战 2026/9/26 13:56:22

Matlab实现正则化逻辑回归:微芯片质检分类完整实战

芯片一条产线跑下来,良率就是生命线。我在实际项目里用Matlab做过不少分类预测的活,正则化逻辑回归在微芯片质检这种“维度不高、样本不大、但噪声不小”的场景里,反而比一堆花里胡哨的集成模型更稳、更可解释。这套流程不光能跑通实验数据&a…

阅读更多 →
基于Java+SSM+Flask的高校就业管理系统设计与实现 2026/9/26 13:56:22

基于Java+SSM+Flask的高校就业管理系统设计与实现

毕业设计选“高校就业管理系统”的同学,这两年肉眼可见地多起来了。基本上每个学校和学院都在催就业数据,加上每年毕业季前老师都要统计就业率、学生要投简历、企业要来校招,这套系统的需求量一直很稳。而“基于JavaSSMFlask高校就业管理系统…

阅读更多 →
Laya-CoreML 如何把Transformer送上Neural Engine:BC1L激活、1×1投影与逐头注意力的ANE图重写 2026/9/26 13:56:22

Laya-CoreML 如何把Transformer送上Neural Engine:BC1L激活、1×1投影与逐头注意力的ANE图重写

Laya-CoreML 如何把Transformer送上Neural Engine:BC1L激活、11投影与逐头注意力的ANE图重写 【免费下载链接】laya-coreml Local Laya typed decisions on Apple Core ML and Neural Engine. Validated ports, ~5 ms short decisions on M3 Max, reproducible spee…

阅读更多 →
Ubuntu 上 PlantUML 安装与序列图语法实战:TaoToken 统一 Key 配置 settings.json 骨架 2026/9/26 13:56:10

Ubuntu 上 PlantUML 安装与序列图语法实战:TaoToken 统一 Key 配置 settings.json 骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉