运行时动态的开关 Spring Security

原创
2018/12/18 14:58
阅读数 3.8K

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 可以向我反馈,我会尽快修复。

 

 

展开阅读全文
加载中

作者的其它热门文章

打赏
2
18 收藏
分享
打赏
4 评论
18 收藏
2
分享
返回顶部
顶部