授权系统源码_(十)SpringSecurity授权和鉴权原理

一、SpringSecurity框架中的授权源码分析

一个完整的权限系统总体上来说可以分为两个方面:

1、认证(Authentication)

2、授权(Authorization)

上篇文章中我们已经自定义了认证流程的骨架,实现了让框架帮我们管理会话以及持久化登录信息到Session的功能。不过默认情况下,所有用户对于所有url,只需要登录就能够访问,这可不是我们想要的。一个完整的系统,应该将用户以某种维度进行分类,不同的用户具有不同的权限,从而能够访问不同的URL。

所以授权的关注点主要有两个:

1、用户和权限的映射

2、权限和Url的映射

1和2结合最终实现用户和可访问的Url的映射

回顾上节的的自定义配置中有这么一段代码:

ca210eaa890689cdb49c80b22b8820f9.png

要想了解SpringSecurity是如何进行授权的,就从这段代码入手。

0d21af0a0bb361107e7c26cbf33e97d0.png

authorizeRequests函数中向HttpSecurity中添加了一个配置类,ExpressionUrlAuthorizationConfigurer,关于SecurityConfigurerSecurityBuilder的设计模式我再之前已经介绍了,如果有兴趣可以去看一下,能够加深对框架的理解:

landexiang:(八)从框架设计的角度来看springSecurityFilterChain​zhuanlan.zhihu.com
9881cd53998ed3d9b2f8a89336702fac.png

按照框架设计的原理,分析一个SecurityConfigurer要从四个关键点入手:继承结构构造函数init方法configure方法

1、继承结构

7d24ff1ff18ff5f9a0949ce34474f0df.png

从类图上可以直观的发现,授权配置相关功能也是一个很复杂设计。有的读者可能没看过类图,这里稍微解释一下:

红色的线表示内部类,蓝色箭头表示继承,白色实线箭头表示组合关系(可以理解为A有一个属性为B,B作为A这个整体的一部分),白色虚线表示依赖关系,图上的依赖关系体现为创建关系(比如A中new了类型为B的对象)。

从类图中观察,授权功能相关配置又可以分为两个部分:ConfigurerRegistry

1.1 授权相关的Configurer

Configurer指的就是SecurityConfigurer,Registry是Configurer的内部定义,具体是什么请请继续往后看。

AbstractHTTPConfigurer就不用了解了,HttpSecurity中使用的SecurityConfigurer几乎都继承于这个抽象类,提供了两个辅助性的功能,其最主要的功能还是体现在语义层次上,即表示子类是服务于HttpSecurity的。所以图上的类我们从AbstractInterceptUrlConfigurer开始着手先来看下这个抽象类的注释:

1efd5a75beff859feeba43d5f4e39885.png

从注释中可以了解到AbstractInterceptUrlConfigurer主要是给FilterSecurityInterceptor、其他的SecurityConfigurer的sharedObject、AuthenticationManager提供支持。

AbstractInterceptUrlConfigurer有两个实现类

7890df6b02b785bd1e0f8c006e495b23.png

从HttpSecurity的配置可以看出,框架中默认使用的实现类为ExpressURLAuthorizationConfigurer。

61f6b82db88bc0e7c112560f0faa70cf.png

通过注释可以了解到ExpressURLAuthorizationConfigurer在抽象父类AbstractInterceptUrlConfigurer的基础之上额外提供了——基于SPEL表达式的授权功能,并且至少有一个@RequestMapping注解所代表的url映射到一个ConfigAttribute,ExpressURLAuthorizationConfigurer才有意义。

从注释中可以了解到两个信息:

1、SpringSecurity框架默认是使用SPEL表达式来对URL进行授权的

2、ConfigAttribute对象和@RequestMapping映射的URL有一定联系

另外可以发现ExpressURLAuthorizationConfigurer中还有几个内部类,一些是自己的,一些是定义在抽象父类里的。

1.2 授权相关的Registry

从类图上来看,registry的顶级类为抽象类AbstractRequestMatcherRegistry

3e84bdfff94fe9206ed879a865082bfd.png

从注释来看主要是用来注册RequestMatcher类,RequestMatcher的提供了url匹配的功能,包括请求方法,请求路径等匹配方式。

AbstractConfigAttributeRequestMatcherRegistry作为AbstractRequestMatcherRegistry的抽象子类,在父类功能的基础之上,额外提供了UrlMapping和ConfigAttribute相关的操作。如下图所示

fc5bac91ffeb021f4b5d797959a9d3ab.png

前面已经提到过ExpressURLAuthorizationConfigurer注释表明@RequestMapping映射的url和ConfigAttribute有一定的联系,从这就可以得出结论,对于已授权的url会被封装成一个UrlMapping对象,其中既包含了匹配url的RequestMatcher对象又包含了表示url权限信息的ConfigAttribute对象,不过目前只是猜测,具体是不是还要继续看源码。

