场景描述
可能在开发过程中设置了一个匿名访问地址,但这个地址我们同时也想让它能够进行身份验证访问。就比如一个地址你把它分享出去就可以匿名访问,但如果这个地址如果没有被分享出去在系统内部或者在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(
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap,
SecurityExpressionHandler<FilterInvocation> expressionHandler) {
// expressionHandler.getExpressionParser()为OAuth2ExpressionParser对象
super(processMap(requestMap, expressionHandler.getExpressionParser()));
Assert.notNull(expressionHandler,
"A non-null SecurityExpressionHandler is required");
}
private static LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> processMap(
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap,
ExpressionParser parser) {
Assert.notNull(parser, "SecurityExpressionHandler returned a null parser object");
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestToExpressionAttributesMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(
requestMap);
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
RequestMatcher request = entry.getKey();
Assert.isTrue(entry.getValue().size() == 1,
"Expected a single expression attribute for " + request);
ArrayList<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(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 如何来处理匿名访问的
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,
Collection<ConfigAttribute> attributes) {
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<FilterSecurityInterceptor> {
private final Logger logger = LoggerFactory.getLogger(CustomObjectPostProcessor.class);
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public <O extends FilterSecurityInterceptor> 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<RequestMatcher, Collection<ConfigAttribute>> map = (LinkedHashMap) requestMap;
for (Entry<RequestMatcher, Collection<ConfigAttribute>> entry : map.entrySet()) {
RequestMatcher reqMatcher = entry.getKey();
if (!(reqMatcher instanceof AntPathRequestMatcher)) {
continue;
}
if (!flag) {
if (accessDecisionManager instanceof AffirmativeBased) {
AffirmativeBased affirmativeBased = (AffirmativeBased) accessDecisionManager;
List<AccessDecisionVoter<?>> 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<ConfigAttribute> 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;
}
/**
* 针对匿名链接的匿名和身份验证访问处理
*
* @return
*/
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<AccessDecisionVoter<? extends Object>> decisionVoters) {
super(decisionVoters);
}
public static List<AccessDecisionVoter<? extends Object>> getDecisionVoters(HttpSecurity http) {
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
// 设置自定义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<ConfigAttribute> 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;
}
}
三种解决方式后两种方式比较容易,第一种比较难,不建议用第一种。另外如果你没有看过源码,可能看起来比较难理解,当然可能也与我表达能力有关。现实中大家可能都是用动态配置权限的可能不会涉及到这样的问题。