1. 为什么要实现动态的获取antMatchers 配置的数据
这两天由于公司项目的需求,对 spring security 的应用过程中需要实现动态的获取 antMatchers ,permitAll ,hasAnyRole ,hasIpAddress 等这些原本通过硬编码的方式配置的数据。为了让每一个业务服务不用再去处理权限验证等这些和业务无关的逻辑,而是只专注于它所负责的业务,就要将认证、授权统一的放在 API 网关层去处理。但是每个不同的业务服务有的接口需要认证后才能访问,有的接口是不需要认证就可以访问的,有的接口可能是需要某些权限、角色才可以访问。这样依赖 API 网关就必须知道并且能够区分出来每个业务服务的接口哪些是需要认证后才可以访问的,那些接口是不需要经过认证就可以访问的。 为了实现这个功能 spring security 提供的 antMatchers 函数硬编码的方式就不适用了。而是应该提供一个管理端,每个业务服务把他们这些个性化的接口通过管理端去进行配置,统一的存储起来,spring security 在获取这些数据的时候从统一的存储中来获取这些数据。基于这个需求前提我来考虑如何实现这个功能。配套视频讲解地址 : http://www.iqiyi.com/w_19s456x5b5.html?pltfm=11&pos=title&flashvars=videoIsFromQidan%3Ditemviewclk_a#vfrm=5-6-0-1
2. 从 Spring Security 框架中找到适合实现该功能的切入点
想要找个框架的切入点必须对框架如何工作,源码要熟悉,不然很难找到一个合适的切入点。有点见缝插针的意思,首先就需要找到一个适合“插针”的位置。
2.1FilterSecurityInterceptor
FilterSecurityInterceptor 过滤器是 Spring Security 过滤器链条中的最后一个过滤器,它的任务是来最终决定一个请求是否可以被允许访问。
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke 函数源码:这个函数中做了调用下一个过滤器的操作,也就是这行代码fi.getChain().doFilter(fi.getRequest(), fi.getResponse()) 。因为FilterSecurityInterceptor 是Security 过滤器链条中的最后一个过滤器,再去调用下一个过滤器就是调用原始过滤器链条中的下一个过滤器了,这也就意味着请求是被允许访问的。但是在调用下一个过滤器之前还有一行代码 ,InterceptorStatusToken token = super.beforeInvocation(fi); 这一行代码就会决定本次请求是否会被放行。
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation 函数源码:这个函数做的事情大致是对这次请求是禁止访问还是允许访问进行投票,如果投票都通过的话就允许访问,如果有一票反对就会禁止访问抛出异常结束后续处理流程。投票的依据就是通过这行代码
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); 获取到的。这行代码也就是我实现功能的切入点。它先获取了一个SecurityMetadataSource 对象,然后通过这个对象获取了投票的依据。 我的思路就是自定义SecurityMetadataSource 类的子类,来替换掉FilterSecurityInterceptor 中的SecurityMetadataSource 实例。
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
2.2替换FilterSecurityInterceptor 中的SecurityMetadataSource 实例
我的目的是替换掉FilterSecurityInterceptor 中的SecurityMetadataSource 实例 , 而不是去替换掉原有的 FilterSecurityInterceptor , 如果要替换掉原有的FilterSecurityInterceptor 那么工作量就变大了,所以替换掉原有的FilterSecurityInterceptor 并不是一个好的选择。首先我需要找到FilterSecurityInterceptor 对象是在什么时候被实例化的。通过使用代码搜索找到 FilterSecurityInterceptor 的实例化位置:org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer#createFilterSecurityInterceptor , 也是在这个函数中SecurityMetadataSource 对象被设置。
private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,
FilterInvocationSecurityMetadataSource metadataSource,
AuthenticationManager authenticationManager) throws Exception {
FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
securityInterceptor.setSecurityMetadataSource(metadataSource);
securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));
securityInterceptor.setAuthenticationManager(authenticationManager);
securityInterceptor.afterPropertiesSet();
return securityInterceptor;
}
createFilterSecurityInterceptor 函数被调用的位置在 :org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer#configure 。这里关键的一行代码是 :securityInterceptor = postProcess(securityInterceptor);
@Override
public void configure(H http) throws Exception {
FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
if (metadataSource == null) {
return;
}
FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
http, metadataSource, http.getSharedObject(AuthenticationManager.class));
if (filterSecurityInterceptorOncePerRequest != null) {
securityInterceptor
.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
}
securityInterceptor = postProcess(securityInterceptor);
http.addFilter(securityInterceptor);
http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
org.springframework.sec