Spring Security 如何允许对同一地址进行匿名和身份验证访问

场景描述

可能在开发过程中设置了一个匿名访问地址,但这个地址我们同时也想让它能够进行身份验证访问。就比如一个地址你把它分享出去就可以匿名访问,但如果这个地址如果没有被分享出去在系统内部或者在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;
	}
}

三种解决方式后两种方式比较容易,第一种比较难,不建议用第一种。另外如果你没有看过源码,可能看起来比较难理解,当然可能也与我表达能力有关。现实中大家可能都是用动态配置权限的可能不会涉及到这样的问题。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Security是一个用于解决认证和授权的框架,它提供了一套强大的功能来处理用户身份验证和授权。下面是Spring Security身份验证访问授权的一些常见步骤: 1.添加Spring Security依赖:在项目的pom.xml文件中添加Spring Security依赖。 2.配置Spring Security:创建一个类并扩展WebSecurityConfigurerAdapter类,然后覆盖configure()方法来配置Spring Security。在这个方法中,你可以定义哪些URL需要被保护,哪些URL不需要被保护,以及如何进行身份验证。 3.定义用户:你可以在内存中定义用户,也可以从数据库中获取用户信息。如果你选择在内存中定义用户,可以使用以下代码: ```java @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER") .and() .withUser("admin").password("{noop}password").roles("USER", "ADMIN"); } ``` 这段代码定义了两个用户,一个是普通用户,一个是管理员。密码使用了{noop}前缀,这是因为Spring Security 5默认要求密码进行加密,{noop}前缀表示不进行加密。 4.定义访问授权:你可以定义哪些用户可以访问哪些URL。例如,以下代码定义了只有管理员可以访问/admin路径: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic(); } ``` 5.启用Spring Security:最后,你需要在Spring Boot应用程序中启用Spring Security。你可以通过在应用程序类上添加@EnableWebSecurity注释来完成这个任务。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值