抽象子类AbstractInterceptUrlRegistry又继承于AbstractConfigAttributeRequestMatcherRegistry,定义在抽象配置父类AbstractInterceptUrlConfigurer

b972966994efacbc493376cbee1b6e19.png

从代码中可以看出AbstractInterceptUrlRegistry提供了AccessDecisionManager类的set功能。AccessDecisionManager提供了请求鉴权的功能。

ExpressionInterceptUrlRegistry继承于AbstractInterceptUrlRegistry,从源码看,除了具有父类特性外,提供了两个功能

1、创建请求的url授权的结果类AuthorizatedUrl和MvcMatchersAuthorizedUrl

2、设置处理SPEL表达式的handler

从源码来看,每个类所提供的功能都比较简单,但是由于继承链比较长和复杂,所以理解起来还是会有些晦涩。所以下面就来从框架实际的运用来进行源码解析。

2、SecurityConfigurer的构造函数

分析一个SecurityConfigurer到底做了哪些事,在粗略了解了相关类的继承结构之后,首先应该看得是构造函数。

d87bca0093d3eb86dcb4c60c300ec7f1.png

在构造函数中创建了ExpressionInterceptUrlRegistry对象,这个registry的功能在前面已经简单叙述到了。不过有一点需要注意:

73bbbebdfca761480c56fc74a1d64c1e.png

29f981454bf20a3d0cc0f758e8ba5d09.png

在HttpSecurity对象中设置授权相关配置时,创建了ExpressionUrlAuthorizationConfigurer配置后,返回值是ExpressionInterceptUrlRegistry,也就是authorizeRequests()方法的返回值是ExpressionInterceptUrlRegistry,所以后面的.anyRequest().authenticated()都是对于
ExpressionInterceptUrlRegistry的设置。刚好通过这个设置来看下Registry相关类的详细功能:

1574e39da0ed1a5acf488204c192597a.png

cec0fe06ece14c793b9f72e1528c25bc.png

5ee33861b15742615024596955cfb39d.png

ANY_REQUEST表示一个matches始终返回true的RequestMatcher类,也就是匹配任何url。

155dd5f22d00f5493d565ce12007cc2b.png

requestMatchers是AbstractRequestMatcherRegistry的方法,

4b490f459fe33394f56ba42007f36529.png

chainRequstMatchers是抽象子类AbstractConfigAttributeRequestMatcherRegistry中实现的。

5ed06a7ed3cb3122dbc09ddafad2bec9.png

而chainRequestMatcherInternal则是子类ExpressionUrlAuthorizationConfigurer中实现的,最终创建了一个AuthorizedUrl对象:

fff99e34ba93859d67d1c65520705b46.png

所以.anyRequests()方法的返回值为AuthorizedUrl,后面的authenticated()是AuthorizedUrl提供的方法。

291549ffee43b74bd41cc269528b1829.png

从注释来看AuthorizedUrl.authenticated方法的功能为指定当前对象中所有requestMatchers所能匹配的url对于所有已认证的用户开放。

24bb5e6f77b23c5af2251c362c4b2bce.png

c2aa3e59ff1b7104a3bc9b579ca10d40.png

bf2232916fe49a77454575012105832a.png

4d9397101a60503b37785b90f5e99c97.png

从源码来看,SpringSecurity框架是将表示匹配所有请求是AnyRequestMatcher和表示已认证权限的"authenticated"字符串作为ConfigurerAttribute构造了一个UrlMapping对象来将url和权限聚合,然后添加到ExpressionInterceptUrlRegistry对象中,这和我们在上面根据类的注释信息推测出的结果一致。

而且从源码的属性定义来看,SpringSecurity的权限应该有固定的6种。

通过上面的源码分析,我们可以知道的是对于Url的访问权限设置是AuthorizedUrl的工作,而AuthorizedUrl是在AbstractRequestMatcherRegistry抽象类中创建的。如果我们想给url进行权限细化,则还需要看下这个类中还提供了哪些方法:

e30d5de857197173d150e899c6d0cf50.png

584935e25a27745e5f5046eec24223f6.png

3ee045d3b98ff707917f910fd8f725f0.png

从源码中可以看出还有额外的三种url映射设置方式,不过url匹配使用的都是ant风格的表达式。

这三种设置分别是:

1、固定请求方法,但匹配所有url的AuthorizedUrl

2、固定请求方法,固定url的AuthorizedUrl

3、只固定url的AuthorizedUrl

而且1其实就是使用2来进行创建的。

现在我们已经知道了使用antMatchers()方法就可以自定义拦截Url创建AuthorizedUrl,但是授权除了authenticated,还有哪些方式呢?当然是来看下AuthorizedUrl还提供了哪些授权方法。

