运行时动态的开关 Spring Security
1. 为什么要在运行时动态的开关 Spring Security?
考虑这样一个场景,当我们构建了一整套微服务架构的系统后,公司某个内部的老系统也感受到了微服务架构的好处,包括实时监控,限流,熔断,高可用的机制等等,老系统的开发人员也希望能减少自己的一些工作量,所以他们系统将老系统加入到我们的微服务架构体系中来。这样就产生了一些适配,兼容性问题,如果让老系统来完全适配已经构建好的微服务架构体系那么老系统改动的代价就比较大,包括技术的升级,开发人员的学习成本提高,测试问题,还有老系统还有一些不断的新需求要开发。比较理想的解决方案是对老系统的改动越小越好,最好能做到无缝集成,已经构建好的微服务架构来为老系统的集成提供支持。比如说老系统原本有自己的认证,授权控制,使用了 Spring Security ,在微服务架构中我们将认证,授权的工作统一放在了 API 网关层去处理。这样就和老系统的集成产生了冲突。于是我就需要让 API 网关路由到老系统上的请求不经过老系统自身的认证、授权流程,也可以正常访问。同时也不能破坏当不通过 API 网关访问时老系统的认证、授权流程也要能正常工作。所以这是我要达到的目的。
2. Spring Security 在 Web 项目中是如何工作的
这是我在网上找的一张图,目的就是为了大概说明问题。 Spring Web Security 的核心功能都是在这一条过滤器链上完成的。具体可以参考这个类 :
org.springframework.security.config.annotation.web.builders.FilterComparator , 这个类中定义了所有的 Spring Security 的过滤器以及他们的顺序。
搞清楚这一点,我就有一个想法,既然我想要关闭 Spring Security 不让他起作用 ,那我不让请求经过这些过滤器不就可以了么。
FilterComparator 源码 :
private static final int STEP = 100; private Map<String, Integer> filterToOrder = new HashMap<String, Integer>(); FilterComparator() { int order = 100; put(ChannelProcessingFilter.class, order); order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; put(WebAsyncManagerIntegrationFilter.class, order); order += STEP; put(SecurityContextPersistenceFilter.class, order); order += STEP; put(HeaderWriterFilter.class, order); order += STEP; put(CorsFilter.class, order); order += STEP; put(CsrfFilter.class, order); order += STEP; put(LogoutFilter.class, order); order += STEP; put(X509AuthenticationFilter.class, order); order += STEP; put(AbstractPreAuthenticatedProcessingFilter.class, order); order += STEP; filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order); order += STEP; put(UsernamePasswordAuthenticationFilter.class, order); order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; filterToOrder.put( "org.springframework.security.openid.OpenIDAuthenticationFilter", order); order += STEP; put(DefaultLoginPageGeneratingFilter.class, order); order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; put(DigestAuthenticationFilter.class, order); order += STEP; put(BasicAuthenticationFilter.class, order); order += STEP; put(RequestCacheAwareFilter.class, order); order += STEP; put(SecurityContextHolderAwareRequestFilter.class, order); order += STEP; put(JaasApiIntegrationFilter.class, order); order += STEP; put(RememberMeAuthenticationFilter.class, order); order += STEP; put(AnonymousAuthenticationFilter.class, order); order += STEP; put(SessionManagementFilter.class, order); order += STEP; put(ExceptionTranslationFilter.class, order); order += STEP; put(FilterSecurityInterceptor.class, order); order += STEP; put(SwitchUserFilter.class, order); }
3. Spring Security 的过滤器链是如何工作的
3.1 Spring Security 过滤器链是什么 ?
通过 debug 调试 可以发现在 Spring Security 提供的过滤器中使用的 FilterChain 的实际类型是这个类 : org.springframework.security.web.FilterChainProxy.VirtualFilterChain 。它实现了 FilterChain 接口。
3.2 Spring Security 过滤器链初始化
通过搜索可以找到过滤器链条是在这个函数中进行初始化的 : org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild 源码:
@Override protected Filter performBuild() throws Exception { Assert.state( !securityFilterChainBuilders.isEmpty(), "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke " + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"); int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>( chainSize); for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; if (debugEnabled) { logger.warn("\n\n" + "********************************************************************\n" + "********** Security debugging is enabled. *************\n" + "********** This may include sensitive information. *************\n" + "********** Do not use in a production system! *************\n" + "********************************************************************\n\n"); result = new DebugFilter(filterChainProxy); } postBuildAction.run(); return result; }
org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain 源码:FilterChainProxy 本身也是一个过滤器这个过滤器会被注册到过滤器链上。然后这个过滤器内部封装了 Spring Security 的过滤器链条。
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); } return webSecurity.build(); }
3.3 Spring Security 过滤器链的工作过程
org.springframework.security.web.FilterChainProxy#doFilter 源码 :
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else { doFilterInternal(request, response, chain); } }
org.springframework.security.web.FilterChainProxy#doFilterInternal 源码 : 这个函数是 Spring Security 过滤器链条的执行入口。每次请求都会 new 一个 VirtualFilterChain 的实例对象,然后调用该对象的 doFilter 函数,于是请求就进入到 Spring Security 的过滤器链处理中。
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); }
org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter 源码 :这里就是去挨个调用 Spring Security 的过滤器的过程 ,重点需要关注的是 originalChain (原始的过滤器链条也就是 servlet 容器的) , currentPosition (spring security 过滤器链当前执行到的位置) , size (spring security 过滤器链中过滤器的个数) 。 当 currentPosition == size 的时候也就意味着 spring security 的过滤器链条执行完了,于是就该使用原始的 originalChain 继续去调用 servlet 容器中注册的过滤器了。
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (currentPosition == size) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } // Deactivate path stripping as we exit the security filter chain this.firewalledRequest.reset(); originalChain.doFilter(request, response); } else { currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " at position " + currentPosition + " of " + size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); } nextFilter.doFilter(request, response, this); } } }
3.4 运行时跳过 Spring Security 过滤器链的思路
了解上面讲的 spring security 过滤器链的执行过程后如何跳过spring security 的过滤器链就显而易见了 , 只需要控制 org.springframework.security.web.FilterChainProxy.Virt ualFilterChain 对象中的 currentPosition == size 就可以了。但是 org.springframework.security.web.FilterChainProxy.VirtualFilterChain 这个类是内部私有的静态成员类。 Spring Security 的目的就是为了封装它将它隐藏起来,想想也可以理解毕竟这是它之所以能实现功能的核心,肯定不希望被乱动。但是没办法为了实现我的需求,我还是要乱动它,想要改变它的话就只有通过反射的方式才能实现。
3.4.1 关于反射的小技巧
Class.forName("org.springframework.security.web.FilterChainProxy.VirtualFilterChain"); 使用这行代码的时候是不能成功获取到 VirtualFilterChain 类的 Class 对象的。因为 Java 中内部类在编译成 .class 文件后名称是这样的 FilterChainProxy$VirtualFilterChain 。想要成功获取到这个内部类的 Class 对象的话需要这样写 Class.forName("org.springframework.security.web.FilterChainProxy$VirtualFilterChain");
3.5 运行时跳过 Spring Security 过滤器链的实现方式
首先我自定义了一个过滤器并且把它加入到 Spring Security 过滤器链条中的最前面,因为我的目的是完全的“关闭”掉spring security 的过滤器链。示例代码 :通过反射的方式改变 currentPosition 的值即可。
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (! isSkipOver(request)) { filterChain.doFilter(request , response); return; } Class<? extends FilterChain> filterChainClass = filterChain.getClass(); try { Class<?> virtualFilterChainClass = Class.forName("org.springframework.security.web.FilterChainProxy$VirtualFilterChain"); if (virtualFilterChainClass.isAssignableFrom(filterChainClass)) { Reflect reflect = Reflect.on(filterChain); Object size = reflect.field("size").get(); reflect.set("currentPosition" , size); } } catch (Throwable t) { throw new ApplicationRuntimeException(t); } filterChain.doFilter(request , response); }
4. 广告
如果你也面临到我所说的类似问题,需要编写一些代码来解决你的问题的话,那么上面这些代码已经不需要你再去花时间编写了,我已经写好了。你只需要通过 maven :
<dependency>
<groupId>org.hepeng</groupId>
<artifactId>hp-java-commons</artifactId>
<version>1.1.3</version>
</dependency>
或者是 gradle : implementation 'org.hepeng:hp-java-commons:1.1.3'
再自定义一个 Filter 并且继承 org.hepeng.commons.spring.security.web.filter.SkipOverSpringSecurityFilterChainFilter 即可。它已经经过了我的多次测试可以正常工作,如果你发现任何 bug 可以向我反馈,我会尽快修复。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
原生JavaScript进行前后端同构
什么是前后端同构 明确三个概念:「后端渲染」指传统的 ASP、Java 或 PHP 的渲染机制;「前端渲染」指使用 JS 来渲染页面大部分内容,代表是现在流行的 SPA 单页面应用;「同构渲染」指前后端共用 JS,首次渲染时使用 Node.js 来直出 HTML。一般来说同构渲染是介于前后端中的共有部分。 感觉前端的确是折腾,之前还在流行前后端分离,现在怎么又要做前后端同构了? 原因是现在流行的SPA前端单页面应用比较沉重,首次访问需要加载文件较多,第一次加载过慢,用户需要等待前端进行渲染页面。而且不利于SEO及缓存,并且有一定的开发门槛。 前后端同构通过复用模板和JS文件,让一份代码可以同时跑在服务器和浏览器,首次渲染使用nodejs渲染页面,之后使用SPA路由跳转。可以有效减少用户首次访问的等待时间,并且对SEO比较友好,也便于缓存。 项目简介 本前后端同构项目主要分为两个部分,一个是基于koa2的渲染服务器,另一个是基于原生JS和zepto的前端SPA。 项目的特点是不使用vue和react等框架,门槛低,开发速度快,便于上手,比较轻巧,核心的router部分只有一百行左右的代码...
- 下一篇
美团即时物流的分布式系统架构设计
背景 美团外卖已经发展了五年,即时物流探索也经历了 3 年多的时间,业务从零孵化到初具规模,在整个过程中积累了一些分布式高并发系统的建设经验。最主要的收获包括两点: 即时物流业务对故障和高延迟的容忍度极低,在业务复杂度提升的同时也要求系统具备分布式、可扩展、可容灾的能力。即时物流系统阶段性的逐步实施分布式系统的架构升级,最终解决了系统宕机的风险。围绕成本、效率、体验核心三要素,即时物流体系大量结合 AI 技术,从定价、ETA、调度、运力规划、运力干预、补贴、核算、语音交互、LBS 挖掘、业务运维、指标监控等方面,业务突破结合架构升级,达到促规模、保体验、降成本的效果。 本文主要介绍在美团即时物流分布式系统架构逐层演变的进展中,遇到的技术障碍和挑战: 订单、骑手规模大,供需匹配过程的超大规模计算问题。遇到节假日或者恶劣天气,订单聚集效应,流量高峰是平常的十几倍。物流履约是线上连接线下的关键环节,故障容忍度极低,不能宕机,不能丢单,可用性要求极高。数据实时性、准确性要求高,对延迟、异常非常敏感。 美团即时物流架构 美团即时物流配送平台主要围绕三件事展开:一是面向用户提供履约的 SL...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Mario游戏-低调大师作品
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- 2048小游戏-低调大师作品
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,CentOS8安装Elasticsearch6.8.6