可能在开发过程中设置了一个匿名访问地址,但这个地址我们同时也想让它能够进行身份验证访问。就比如一个地址你把它分享出去就可以匿名访问,但如果这个地址如果没有被分享出去在系统内部或者在app中就可以进行身份验证访问。
遇到的问题Spring Security 版本:4.1.0.RELEASE、 spring-security-oauth2 为2.3.5.RELEASE。现在Spring Security已经版本到了 5.5.2 为啥不用最新的,我只能说我也想。
Spring Security中默认对匿名访问的设置是如果你当前请求的地址为匿名访问设置,并且没有携带token的话,Spring Security会在AnonymousAuthenticationFilter中的SecurityContextHolder.getContext()中设置一个Authentication对象:AnonymousAuthenticationToken,它的默认属性为:
this.key = UUID;
this.principal = "anonymousUser";
this.authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS");
但如果SecurityContextHolder.getContext()存在Authentication对象为OAuth2Authentication,AnonymousAuthenticationFilter不做任何操作,继续后面的操作,这时后面的程序会判断SecurityContextHolder.getContext()里的Authentication对象是不是AnonymousAuthenticationToken,如果不是,那不好意思Spring Security不允许你访问。
流程了解如果Authentication对象为OAuth2Authentication也能通过,我们的问题就解决了。
了解整个Spring Security 启动和匿名访问验证的流程,有几个点注意了下。
Spring Security 如何设置的匿名访问地址的SecuritymetadataSource。这个SecuritymetadataSource对应实现类为expressionbasedFilterInvocationSecuritymetadataSource,它里面调用了父类的构造函数,最终完成数据的封装。这里注意,parser.parseexpression(expression)这个方法,可以理解它是对字符串“anonymous”转换Spelexpression对象的处理。另外字符串“anonymous”的定义来源于我们的配置,也就是方法anonymous()的设置,指定匿名用户允许使用 URL:/anonymous/test。其实“anonymous”最终对应到SecurityexpressionRoot的isAnonymous()方法,在后面的代码上会有体现。
@EnableResourceServer
@Configuration
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 允许部分接口匿名访问
.antMatchers("/anonymous/test").anonymous()
.anyRequest().authenticated().and().logout();
}
}
public expressionbasedFilterInvocationSecuritymetadataSource( linkedHashMapSpring Security 如何来处理匿名访问的> requestMap, SecurityexpressionHandler expressionHandler) { // expressionHandler.getexpressionParser()为OAuth2expressionParser对象 super(processMap(requestMap, expressionHandler.getexpressionParser())); Assert.notNull(expressionHandler, "A non-null SecurityexpressionHandler is required"); } private static linkedHashMap > processMap( linkedHashMap > requestMap, expressionParser parser) { Assert.notNull(parser, "SecurityexpressionHandler returned a null parser object"); linkedHashMap > requestToexpressionAttributesMap = new linkedHashMap >( requestMap); for (Map.Entry > entry : requestMap .entrySet()) { RequestMatcher request = entry.getKey(); Assert.isTrue(entry.getValue().size() == 1, "Expected a single expression attribute for " + request); ArrayList attributes = new ArrayList (1); String expression = entry.getValue().toArray(new ConfigAttribute[1])[0] .getAttribute(); logger.debug("Adding web access control expression '" + expression + "', for " + request); // 返回AntPathMatcherevaluationContextPostProcessor对象 AbstractVariableevaluationContextPostProcessor postProcessor = createPostProcessor( request); try { // parser.parseexpression(expression)将表达式expression(“anonymous”)替换为表达式#oauth2.throwonError(anonymous) // 将表达式#oauth2.throwonError(anonymous)解析为SpelNodeImpl数组,数组[0]=VariableReference对象,数组[1]=MethodReference对象 // 变量的引用#oauth2对应VariableReference, // 方法的引用throwOnError对应MethodReference, // 方法的参数anonymous对应PropertyOrFieldReference, // PropertyOrFieldReference在MethodReference的children属性中 // 最后返回Spelexpression对象,对象的ast属性就对应上面说的SpelNodeImpl数组 attributes.add(new WebexpressionConfigAttribute( parser.parseexpression(expression), postProcessor)); } catch (ParseException e) { throw new IllegalArgumentException( "Failed to parse expression '" + expression + "'"); } requestToexpressionAttributesMap.put(request, attributes); } return requestToexpressionAttributesMap; }
Spring Security 通过FilterSecurityInterceptor用来拦截SecuritymetadataSource 是FilterInvocationSecuritymetadataSource 类型的。然后执行doFilter方法:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
往下就是方法之间的调用,我简单画了下类之间的依赖关系以及方法的执行顺序从1到8。这里有一个关联点在类WebexpressionConfigAttribute上,因为在expressionbasedFilterInvocationSecuritymetadataSource封装SecuritymetadataSource数据时用到了WebexpressionConfigAttribute,在WebexpressionVoter的vote方法中也用到了WebexpressionConfigAttribute,这样就把数据的封装和获取联系起来了。
WebexpressionVoter#vote方法的具体逻辑如下:
public int vote(Authentication authentication, FilterInvocation fi, Collectionattributes) { assert authentication != null; assert fi != null; assert attributes != null; // 这里和上面的processMap方法联系上,processMap方法块中有一步是new WebexpressionConfigAttribute // 这里就是获取当时的设置的WebexpressionConfigAttribute对象 WebexpressionConfigAttribute weca = findConfigAttribute(attributes); if (weca == null) { return ACCESS_ABSTAIN; } // 最终返回StandardevaluationContext对象,属性 // variables中存放着:{"oauth2":new OAuth2SecurityexpressionMethods(authentication)} // rootObject存放着: new TypedValue(WebSecurityexpressionRoot对象) evaluationContext ctx = expressionHandler.createevaluationContext(authentication, fi); // 实际返回的是DelegatingevaluationContext对象 ctx = weca.postProcess(ctx, fi); // weca.getAuthorizeexpression()为Spelexpression对象 // evaluateAsBoolean内部的逻辑是将Spelexpression对象中的"oauth2"对应到OAuth2SecurityexpressionMethods上 // 将“anonymous”对应到WebSecurityexpressionRoot对象上,然后调用WebSecurityexpressionRoot对象的isAnonymous方法, // 实际上调用的是父类SecurityexpressionRoot的isAnonymous()方法 // 判断Authentication是否为AnonymousAuthenticationToken,是返回true,不是返回false return expressionUtils.evaluateAsBoolean(weca.getAuthorizeexpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; }
根据上面的vote方法来看expressionUtils.evaluateAsBoolean方法执行整体逻辑就是判断Authentication是否为AnonymousAuthenticationToken。上面是大概的流程。
解决问题 第一种方式通过从SecuritymetadataSource着手,就是替换掉expressionbasedFilterInvocationSecuritymetadataSource中的OAuth2WebSecurityexpressionHandler对象然后自定义OAuth2WebSecurityexpressionHandler对象。替换掉 expressionbasedFilterInvocationSecuritymetadataSource中“anonymous”表达式设置为自定义表达式,本人自定义的为“through”, 然后在自定义WebSecurityexpressionRoot 设置对应自定义表达式的方法,这个方法用来判断匿名访问时,Authentication为AnonymousAuthenticationToken还是为OAuth2Authentication都返回true。
如何替换expressionbasedFilterInvocationSecuritymetadataSource数据呢?本人通过实现ObjectPostProcessor类,然后添加到Spring Security HttpSecurity的withObjectPostProcessor方法中。
@EnableResourceServer
@Configuration
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 自定义ObjectPostProcessor要来替换匿名访问的SecuritymetadataSource对象
.withObjectPostProcessor(new CustomObjectPostProcessor())
// 允许部分接口匿名访问
.antMatchers("/anonymous/test").anonymous()
.anyRequest().authenticated().and().logout();
}
}
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.expression.spel.standard.Spelexpression; import org.springframework.expression.spel.standard.SpelexpressionParser; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.vote.Affirmativebased; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.oauth2.provider.expression.OAuth2expressionParser; import org.springframework.security.web.access.expression.expressionbasedFilterInvocationSecuritymetadataSource; import org.springframework.security.web.access.expression.WebexpressionVoter; import org.springframework.security.web.access.intercept.FilterInvocationSecuritymetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; public class CustomObjectPostProcessor implements ObjectPostProcessor{ private final Logger logger = LoggerFactory.getLogger(CustomObjectPostProcessor.class); @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public O postProcess(O object) { FilterSecurityInterceptor interceptor = object; FilterInvocationSecuritymetadataSource source = interceptor.getSecuritymetadataSource(); AccessDecisionManager accessDecisionManager = interceptor.getAccessDecisionManager(); boolean flag = false; if (source instanceof expressionbasedFilterInvocationSecuritymetadataSource) { expressionbasedFilterInvocationSecuritymetadataSource metadataSource = (expressionbasedFilterInvocationSecuritymetadataSource) source; Class> clazz = source.getClass().getSuperclass(); Field field; try { field = clazz.getDeclaredField("requestMap"); field.setAccessible(true); Object requestMap = field.get(metadataSource); if (requestMap instanceof linkedHashMap) { linkedHashMap > map = (linkedHashMap) requestMap; for (Entry > entry : map.entrySet()) { RequestMatcher reqMatcher = entry.getKey(); if (!(reqMatcher instanceof AntPathRequestMatcher)) { continue; } if (!flag) { if (accessDecisionManager instanceof Affirmativebased) { Affirmativebased affirmativebased = (Affirmativebased) accessDecisionManager; List > list = affirmativebased.getDecisionVoters(); WebexpressionVoter webexpressionVoter = (WebexpressionVoter) list.get(0); // 设置自定义OAuth2WebSecurityexpressionHandler webexpressionVoter.setexpressionHandler(new CustomOAuth2WebSecurityexpressionHandler()); list.set(0, webexpressionVoter); flag = true; } } AntPathRequestMatcher requestMatcher = (AntPathRequestMatcher) entry.getKey(); Collection setValue = entry.getValue(); String path = requestMatcher.getPattern(); if ("/anonymous/test".equals(path)) { for (ConfigAttribute configAttribute : setValue) { Class> cla = configAttribute.getClass(); Field finalField = cla.getDeclaredField("authorizeexpression"); finalField.setAccessible(true); OAuth2expressionParser spelexpressionParser = new OAuth2expressionParser( new SpelexpressionParser()); // 设置自定义表达式 Spelexpression spelexpression = (Spelexpression) spelexpressionParser .parseexpression("through"); finalField.set(configAttribute, spelexpression); } } } } } catch (Exception e) { logger.warn("postProcess exception: [{}].", e.getMessage()); } } return object; } }
import org.springframework.security.access.expression.SecurityexpressionOperations;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityexpressionHandler;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebSecurityexpressionRoot;
public class CustomOAuth2WebSecurityexpressionHandler extends OAuth2WebSecurityexpressionHandler {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private String defaultRolePrefix = "ROLE_";
@Override
protected SecurityexpressionOperations createSecurityexpressionRoot(Authentication authentication,
FilterInvocation fi) {
WebSecurityexpressionRoot root = new CustomWebSecurityexpressionRoot(authentication, fi);
root.setPermissionevaluator(getPermissionevaluator());
root.setTrustResolver(trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(defaultRolePrefix);
return root;
}
}
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebSecurityexpressionRoot;
public class CustomWebSecurityexpressionRoot extends WebSecurityexpressionRoot {
private FilterInvocation filterInvocation;
public CustomWebSecurityexpressionRoot(Authentication a, FilterInvocation fi) {
super(a, fi);
this.filterInvocation = fi;
}
public boolean isThrough() {
if (super.isAnonymous() || super.isAuthenticated()) {
return true;
}
return false;
}
public FilterInvocation getFilterInvocation() {
return filterInvocation;
}
}
最终执行的就是isThrough方法不是isAnonymous 方法了,AnonymousAuthenticationToken和OAuth2Authentication都可以通过。
第二种方式通过配置自定义AccessDecisionManager访问决策管理器,来控制访问是否被允许,针对匿名访问,我们设置为允许访问。
@EnableResourceServer
@Configuration
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 自定义ObjectPostProcessor要来替换匿名访问的SecuritymetadataSource对象
// .withObjectPostProcessor(new CustomObjectPostProcessor())
// 自定义AccessDecisionManager
.accessDecisionManager(new CustomAffirmativebased(http))
// 允许部分接口匿名访问
.antMatchers("/anonymous/test").anonymous()
.anyRequest().authenticated().and().logout();
}
}
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.Affirmativebased;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityexpressionHandler;
import org.springframework.security.web.access.expression.WebexpressionVoter;
public class CustomAffirmativebased extends Affirmativebased {
public CustomAffirmativebased(HttpSecurity http) {
this(getDecisionVoters(http));
}
public CustomAffirmativebased(List> decisionVoters) {
super(decisionVoters);
}
public static List> getDecisionVoters(HttpSecurity http) {
List> decisionVoters = new ArrayList>();
// 设置自定义AccessDecisionVoter(访问权限投票器)
WebexpressionVoter expressionVoter = new CustomWebexpressionVoter();
expressionVoter.setexpressionHandler(new OAuth2WebSecurityexpressionHandler());
decisionVoters.add(expressionVoter);
return decisionVoters;
}
}
CustomWebexpressionVoter 为自定义的AccessDecisionVoter针对匿名访问返回的vote 为 -1 (代表访问拒绝),设置为 1(代表允许访问),就可以正常访问了。
import java.util.Collection;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebexpressionVoter;
public class CustomWebexpressionVoter extends WebexpressionVoter {
@Override
public int vote(Authentication authentication, FilterInvocation fi, Collection attributes) {
int vote = super.vote(authentication, fi, attributes);
String url = fi.getHttpRequest().getServletPath();
if ("/anonymous/test".equals(url) && -1 == vote) {
return 1;
}
return vote;
}
}
第三种方式
通过自定义OAuth2WebSecurityexpressionHandler来允许匿名访问,重写createSecurityexpressionRoot(Authentication authentication,FilterInvocation fi)方法然后自定义AuthenticationTrustResolver的实现类重写isAnonymous方法来判断是否是匿名访问,如果是放行。
@EnableResourceServer
@Configuration
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
// 自定义SecurityexpressionHandler来允许匿名访问
.expressionHandler(new CustomAnonymousexpressionHandler());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 自定义ObjectPostProcessor要来替换匿名访问的SecuritymetadataSource对象
// .withObjectPostProcessor(new CustomObjectPostProcessor())
// 自定义AccessDecisionManager
// .accessDecisionManager(new CustomAffirmativebased(http))
// 允许部分接口匿名访问
.antMatchers("/anonymous/test").anonymous()
.anyRequest().authenticated().and().logout();
}
}
import org.springframework.security.access.expression.SecurityexpressionOperations;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityexpressionHandler;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.WebSecurityexpressionRoot;
public class CustomAnonymousexpressionHandler extends OAuth2WebSecurityexpressionHandler {
private String defaultRolePrefix = "ROLE_";
@Override
protected SecurityexpressionOperations createSecurityexpressionRoot(Authentication authentication,
FilterInvocation fi) {
WebSecurityexpressionRoot root = new WebSecurityexpressionRoot(authentication, fi);
root.setPermissionevaluator(getPermissionevaluator());
root.setTrustResolver(new CustomAuthenticationTrustResolverImpl(fi));
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(defaultRolePrefix);
return root;
}
}
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
public class CustomAuthenticationTrustResolverImpl extends AuthenticationTrustResolverImpl {
private FilterInvocation filterInvocation;
public CustomAuthenticationTrustResolverImpl(FilterInvocation fi) {
this.filterInvocation = fi;
}
@Override
public boolean isAnonymous(Authentication authentication) {
boolean flag = super.isAnonymous(authentication);
String url = filterInvocation.getHttpRequest().getServletPath();
if ("/anonymous/test".equals(url) && !flag) {
return true;
}
return flag;
}
}
三种解决方式后两种方式比较容易,第一种比较难,不建议用第一种。另外如果你没有看过源码,可能看起来比较难理解,当然可能也与我表达能力有关。现实中大家可能都是用动态配置权限的可能不会涉及到这样的问题。



