一、SpringSecurity框架中的授权源码分析
一个完整的权限系统总体上来说可以分为两个方面:
1、认证(Authentication)
2、授权(Authorization)
上篇文章中我们已经自定义了认证流程的骨架,实现了让框架帮我们管理会话以及持久化登录信息到Session的功能。不过默认情况下,所有用户对于所有url,只需要登录就能够访问,这可不是我们想要的。一个完整的系统,应该将用户以某种维度进行分类,不同的用户具有不同的权限,从而能够访问不同的URL。
所以授权的关注点主要有两个:
1、用户和权限的映射
2、权限和Url的映射
1和2结合最终实现用户和可访问的Url的映射
回顾上节的的自定义配置中有这么一段代码:
要想了解SpringSecurity是如何进行授权的,就从这段代码入手。
authorizeRequests函数中向HttpSecurity中添加了一个配置类,ExpressionUrlAuthorizationConfigurer,关于SecurityConfigurer和SecurityBuilder的设计模式我再之前已经介绍了,如果有兴趣可以去看一下,能够加深对框架的理解:
landexiang:(八)从框架设计的角度来看springSecurityFilterChainzhuanlan.zhihu.com按照框架设计的原理,分析一个SecurityConfigurer要从四个关键点入手:继承结构、构造函数、init方法、configure方法。
1、继承结构
从类图上可以直观的发现,授权配置相关功能也是一个很复杂设计。有的读者可能没看过类图,这里稍微解释一下:
红色的线表示内部类,蓝色箭头表示继承,白色实线箭头表示组合关系(可以理解为A有一个属性为B,B作为A这个整体的一部分),白色虚线表示依赖关系,图上的依赖关系体现为创建关系(比如A中new了类型为B的对象)。
从类图中观察,授权功能相关配置又可以分为两个部分:Configurer和Registry
1.1 授权相关的Configurer
Configurer指的就是SecurityConfigurer,Registry是Configurer的内部定义,具体是什么请请继续往后看。
AbstractHTTPConfigurer就不用了解了,HttpSecurity中使用的SecurityConfigurer几乎都继承于这个抽象类,提供了两个辅助性的功能,其最主要的功能还是体现在语义层次上,即表示子类是服务于HttpSecurity的。所以图上的类我们从AbstractInterceptUrlConfigurer开始着手。先来看下这个抽象类的注释:
从注释中可以了解到AbstractInterceptUrlConfigurer主要是给FilterSecurityInterceptor、其他的SecurityConfigurer的sharedObject、AuthenticationManager提供支持。
AbstractInterceptUrlConfigurer有两个实现类
从HttpSecurity的配置可以看出,框架中默认使用的实现类为ExpressURLAuthorizationConfigurer。
通过注释可以了解到ExpressURLAuthorizationConfigurer在抽象父类AbstractInterceptUrlConfigurer的基础之上额外提供了——基于SPEL表达式的授权功能,并且至少有一个@RequestMapping注解所代表的url映射到一个ConfigAttribute,ExpressURLAuthorizationConfigurer才有意义。
从注释中可以了解到两个信息:
1、SpringSecurity框架默认是使用SPEL表达式来对URL进行授权的
2、ConfigAttribute对象和@RequestMapping映射的URL有一定联系
另外可以发现ExpressURLAuthorizationConfigurer中还有几个内部类,一些是自己的,一些是定义在抽象父类里的。
1.2 授权相关的Registry
从类图上来看,registry的顶级类为抽象类AbstractRequestMatcherRegistry,
从注释来看主要是用来注册RequestMatcher类,RequestMatcher的提供了url匹配的功能,包括请求方法,请求路径等匹配方式。
AbstractConfigAttributeRequestMatcherRegistry作为AbstractRequestMatcherRegistry的抽象子类,在父类功能的基础之上,额外提供了UrlMapping和ConfigAttribute相关的操作。如下图所示
前面已经提到过ExpressURLAuthorizationConfigurer注释表明@RequestMapping映射的url和ConfigAttribute有一定的联系,从这就可以得出结论,对于已授权的url会被封装成一个UrlMapping对象,其中既包含了匹配url的RequestMatcher对象又包含了表示url权限信息的ConfigAttribute对象,不过目前只是猜测,具体是不是还要继续看源码。
抽象子类AbstractInterceptUrlRegistry又继承于AbstractConfigAttributeRequestMatcherRegistry,定义在抽象配置父类AbstractInterceptUrlConfigurer中
从代码中可以看出AbstractInterceptUrlRegistry提供了AccessDecisionManager类的set功能。AccessDecisionManager提供了请求鉴权的功能。
ExpressionInterceptUrlRegistry继承于AbstractInterceptUrlRegistry,从源码看,除了具有父类特性外,提供了两个功能
1、创建请求的url授权的结果类AuthorizatedUrl和MvcMatchersAuthorizedUrl
2、设置处理SPEL表达式的handler
从源码来看,每个类所提供的功能都比较简单,但是由于继承链比较长和复杂,所以理解起来还是会有些晦涩。所以下面就来从框架实际的运用来进行源码解析。
2、SecurityConfigurer的构造函数
分析一个SecurityConfigurer到底做了哪些事,在粗略了解了相关类的继承结构之后,首先应该看得是构造函数。
在构造函数中创建了ExpressionInterceptUrlRegistry对象,这个registry的功能在前面已经简单叙述到了。不过有一点需要注意:
在HttpSecurity对象中设置授权相关配置时,创建了ExpressionUrlAuthorizationConfigurer配置后,返回值是ExpressionInterceptUrlRegistry,也就是authorizeRequests()方法的返回值是ExpressionInterceptUrlRegistry,所以后面的.anyRequest().authenticated()都是对于
ExpressionInterceptUrlRegistry的设置。刚好通过这个设置来看下Registry相关类的详细功能:
ANY_REQUEST表示一个matches始终返回true的RequestMatcher类,也就是匹配任何url。
requestMatchers是AbstractRequestMatcherRegistry的方法,
chainRequstMatchers是抽象子类AbstractConfigAttributeRequestMatcherRegistry中实现的。
而chainRequestMatcherInternal则是子类ExpressionUrlAuthorizationConfigurer中实现的,最终创建了一个AuthorizedUrl对象:
所以.anyRequests()方法的返回值为AuthorizedUrl,后面的authenticated()是AuthorizedUrl提供的方法。
从注释来看AuthorizedUrl.authenticated方法的功能为指定当前对象中所有requestMatchers所能匹配的url对于所有已认证的用户开放。
从源码来看,SpringSecurity框架是将表示匹配所有请求是AnyRequestMatcher和表示已认证权限的"authenticated"字符串作为ConfigurerAttribute构造了一个UrlMapping对象来将url和权限聚合,然后添加到ExpressionInterceptUrlRegistry对象中,这和我们在上面根据类的注释信息推测出的结果一致。
而且从源码的属性定义来看,SpringSecurity的权限应该有固定的6种。
通过上面的源码分析,我们可以知道的是对于Url的访问权限设置是AuthorizedUrl的工作,而AuthorizedUrl是在AbstractRequestMatcherRegistry抽象类中创建的。如果我们想给url进行权限细化,则还需要看下这个类中还提供了哪些方法:
从源码中可以看出还有额外的三种url映射设置方式,不过url匹配使用的都是ant风格的表达式。
这三种设置分别是:
1、固定请求方法,但匹配所有url的AuthorizedUrl
2、固定请求方法,固定url的AuthorizedUrl
3、只固定url的AuthorizedUrl
而且1其实就是使用2来进行创建的。
现在我们已经知道了使用antMatchers()方法就可以自定义拦截Url创建AuthorizedUrl,但是授权除了authenticated,还有哪些方式呢?当然是来看下AuthorizedUrl还提供了哪些授权方法。
not表示不具有xxx权限
上面几种是框架提供的默认权限,同来进行粗粒度的授权工作
可以发现框架中除了提供可选的6种默认权限之外,还提供了两种自定义的设置方式:
1、hasRole
2、hasAuthority
通过源码可以看出,其实返回的就是一个SPEL表达式,role和authority的不同之处在于如果使用hasRole则表示使用的是url角色,注册时不能带ROLE_前缀,框架会给你自动补上这个前缀。使用authority表示使用的是url权限,则没有任何限制。
也就是说通过框架提供的这种特性我们可以对于同一个url进行两种授权设置,一种基于角色,一种基于权限。比如我们想设置"/user"所有子路径必须具有ROLE_USER角色或者MODEL_USER_REQUEST权限才能进行访问则可以像下面这样设置:
2、SecurityConfigurer的init方法
通过上面的源码分析,我们已经知道了如何使用框架提供的功能去自定义url授权,但是我们仍不了解框架是如何鉴权的。所以还需要继续看下相关的源码,才能够进行我们自己的权限系统的设计。
之前的文章中已经介绍了SpringSecurity的基本设计模式,所以SecurityConfigurer的init方法是我们阅读源码时必须了解的。
但是通过源码可以看到ExpressURLAuthorizationConfigurer以及其所有父类都没有重写init方法,根据init方法的初衷,可以知道ExpressURLAuthorizationConfigurer是一个功能相对比较独立的模块,因为他没有经过init方法设置共享变量。
3、SecurityConfigurer的configure方法
init方法是用来设置共享变量到SecurityBuilder中的,而configure方法则是用来处理SecurityBuilder的直接属性。ExpressURLAuthorizationConfigurer中并没有重写抽象父类AbstractInterceptUrlConfigurerd的configure方法:
metadataSource是之后用来鉴权的的元数据,可以看一下他有哪些数据,了解一下鉴权需要用到的信息:
从源码来看,metadataSource中只有一个处理SPEL表达式的handler和我们授权的url的信息。不过从源码来看,如果设置了多个相同的RequestMatcher,只有最后的会生效,前面设置的会被覆盖掉,所以我们设想的给同一个url即授予角色又授予权限是在默认情况下是行不通了~
如图所示,我们定义了三个antMatcher,但最终元数据中只有两条,因为有两个requestMatcher重复了。
configure中只有这个需要特别关注,下面就是创建filter了,没什么特别的地方。
二、SpringSecurity中的鉴权源码分析
通过上面的几个关键点,现在我们已经知道如何自定义url的授权,也知道了我们定义的授权哪些是有效的,而最终的鉴权则是FilterSecurityInterceptor来完成的。
FilterSecurityInterceptor作为一个filter,所以鉴权相关肯定是在doFilter方法中进行的。
从源码来看对于同一个请求只需要鉴权一次,所以对于请求的转发,是不是可以越权访问呢?网上搜了一下,好像没发现相关资料,因为请求的转发还算做同一次请求,所以我也是猜测,以后抽空测验一下~。
鉴权操作主要是父类来做的,核心代码只有这一行。authenticated是我们的登录信息,attributes则是从metadataSource中取出当前访问url所需要的权限信息:
比如我们访问根目录"/",则对应着authenticated这条权限。如果没有找到匹配当前访问url的授权定义,默认情况下不做拦截。不过这里有一点需要我们注意!!!我们来看下attributes是如何取出来的:
不是取出所有匹配项,而是取出第一条匹配项!所以对于同一个url,如果定义了不同的antPath都匹配到了,结果是只有定义在前面的授权生效!!!所以下面的定义是错误的
正确的定义方式应该是:
将anyRequest的授权放在最后面。
accessDecisionManager就是用来进行鉴权的类。框架默认的AccessDecisionManager为:AffirmativeBased
框架中一共定义了三种decisionManager。我们还是先了解一下框架默认行为是否满足我们的需求:
从注释来看AffirmativeBased的鉴权其实是委托给AccessDecisionVoter来做的,并且只要其内部的任意一个AccessDecisionVoter鉴权通过,则算作当前请求有权限访问。
框架中虽然定义了很多voter,但默认用到的只有一个,即WebExpressionVoter。我们定义的SPEL表达式权限就是交给这个voter进行鉴定的,鉴权方法为vote:
WebExpressionVoter鉴权有三种结果:弃权,肯定,否定。当返回肯定的时候则表示鉴权通过,当返回弃权和否定的时候,将鉴权交给下一个voter进行。
WebExpressionVoter最主要的鉴权是上面这段代码,即将用户的登陆信息中的权限和SPEL表达式进行对比:
鉴权的代码十分复杂,通过debug发现原来是通过反射调用了SecurityExpressionRoot类的hasRole方法:
hasRole方法的参数就是我们给url授予的权限信息:
从源码可以看到,框架认为系统所具有的权限为从登陆信息中的authorities字段,是一个GrantedAuthority类型的List。
而框架中默认使用的GrantedAuthority则是SimpleGrantedAuthority,其实就是一个字符串的包装类,现在我们已经知道如何去给用户赋予权限了了,只需要将权限字符串设置到其登陆信息的authorities字段即可。
鉴权很简单,只要用户所具有的角色和url中授予的角色任一匹配,则表示鉴权通过。不过有一点可能你会奇怪,为什么这里是调用的hasRole而不是hasAuthority函数呢?回过头来看下我们的配置内容:
根据前面的分析,我们已经知道了对于相同的url,后面的配置会覆盖前面的所以只有hasRole生效了。如果我们来调换一下这两个配置的位置:
将hasRole放到前面,再来看一下结果:
不出所料,使用的是authority进行鉴权。
三、总结
通过本篇文章我们知道了框架授权和鉴权的原理。
授权:在ExpressionInterceptURLRegistry中注册AuthorizedUrl对象,AuthorizedUrl对象包含了匹配url的RequestMatcher以及表示权限的AttributeConfigure。权限分为authority和role两种,对于相同的url,后面定义的授权会覆盖前面的授权。而对于不相同但是被多个antPath都匹配到的url,则前面的定义会覆盖掉后面的定义,也就是说对于覆盖范围越广的授权定义,越要放在配置的后面。
鉴权:使用用户登录信息中的authorities字段所表示的权限和配置中对url的授权进行匹配,匹配通过则表示该用户具有访问权限,匹配不通过则表示该用户没有访问权限。使用role授权则不能以ROLE作为字符串的开头,而且框架会在鉴权时给字符串加上ROLE_前缀。而使用authority时则没有限制,鉴权时是字符串的完全匹配。
所以我们要向自定义权限系统其实只需要考虑两个方面:
1、使用authoriy还是role进行授权
2、什么时候将权限设置到用户的authorities字段
下一节就让我们结合前面的所有内容来自定义一个权限系统。