前言
今天我们重点聊聊授权方式的另外一种:基于HttpServletRequest配置权限
基于HttpServletRequest配置权限
一个典型的配置demo
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的。
/**
* 这个方法是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;
}
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表达式:
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中,对应的是AuthoriztyAuthorizationManager
和AuthenticatedAuthorizationManager
。前者,意味着我们配置的是角色/权限,后者对应的是authenticated()
这个方法。
如果你认真看了这个关系图,那么一定会发现右边的4个实现类正是我们在上一文讲述基于方法配置权限中所使用到的。
鉴权源码分析
权限过滤的入口:AuthorizationFilter
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
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的情况下,默认是拒绝请求的。
总结
-
我们在配置中配置的url被封装成RequestMatcher,而hasRole被封装成AuthorityAuthorizationManager。进行注册,在请求过来时,便通过遍历所有注册好的RequestMatch进行匹配,存在匹配就调用
AuthorizationManager<RequestAuthorizationContext>#check
方法。 -
配置的链式调用,会跨越多个不同的类,最终又回到第一个对象的类型。
后记
本文我们聊了基于HttpRequest配置权限的方方面面。相信这里有一个点应该会引起大家的注意:配置。下一次,我们聊聊Spring Security的配置体系。