Spring Security之基于HttpRequest配置权限

前言

今天我们重点聊聊授权方式的另外一种:基于HttpServletRequest配置权限

基于HttpServletRequest配置权限

一个典型的配置demo

java 复制代码
http.authorizeHttpRequests(requestMatcherRegstry -> 
// /admin/** 需要有AMIND角色
	requestMatcherRegstry.requestMatchers("/admin/**").hasRole("ADMIN")
			// /log/** 只要有AMIND、USER角色之一
          .requestMatchers("/log/**").hasAnyRole("ADMIN", "USER")
			// 任意请求 只要登录了即可访问
          .anyRequest().authenticated()
);

从这里也可以看出,要实现基于RBAC,还是比较容易的。也比较容易使用。但是如果想要动态的增加角色,就需要我们定制AuthorizationManager。

配置原理

HttpSecurity是负责构建DefaultSecurityFilterChain的。而这个安全过滤器链,则是允许我们进行配置的。而authorizeHttpRequests方法,正是配置AuthorizationFilter的。而我们传入的入参-lambada表达式-则是指引如何配置AuthorizationFilter的。

java 复制代码
/**
 * 这个方法是HttpSecurity的方法。
 * 作用是配置AuthorizationFilter。
 * 其入参authorizeHttpRequestsCustomizer正是让我们配置AuthorizationFilter的关键。
 * Customizer:就是定制。原理比较容易理解,就是我把你需要配置的东西丢给你,你往里面赋值。
 * AuthorizeHttpRequestsConfigurer<HttpSecurity>:这个是Configurer的实现,负责引入过滤器的。这里明显就是引入AuthorizationFilter
 * AuthorizationManagerRequestMatcherRegistry:这个就是我们最终配置的东西。而这个配置的正是我们上面的RequestMatcherDelegatingAuthorizationManager。说白了就是往里面添加哪些路径对应哪些AuthorizationManager。只不过,为了方便使用,也帮我们都封装好了。不妨继续往后看看。
 */
public HttpSecurity authorizeHttpRequests(
			Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer)
			throws Exception {
		ApplicationContext context = getContext();
		// 这里干了三个事情:
		// 1. 如果当前HttpSecurity不存在AuthorizeHttpRequestsConfigurer,则创建一个,并注册到当前的HttpSecurity对象中。
		// 2. 从AuthorizeHttpRequestsConfigurer拿到他的注册器也就是AuthorizationManagerRequestMatcherRegistry
		// 3. 调用传入的参数的customize。如此,我们传入的lambda表达式就被调用了。
		authorizeHttpRequestsCustomizer
			.customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry());
		return HttpSecurity.this;
	}
java 复制代码
public final class AuthorizationManagerRequestMatcherRegistry
		extends AbstractRequestMatcherRegistry<AuthorizedUrl> {
	/**
	 * 这是父类的方法
	 * C代表的是AuthorizedUrl
	 */
	public C requestMatchers(String... patterns) {
		// 调用的重载方法第一个参数为HttpMethod,也就是说,我们还可以指定HTTP请求的方法,例如:POST、GET等
		return requestMatchers(null, patterns);
	}
	
	@Override
	protected AuthorizedUrl chainRequestMatchers(List<RequestMatcher> requestMatchers) {
		this.unmappedMatchers = requestMatchers;
		return new AuthorizedUrl(requestMatchers);
	}
}
public class AuthorizedUrl {
	private final List<? extends RequestMatcher> matchers;
	public AuthorizationManagerRequestMatcherRegistry permitAll() {
		return access(permitAllAuthorizationManager);
	}
	public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
		return access(withRoleHierarchy(AuthorityAuthorizationManager.hasRole(role)));
	}
	public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) {
		return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
	}
	public AuthorizationManagerRequestMatcherRegistry authenticated() {
		return access(AuthenticatedAuthorizationManager.authenticated());
	}
	public AuthorizationManagerRequestMatcherRegistry access(
		AuthorizationManager<RequestAuthorizationContext> manager) {
		Assert.notNull(manager, "manager cannot be null");
		return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
	}
}
public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<AuthorizeHttpRequestsConfigurer<H>, H> {
		
	private AuthorizationManagerRequestMatcherRegistry addMapping(List<? extends RequestMatcher> matchers,
		AuthorizationManager<RequestAuthorizationContext> manager) {
		for (RequestMatcher matcher : matchers) {
			this.registry.addMapping(matcher, manager);
		}
		return this.registry;
	}
}