350e5c428638492abbfd5a50c349cf10.png

not表示不具有xxx权限

1db05185c4bb9af3b1599dd399a3ecd6.png

225604e0ed91355b897f295f19181de1.png

718406eb9d52222b1c26f040b5f24343.png

1c5bd2f641b6988d45835bb28f0b6619.png

6619af425bea1d0e2a364792bdc5a735.png

72f32721562922d6b28bc9de3ecfef22.png

上面几种是框架提供的默认权限,同来进行粗粒度的授权工作

7836d707fd289f0f4054d6511197e25e.png

4ce72b459d74f75466ba01e94ffdfc30.png

560834d7c239cda9ea61068f83e6d773.png

ec86b8513c7fa92df590b35908b70b65.png

可以发现框架中除了提供可选的6种默认权限之外,还提供了两种自定义的设置方式:

1、hasRole

2、hasAuthority

c58bbaaaf04629f1662f7e3280c76181.png

通过源码可以看出,其实返回的就是一个SPEL表达式,role和authority的不同之处在于如果使用hasRole则表示使用的是url角色,注册时不能带ROLE_前缀,框架会给你自动补上这个前缀。使用authority表示使用的是url权限,则没有任何限制。

也就是说通过框架提供的这种特性我们可以对于同一个url进行两种授权设置,一种基于角色,一种基于权限。比如我们想设置"/user"所有子路径必须具有ROLE_USER角色或者MODEL_USER_REQUEST权限才能进行访问则可以像下面这样设置:

5100322597e4b36344b77ed4c0a156c1.png

2、SecurityConfigurer的init方法

通过上面的源码分析,我们已经知道了如何使用框架提供的功能去自定义url授权,但是我们仍不了解框架是如何鉴权的。所以还需要继续看下相关的源码,才能够进行我们自己的权限系统的设计。

之前的文章中已经介绍了SpringSecurity的基本设计模式,所以SecurityConfigurer的init方法是我们阅读源码时必须了解的。

但是通过源码可以看到ExpressURLAuthorizationConfigurer以及其所有父类都没有重写init方法,根据init方法的初衷,可以知道ExpressURLAuthorizationConfigurer是一个功能相对比较独立的模块,因为他没有经过init方法设置共享变量。

3、SecurityConfigurer的configure方法

init方法是用来设置共享变量到SecurityBuilder中的,而configure方法则是用来处理SecurityBuilder的直接属性。ExpressURLAuthorizationConfigurer中并没有重写抽象父类AbstractInterceptUrlConfigurerd的configure方法:

80b6ee7de977df8542c6439e17764570.png

metadataSource是之后用来鉴权的的元数据,可以看一下他有哪些数据,了解一下鉴权需要用到的信息:

f02efd4eeb8b2923befcce1d3d37ca81.png

021b12640300e9a7edaae2bce77a4cfa.png

从源码来看,metadataSource中只有一个处理SPEL表达式的handler和我们授权的url的信息。不过从源码来看,如果设置了多个相同的RequestMatcher,只有最后的会生效,前面设置的会被覆盖掉,所以我们设想的给同一个url即授予角色又授予权限是在默认情况下是行不通了~

6f127dcc7a3c61892a18129b4b7cfa87.png

如图所示,我们定义了三个antMatcher,但最终元数据中只有两条,因为有两个requestMatcher重复了。

configure中只有这个需要特别关注,下面就是创建filter了,没什么特别的地方。

二、SpringSecurity中的鉴权源码分析

通过上面的几个关键点,现在我们已经知道如何自定义url的授权,也知道了我们定义的授权哪些是有效的,而最终的鉴权则是FilterSecurityInterceptor来完成的

FilterSecurityInterceptor作为一个filter,所以鉴权相关肯定是在doFilter方法中进行的。

a806c22c53c27d69b1df0f82d6758f9a.png

eec5fb8f188bab8b15b721840e8326d6.png

从源码来看对于同一个请求只需要鉴权一次,所以对于请求的转发,是不是可以越权访问呢?网上搜了一下,好像没发现相关资料,因为请求的转发还算做同一次请求,所以我也是猜测,以后抽空测验一下~。

1bdd16c15407528daec0ca2bff40020e.png

鉴权操作主要是父类来做的,核心代码只有这一行。authenticated是我们的登录信息,attributes则是从metadataSource中取出当前访问url所需要的权限信息:

42024f31ece39628fe924c9ebbd21c5a.png

比如我们访问根目录"/",则对应着authenticated这条权限。如果没有找到匹配当前访问url的授权定义,默认情况下不做拦截。不过这里有一点需要我们注意!!!我们来看下attributes是如何取出来的:

ffa0e2ebb2384fc0a422368069d1f817.png