我们通过lambda表达式:

java 复制代码
requestMatcherRegstry -> requestMatcherRegstry.requestMatchers("/admin/**").hasRole("ADMIN")

配置的正是AuthorizationManagerRequestMatcherRegistry

requestMachers方法,构建出AuthorizedUrl,然后通过这个类的hasRole方法注册当前路径所对应的权限/角色。这个对应关系由RequestMatcherEntry保存。key:RequestMatcher requestMatcher;value: AuthorizationManager。

值得一提的是,这个lambda表达式以及其链式调用看起来简单方便,但是其内部涉及多个类的方法调用,实在很容易犯迷糊,这是我觉得比较诟病的地方。在我看来,链式调用还是同一个返回值(每次都返回this)才能做到在方便至于也能清晰明了,容易理解。

而这里在lambda表达式内部:

  • 第一个方法是requestMatcherRegstry.requestMatchers
    AbstractRequestMatcherRegistry,也就是我们的AuthorizationManagerRequestMatcherRegistry的父类。方法返回值是AuthorizedUrl。
  • 第二个方法是AuthorizedUrl.hasRole
    而该方法的返回值为AuthorizationManagerRequestMatcherRegistry

发现什么了吗?链式调用还能玩起递归,又回到最开始的第一个方法了。而要是我们配置HttpSecurity,直接一连串的链式调用,那更是没谱了。经常就是,你只能看着别人这样配置,然后照猫画虎。这个链式调用咋调回来的,一头雾。因为中间可能跨越好几个不同的类。。。

PS:可能官方也有些意识到这点,所以sample工程都是类似于本文开头的那样,传入一个基于lambda表达式的Customizer。一个方法配置一个过滤器的SecurityConfigurer。但,如果你翻看源码,你看到的就是一连串的链式调用。最为明显的一个证明就是HttpSecurity#and方法过期了。因此个人推荐大家用文章开头的那种方法,相对清晰易理解。

我想说,这么玩是深怕别人搞明白了是吗???更绝的是,即便你知晓了原理也没有办法直接注册对应关系,除非你使用反射!

这里给大家提个醒,如果你想搞明白你在使用SpringSecurity究竟在配置些什么,那么你就必须要搞明白上面的套路。

设计方案

Spring Security在5.5版本之后,在鉴权架构上,进行了较大的改动。以至于官方也出了迁移指南

组件 5.5之前 5.5之后
过滤器 FilterSecurityInterceptor AuthorizationFilter
鉴权管理器 AccessDecisionManager AuthorizationManager
访问决策投票员 AccessDecisionVoter -

而原来的设计方案,相较于新的方案,更为复杂。这里给大家一张官方的UML感受感受:

除却过滤器外,还需要三个组件来构建完整的鉴权:

AccessDecisionManager 、AccessDecisionVoter 、ConfigAttribute。

感兴趣的同学可以自己琢磨琢磨,但已经废弃的方案,这里就不讨论了。

5.6之后的新方案

新方案只有一个包罗万象、且极具扩展性的AuthorizationManager

我们前面的配置demo,本质上都是在配置RequestMatcherDelegatingAuthorizationManager。他主要是记录每一个路径对应的AuthorizationManager<HttpServletRequest>。当有请求过来时,只需要遍历每一个路径,当找到匹配者就委托该AuthorizationManager<HttpServletRequest>进行鉴权。

在我们的配置demo中,对应的是AuthoriztyAuthorizationManagerAuthenticatedAuthorizationManager。前者,意味着我们配置的是角色/权限,后者对应的是authenticated()这个方法。

如果你认真看了这个关系图,那么一定会发现右边的4个实现类正是我们在上一文讲述基于方法配置权限中所使用到的。

鉴权源码分析

权限过滤的入口:AuthorizationFilter

java 复制代码
public class AuthorizationFilter extends GenericFilterBean {

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
			throws ServletException, IOException {
		// 类型转换
		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;
		// 是否需要执行鉴权
		if (this.observeOncePerRequest && isApplied(request)) {
			chain.doFilter(request, response);
			return;
		}
		// /error和异步请求不处理
		if (skipDispatch(request)) {
			chain.doFilter(request, response);
			return;
		}
		// 是否已经执行过鉴权逻辑了
		String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
		request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
		try {
			// 从SecurityContextHolder中获取凭证,并通过AuthorizationManager做出决策
			AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
			
			// 发布鉴权事件
			this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
			if (decision != null && !decision.isGranted()) {
				// 拒绝访问异常
				throw new AccessDeniedException("Access Denied");
			}
			// 正常执行后续业务逻辑
			chain.doFilter(request, response);
		}
		finally {
			// 处理完业务逻辑后,为当前请求清理标识			
			request.removeAttribute(alreadyFilteredAttributeName);
		}
	}
}

RequestMatcherDelegatingAuthorizationManager

java 复制代码
public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
@Override
	public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
		// 遍历每一个已经登录好的路径,找到对应的AuthorizationManager<RequestAuthorizationContext>>
		for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {

			RequestMatcher matcher = mapping.getRequestMatcher();
			// 匹配当前请求
			MatchResult matchResult = matcher.matcher(request);
			if (matchResult.isMatch()) {
				// 找到匹配的AuthorizationManager就直接调用check方法并返回鉴权结果
				AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
				return manager.check(authentication,
						new RequestAuthorizationContext(request, matchResult.getVariables()));
			}
		}
		// 没有匹配的AuthorizationManager则返回拒绝当前请求
		return DENY;
	}
}

可见,在没有匹配的AuthorizationManager的情况下,默认是拒绝请求的。

总结

  1. 我们在配置中配置的url被封装成RequestMatcher,而hasRole被封装成AuthorityAuthorizationManager。进行注册,在请求过来时,便通过遍历所有注册好的RequestMatch进行匹配,存在匹配就调用AuthorizationManager<RequestAuthorizationContext>#check方法。

  2. 配置的链式调用,会跨越多个不同的类,最终又回到第一个对象的类型。

后记

本文我们聊了基于HttpRequest配置权限的方方面面。相信这里有一个点应该会引起大家的注意:配置。下一次,我们聊聊Spring Security的配置体系。

相关推荐
hello早上好40 分钟前
CGLIB代理核心原理
java·spring
先睡7 小时前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
Bug退退退12311 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq
booooooty17 小时前
基于Spring AI Alibaba的多智能体RAG应用
java·人工智能·spring·多智能体·rag·spring ai·ai alibaba
极光雨雨17 小时前
Spring Bean 控制销毁顺序的方法总结
java·spring
Spirit_NKlaus18 小时前
解决HttpServletRequest无法获取@RequestBody修饰的参数
java·spring boot·spring
lwb_011818 小时前
SpringCloud——Gateway新一代网关
spring·spring cloud·gateway
lxsy20 小时前
spring-ai-alibaba 1.0.0.2 学习(七)——集成阿里云百炼平台知识库
学习·spring·阿里云·spring-ai·ai-alibaba
程序猿小D20 小时前
[附源码+数据库+毕业论文]基于Spring+MyBatis+MySQL+Maven+jsp实现的电影小说网站管理系统,推荐!
java·数据库·mysql·spring·毕业设计·ssm框架·电影小说网站
CodeWithMe21 小时前
【Note】《深入理解Linux内核》 Chapter 15 :深入理解 Linux 页缓存
linux·spring·缓存