不是取出所有匹配项,而是取出第一条匹配项!所以对于同一个url,如果定义了不同的antPath都匹配到了,结果是只有定义在前面的授权生效!!!所以下面的定义是错误的

7e618a2eae3d1d82ec0399c37ec3304a.png

正确的定义方式应该是:

0ec987a3ce214dd993dc7f3ccd7ab91c.png

将anyRequest的授权放在最后面。

accessDecisionManager就是用来进行鉴权的类。框架默认的AccessDecisionManager为:AffirmativeBased

9fbefeec8c093b4959500da9642e3b8f.png

框架中一共定义了三种decisionManager。我们还是先了解一下框架默认行为是否满足我们的需求:

2195966e7bf135be27cb9894f819693c.png

从注释来看AffirmativeBased的鉴权其实是委托给AccessDecisionVoter来做的,并且只要其内部的任意一个AccessDecisionVoter鉴权通过,则算作当前请求有权限访问。

665755553acc60f46890c3bca5a8460e.png

e6ca4c3edc4e5ce4d864524955d8d325.png

1aa453a0e182e8132b30cb665d45fcd3.png

框架中虽然定义了很多voter,但默认用到的只有一个,即WebExpressionVoter。我们定义的SPEL表达式权限就是交给这个voter进行鉴定的,鉴权方法为vote:

692d7aaa47592aadcc2f1ab7fb4b1130.png

WebExpressionVoter鉴权有三种结果:弃权,肯定,否定。当返回肯定的时候则表示鉴权通过,当返回弃权和否定的时候,将鉴权交给下一个voter进行。

a1dcc3b087f9f7ff6a4c4e75eee11fb8.png

WebExpressionVoter最主要的鉴权是上面这段代码,即将用户的登陆信息中的权限和SPEL表达式进行对比:

59805fe624d56a47a041891f167da19e.png

fbbd2aa7752a519ef1c10c397afca37a.png

鉴权的代码十分复杂,通过debug发现原来是通过反射调用了SecurityExpressionRoot类的hasRole方法:

e1dc2fa702dfc766bc9ebbab7464e3ba.png

hasRole方法的参数就是我们给url授予的权限信息:

2777da3f5bea6e762932cc7b124458aa.png

586967f60bf7efda2ad4e95b50e349a5.png

5308127d926169b0a0b1f26f822f0872.png

db8894262cb8e9530a5af351683e7f1b.png

edc25dd5718ba5f46c7bc33c2d96b633.png

从源码可以看到,框架认为系统所具有的权限为从登陆信息中的authorities字段,是一个GrantedAuthority类型的List。

f1f163cff1b1ebf01a707c6e8a095986.png

而框架中默认使用的GrantedAuthority则是SimpleGrantedAuthority,其实就是一个字符串的包装类,现在我们已经知道如何去给用户赋予权限了了,只需要将权限字符串设置到其登陆信息的authorities字段即可。

鉴权很简单,只要用户所具有的角色和url中授予的角色任一匹配,则表示鉴权通过。不过有一点可能你会奇怪,为什么这里是调用的hasRole而不是hasAuthority函数呢?回过头来看下我们的配置内容:

8e26f70c73a28c562568c535565ac544.png

根据前面的分析,我们已经知道了对于相同的url,后面的配置会覆盖前面的所以只有hasRole生效了。如果我们来调换一下这两个配置的位置:

f73cf55d1a2db3e768d5a3ce7a81cde3.png

将hasRole放到前面,再来看一下结果:

037d15b47b0c0d0556eac76704089648.png

不出所料,使用的是authority进行鉴权。

三、总结

通过本篇文章我们知道了框架授权和鉴权的原理。

授权:在ExpressionInterceptURLRegistry中注册AuthorizedUrl对象,AuthorizedUrl对象包含了匹配url的RequestMatcher以及表示权限的AttributeConfigure。权限分为authority和role两种,对于相同的url,后面定义的授权会覆盖前面的授权。而对于不相同但是被多个antPath都匹配到的url,则前面的定义会覆盖掉后面的定义,也就是说对于覆盖范围越广的授权定义,越要放在配置的后面。

鉴权:使用用户登录信息中的authorities字段所表示的权限和配置中对url的授权进行匹配,匹配通过则表示该用户具有访问权限,匹配不通过则表示该用户没有访问权限。使用role授权则不能以ROLE作为字符串的开头,而且框架会在鉴权时给字符串加上ROLE_前缀。而使用authority时则没有限制,鉴权时是字符串的完全匹配。

所以我们要向自定义权限系统其实只需要考虑两个方面:

1、使用authoriy还是role进行授权

2、什么时候将权限设置到用户的authorities字段

下一节就让我们结合前面的所有内容来自定义一个权限系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值