文章目录
权限管理
13.1什么是权限管理
Spring security支持多种不同的认证方式,但是无论开发者使用哪种认证方式,都不会影响授权功能的使用,spring security很好地实现了认证和授权两大功能的解耦。
13.2Spring security权限管理策略
从技术上来说,spring security中提供的权限管理功能主要有两种类型:
- 基于过滤器的权限管理(
FilterSecurityInterceptor
)。- 基于AOP的权限管理(
MethodSecurityInterceptor
)。基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来之后,根据HTTP请求地址进行权限校验。
基于AOP的权限管理则主要用来处理方法级别的权限问题。当需要调用某一个方法时,通过AOP将操作拦截下来,然后判断用户是否具备相关的权限,如果具备,则允许方法调用;否则禁止方法调用。
13.3核心概念
13.3.1角色与权限
在spring security中,当用户登录成功后,当前登录用户信息将保存在
Authentication
对象中,该对象中有一个
getAuthorities
方法,用来返回当前对象所具备的权限信息,也就是已经授予当前登录用户的权限,
getAuthorities
方法返回值是
Collection<? extends GrantedAuthority>
,即集合中存放的是
GrantedAuthority
的子类,当需要进行权限判断的时候,就会调用该方法获取用户的权限,进而做出判断。
无论用户的认证方式是用户名/密码形式、remember-me形式,还是其他如CAS、OAuth2等认证方式,最终用户的权限信息都可以通过getAuthorities
方法获取。
那么对于
Authentication#getAuthorities
方法的返回值,应该如何理解:
- 从设计层面来讲,角色和权限是两个完全不同的东西:权限就是一些具体的操作,例如针对员工数据的读权限(
READ_EMPLOYEE
)和针对员工数据的写权限(WRITE_EMPLOYEE
);角色则是某些权限的集合,例如管理员角色ROLE_ADMIN
、普通用户角色ROLE_USER
。- 从代码层面来讲,角色和权限并没有太大的不同,特别是在spring security中,角色和权限的处理的方式基本上是一样的,唯一的区别在于spring security在多个地方会自动给角色添加一个
ROLE_
前缀,而权限则不会自动添加任何前缀。至于
Authentication#getAuthorities
方法的返回值,则要分情况来对待:
- 如果权限系统设计比较简单,就是
用户<=>权限<=>资源
三者之间的关系,那么getAuthorities
方法的含义就很明确,就是返回用户的权限。- 如果权限系统设计比较复杂,同时存在角色和权限的概念,如
用户<=>角色<=>权限<=>资源
(用户关联角色、角色关联权限、权限关联资源),此时可以将getAuthorities
方法的返回值当做权限来理解。由于spring security并未提供相关的角色类,因此这个时候需要自定义角色类。对于第一种情况,相对来说比较好理解,这里简单介绍一下第二种情况。
如果系统同时存在角色和权限,可以使用GrantedAuthority
的实现类
SimpleGrantedAuthority
来表示一个权限,在
SimpleGrantedAuthority
类中,可以将权限描述为一个字符串,如
READ_EMPLOYEE
、
WRITE_EMPLOYEE
。据此,定义角色类如下:
publicclassRoleimplementsGrantedAuthority{privateString name;privateList<SimpleGrantedAuthority> allowedOperations =newArrayList<>();@OverridepublicStringgetAuthority(){return name;}// 省略getter/setter}
角色继承自
GrantedAuthority
,一个角色对应多个权限。然后在定义用户类的时候,将角色转换为权限即可:
publicclassUserimplementsUserDetails{privateList<Role> roles =newArrayList<>();@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){List<SimpleGrantedAuthority> authorities=newArrayList<>();for(Role role : roles){
authorities.addAll(role.getAllowedOperations());}return authorities.stream().distinct().collect(Collectors.toList());}// 省略getter/setter}
整体上来说,设计层面上,角色和权限是两个东西;代码层面上,角色和权限其实差别不大,注意区分即可。
13.3.2角色继承
角色继承就是指角色存在一个上下级的关系,例如
ROLE_ADMIN
继承自
ROLE_USER
,那么
ROLE_ADMIN
就自动具备
ROLE_USER
的所有权限。
Spring security中通过RoleHierarchy
类对角色继承提供支持:
publicinterfaceRoleHierarchy{/**
* 该方法返回用户真正可触达的权限。例如,假设用户定义了ROLE_ADMIN继承自ROLE_USER,ROLE_USER继承自ROLE_GUEST,
* 现在当前用户角色是ROLE_ADMIN,但是它实际可访问的资源也包含ROLE_USER和ROLE_GUEST能访问的资源。该方法就是根据
* 当前用户所具有的角色,从角色层级映射中解析出用户真正可触达的权限。
*/Collection<?extendsGrantedAuthority>getReachableGrantedAuthorities(Collection<?extendsGrantedAuthority> authorities);}
RoleHierarchy
只有一个实现类
RoleHierarchyImpl
,开发者一般通过
RoleHierarchyImpl
类来定义角色的层级关系:
@BeanRoleHierarchyroleHierarchy(){RoleHierarchyImpl hierarchy =newRoleHierarchyImpl();// ROLE_A继承自ROLE_B,ROLE_B继承自ROLE_C,ROLE_C继承自ROLE_D
hierarchy.setHierarchy("ROLE_A > ROLE_B > ROLE_C > ROLE_D");return hierarchy;}
这样的角色层级,在
RoleHierarchyImpl
类中首先通过
buildRolesReachableInOneStepMap
方法解析成
Map
集合:
ROLE_A -> ROLE_B
ROLE_B -> ROLE_C
ROLE_C -> ROLE_D
然后再通过
buildRolesReachableInOneOrMoreStepsMap
方法对上面的集合再次解析,最终解析结果如下:
ROLE_A ->[ROLE_B, ROLE_C, ROLE_D]
ROLE_B ->[ROLE_C, ROLE_D]
ROLE_C -> ROLE_D
最后通过
getReachableGrantedAuthorities
方法从该
Map
集合中获取用户真正可触达的权限。
13.3.3两种处理器
基于过滤器的权限管理(
FilterSecurityInterceptor
)和基于AOP的权限管理(
MethodSecurityInterceptor
),无论是哪种,都涉及一个前置处理器和后置处理器。
在基于过滤器的权限管理中,请求首先到达过滤器
FilterSecurityInterceptor
,在其执行过程中,首先会由前置处理器去判断发起当前请求的用户是否具备相应的权限,如果具备,则请求继续走下去,到达目标方法并执行完毕。在响应时,又会经过
FilterSecurityInterceptor
过滤器,此时由后置处理器再去完成其他收尾工作。在基于过滤器的权限管理中,后置处理器一般是不工作的。这也很好理解,因为基于过滤器的权限管理,实际上就是拦截请求URL地址,这种权限管理方式粒度较粗,而且过滤器中拿到的是响应的
HttpServletResponse
对象,对其所返回的数据做二次处理并不方便。
在基于方法的权限管理中,目标方法的调用会被MethodSecurityInterceptor
拦截下来,实现原理当然就是大家所熟知的AOP机制。当目标方法的调用被
MethodSecurityInterceptor
拦截下之后,在其
invoke
方法中首先会由前置处理器去判断当前用户是否具备调用目标方法所需要的权限,如果具备,则继续执行目标方法。当目标方法执行完毕并给出返回结果后,在
MethodSecurityInterceptor#invoke
方法中,由后置处理器再去对目标方法的返回结果进行过滤或者鉴权,然后在
invoke
方法中将处理后的结果返回。
13.3.4前置处理器
要理解前置处理器,需要先了解投票器。
投票器
投票器是spring security权限管理功能中的一个组件,顾名思义,投票器的作用就是针对是否允许某一个操作进行投票。当请求的URL地址被拦截下来之后,或者当调用的方法被AOP拦截下来之后,都会调用投票器对当前操作进行投票,以便决定是否允许当前操作。
在spring security中,投票器由AccessDecisionVoter
来定义:
publicinterfaceAccessDecisionVoter<S>{// 表示投票通过int ACCESS_GRANTED =1;// 表示弃权int ACCESS_ABSTAIN =0;// 表示拒绝int ACCESS_DENIED =-1;// 用来判断是否支持处理ConfigAttribute对象booleansupports(ConfigAttribute attribute);// 用来判断是否支持处理受保护的安全对象booleansupports(Class<?> clazz);/**
* 具体的投票方法,根据用户所具有的权限以及当前请求需要的权限进行投票。
* @param authentication 进行调用的调用者,可以提取出来当前用户所具备的权限
* @param object 受保护的安全对象,如果受保护的是URL地址,则object就是一个FilterInvocation对象;如果受保护的是一个
* 方法,则object就是一个MethodInvocation对象
* @param attributes 访问受保护对象所需要的权限
* @return 定义的三个常量之一
*/intvote(Authentication authentication,S object,Collection<ConfigAttribute> attributes);}
Spring security中为
AccessDecisionVoter
提供了诸多不同的实现类:
RoleVoter
:RoleVoter
是根据登录主体的角色进行投票,即判断当前用户是否具备受保护对象所需要的角色。需要注意的是,默认情况下角色需以ROLE_
开始,否则supports
方法直接返回false
,不进行后续的投票操作。RoleHierarchyVoter
:RoleHierarchyVoter
继承自RoleVoter
,投票逻辑和RoleVoter
一致,不同的是,RoleHierarchyVoter
支持角色的继承,它通过RoleHierarchyImpl
对象对用户所具有的角色进行解析,获取用户真正可触达的角色;而RoleVoter
则直接调用authentication.getAuthorities()
方法获取用户的角色。WebExpressionVoter
:基于URL地址进行权限控制时的投票器(支持SpEL)。Jsr250Voter
:处理JSR-250权限注解的投票器,如@PermitAll
、@DenyAll
等。AuthenticatedVoter
:AuthenticatedVoter
用于判断当前用户的认证形式,它有三种取值:IS_AUTHENTICATED_FULLY
、IS_AUTHENTICATED_REMEMBERED
、IS_AUTHENTICATED_ANONYMOUSLY
。其中,IS_AUTHENTICATED_FULLY
要求当前用户既不是匿名用户也不是通过remember-me进行认证;IS_AUTHENTICATED_REMEMBERED
则在前者的基础上,允许用户通过remember-me进行认证;IS_AUTHENTICATED_ANONYMOUSLY
则允许当前用户通过remember-me进行认证,也允许当前用户是匿名用户。AbstractAclVoter
:基于ACL进行权限控制时的投票器。这是一个抽象类,没有绑定到具体的ACL系统。PreInvocationAuthorizationAdviceVoter
:处理@PreFilter
和@PreAuthorize
注解的投票器。这些投票器在具体使用中,可以单独使用一个,也可以多个一起使用。如果上面这些投票器都无法满足需求,开发者也可以自定义投票器。需要注意的是,投票结果并非最终结果(通过或拒绝),最终结果还是要看决策器(
AccessDecisionManager
)。
决策器
决策器由
AccessDecisionManager
负责,
AccessDecisionManager
会同时管理多个投票器,由
AccessDecisionManager
调用投票器进行投票,然后根据投票结果做出相应的决策,所以将
AccessDecisionManager
也称作是一个决策管理器:
publicinterfaceAccessDecisionManager{// 核心的决策方法,在这个方法中判断是否允许当前URL或者方法的调用,如果不允许,则抛出AccessDeniedException异常voiddecide(Authentication authentication,Object object,Collection<ConfigAttribute> configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;// 用来判断是否支持处理ConfigAttribute对象booleansupports(ConfigAttribute attribute);// 用来判断是否支持当前安全对象booleansupports(Class<?> clazz);}
可以看出,
AccessDecisionManager
有一个实现类
AbstractAccessDecisionManager
,一个
AbstractAccessDecisionManager
对应多个投票器。多个投票器针对同一个请求可能会给出不同的结果,那么听谁的呢,这就要看决策器了。
AffirmativeBased
:一票通过机制,即只要有一个投票器通过就可以访问(默认即此)。UnanimousBased
:一票否决机制,即只要有一个投票器反对就不可以访问。ConsensusBased
:少数服从多数机制。如果是平局并且至少有一张赞同票,则根据allowIfEqualGrantedDeniedDecisions
参数的取值来决定,如果该参数的取值为true
,则可以访问,否则不可以访问。如果这三个决策器无法满足需求,开发者也可以自定义类继承自
AbstractAccessDecisionManager
实现自己的决策器。
这就是前置处理器中的大致逻辑,无论是基于URL地址的权限管理,还是基于方法的权限管理,都是在前置处理器中通过
AccessDecisionManager
调用
AccessDecisionVoter
进行投票,进而做出相应的决策。
13.3.5后置处理器
后置处理器一般只在基于方法的权限控制中会用到,当目标方法执行完毕后,通过后置处理器可以对目标方法的返回值进行权限校验或者过滤。
后期处理器由AfterInvocationManager
负责:
// 和AccessDecisionManager高度相似publicinterfaceAfterInvocationManager{/**
* 主要的区别在于decide方法的参数和返回值。当后置处理器执行时,被权限保护的方法以及执行完毕,后置处理器主要是对执行的结果
* 进行过滤,所以decide方法中有一个returnedObject参数,这就是目标方法的执行结果,decide方法的返回值就是对returnedObject
* 对象进行过滤/鉴权后的结果
*/Objectdecide(Authentication authentication,Object object,Collection<ConfigAttribute> attributes,Object returnedObject)throwsAccessDeniedException;booleansupports(ConfigAttribute attribute);booleansupports(Class<?> clazz);}
和
AuthenticationManager
、
ProviderManager
以及
AuthenticationProvider
相似。
AfterInvocationManager
只有一个实现类
AfterInvocationProviderManager
,一个
AfterInvocationProviderManager
关联多个
AfterInvocationProvider
。在
AfterInvocationManager
的
decide
以及
supports
方法执行时,都是遍历
AfterInvocationProvider
并执行它里边对应的方法。
AfterInvocationProvider
有多个不同的实现类,常见到的是
PostInvocationAdviceProvider
,该类主要用来处理
@PostAuthorize
和
@PostFilter
注解配置的过滤器。
13.3.6权限元数据
*
ConfigAttribute
在投票器具体的投票方法
vote
中,受保护对象所需要的权限保存在一个
Collection<ConfigAttribute>
集合中,集合中的对象是
ConfigAttribute
,而不是所熟知的
GrantedAuthority
。
ConfigAttribute
用来存储与安全系统相关的配置属性,也就是系统关于权限的配置,通过
ConfigAttribute
来存储:
publicinterfaceConfigAttributeextendsSerializable{StringgetAttribute();}
该接口只有一个
getAttribute
方法返回具体的权限字符串,而
GrantedAuthority
中则是通过
getAuthority
方法返回用户所具有的权限,两者返回值都是字符串。所以虽然是
ConfigAttribute
和
GrantedAuthority
两个不同的对象,但是最终是可以比较的。
WebExpressionConfigAttribute
:如果用户是基于URL地址来控制权限并且支持SpEL,那么默认配置的权限控制表达式最终会被封装为WebExpressionConfigAttribute
对象。SecurityConfig
:如果用户使用了@Secured
注解来控制权限,那么配置的权限就会被封装为SecurityConfig
对象。Jsr250SecurityConfig
:如果用户使用了JSR-250相关的注解来控制权限(如@PermitAll
、@DenyAll
),那么配置的权限就会被封装为Jsr250SecurityConfig
对象。PreInvocationExpressionAttribute
:如果用户使用了@PreAuthorize
、@PreFilter
注解来控制权限,那么相关的配置就会被封装为PreInvocationExpressionAttribute
对象。PostInvocationExpressionAttribute
:如果用户使用了@PostAuthorize
、@PostFilter
注解来控制权限,那么相关的配置就会被封装为PostInvocationExpressionAttribute
对象。
*
SecurityMetadataSource
当投票器在投票时,需要两方面的权限:其一是当前用户具备哪些权限;其二是当前访问的URL或者方法需要哪些权限才能访问。投票器所做的事情就是对这两种权限进行比较。
用户具备的权限保存在authentication
中,而当前访问的URL或者方法所需要的权限和
SecurityMetadataSource
有关。
SecurityMetadataSource
所做的事情,就是提供受保护对象所需要的权限。例如,用户访问了一个URL地址,访问该URL地址所需要的权限就由
SecurityMetadataSource
来提供。
publicinterfaceSecurityMetadataSourceextendsAopInfrastructureBean{/**
* 根据传入的安全对象参数返回其所需要的权限。如果受保护的对象是一个URL地址,那么传入的参数object就是一个FilterInvocation
* 对象;如果受保护的是一个方法,那么object就是一个MethodInvocation对象。
*/Collection<ConfigAttribute>getAttributes(Object object)throwsIllegalArgumentException;// 返回所有的角色/权限,以便验证是否支持。不过这个方法不是必须的,也可以直接返回nullCollection<ConfigAttribute>getAllConfigAttributes();// 返回当前的SecurityMetadataSource是否支持受保护的对象如FilterInvocation或者MethodInvocationbooleansupports(Class<?> clazz);}
可以看到,直接继承自
SecurityMetadataSource
的接口主要有两个:
FilterInvocationSecurityMetadataSource
和
MethodSecurityMetadataSource
。
FilterInvocationSecurityMetadataSource
:这是一个空接口,更像是一个标记。如果被保护的对象是一个URL地址,那么将由该接口的实现类提供访问该URL地址所需要的权限。MethodSecurityMetadataSource
:也是一个接口,如果受保护的对象是一个方法,那么将通过该接口的实现类来获取受保护对象所需要的权限。
FilterInvocationSecurityMetadataSource
有一个子类
DefaultFilterInvocationSecurityMetadataSource
,该类中定义了一个如下格式的
Map
集合:
privatefinalMap<RequestMatcher,Collection<ConfigAttribute>> requestMap;
可以看到,在这个
Map
集合中,
key
是一个请求匹配器,
value
则是一个权限集合,也就是说,
requestMap
中保存了请求URL和其所需权限之间的映射关系。在spring security中,如果直接在
configure(HttpSecurity)
方法中配置URL请求地址拦截:
http.autorizeRequests()// 访问/admin/**格式的URL地址需要admin角色.antMatchers("/admin/**").hasRole("admin")// 访问/user/**格式的URL地址需要user角色.antMatchers("/user/**").access("hasRole('user')")// 其余地址认证后即可访问.anyRequest().access("isAuthenticated()")
这段请求和权限之间的映射关系,会经过
DefaultFilterInvocationSecurityMetadataSource
的子类
ExpressionBasedFilterInvocationSecurityMetadataSource
进行处理,并最终将映射关系保存到
requestMap
变量中,以备后续使用。
在实际开发中,URL地址以及访问它所需要的权限可能保存在数据库中,此时可以自定义类实现FilterInvocationSecurityMetadataSource
接口,然后重写
getAttributes
方法,在该方法中,根据当前请求的URL地址去数据库中查询其所需要的权限,然后将查询结果封装为相应的
ConfigAttribute
集合返回即可。
如果是基于方法的权限管理,那么对应的MethodSecurityMetadataSource
实现类就比较多了:
PrePostAnnotationSecurityMetadataSource
:@PreAuthorize
、@PreFilter
、@PostAuthorize
、@PostFilter
四个注解所标记的权限规则,将由该类负责提供。SecuredAnnotationSecurityMetadataSource
:@Secured
注解所标记的权限规则,将由该类负责提供。MapBasedMethodSecurityMetadataSource
:基于XML文件配置的方法权限拦截规则(基于sec:protect
节点),将由该类负责提供。Jsr250MethodSecurityMetadataSource
:JSR-250相关的注解(如@PermitAll
、@DenyAll
)所标记的权限规则,将由该类负责提供。
13.3.7权限表达式
可以在请求的URL或者访问的方法上,通过SpEL来配置需要的权限。内置的权限表达式:
配置类名称作用
hasRole(String role)
当前用户是否具备指定角色
hasAnyRole(String... roles)
当前用户是否具备指定角色中的任意一个
hasAuthority(String authority)
当前用户是否具备指定的权限
hasAnyAuthority(String... authorities)
当前用户是否具备指定权限中的任意一个
principal
代表当前登录主体
Principal
authentication
这个是从
SecurityContext
中获取到的
Authentication
对象
permitAll
允许所有的请求/调用
denyAll
拒绝所有的请求/调用
isAnonymouse()
当前用户是否是一个匿名用户
isRememberMe()
当前用户是否是通过remember-me自动登录
isAuthenticated()
当前用户是否已经认证成功
isFullyAuthenticated()
当前用户是否既不是匿名用户又不是通过remember-me自动登录的
hasPermission(Object target, Object permission)
当前用户是否具备指定目标的指定权限
hasPermission(Object targetId, String targetType, Object permission)
当前用户是否具备指定目标的指定权限
hasIpAddress(String ipAddress)
当前请求IP地址是否为指定IP
Spring security中通过
SecurityExpressionOperations
接口定义了基本的权限表达式:
publicinterfaceSecurityExpressionOperations{AuthenticationgetAuthentication();booleanhasAuthority(String authority);booleanhasAnyAuthority(String... authorities);booleanhasRole(String role);booleanhasAnyRole(String... roles);booleanpermitAll();booleandenyAll();booleanisAnonymous();booleanisAuthenticated();booleanisRememberMe();booleanisFullyAuthenticated();booleanhasPermission(Object target,Object permission);booleanhasPermission(Object targetId,String targetType,Object permission);}
返回值为
boolean
类型的就是权限表达式,如果返回
true
,则表示权限校验通过,否则表示权限校验失败。
*
SecurityExpressionRoot
SecurityExpressionRoot
对
SecurityExpressionOperations
接口做了基本的实现,并在此基础上增加了
principal
。
接口的实现原理都很简单,所以说一下实现思路。
hasAuthority
、
hasAnyAuthority
、
hasRole
以及
hasAnyRole
四个方法主要是将传入的参数和
authentication
对象中保存的用户权限进行比对,如果用户具备相应的权限就返回
true
,否则返回
false
。
permitAll
方法总是返回
true
,而
denyAll
方法总是返回
false
。
isAnonymous
、
isAuthenticated
、
isRememberMe
以及
isFullyAuthenticated
四个方法则是根据对
authentication
对象的分析,然后返回
true
或者
false
。最后的
hasPermission
则需要调用
PermissionEvaluator
中对应的方法进行计算,然后返回
true
或者
false
。
SecurityExpressionRoot
中定义的表达式既可以在基于URL地址的权限管理中使用,也可以在基于方法的权限管理中使用。
*
WebSecurityExpressionRoot
继承自
SecurityExpressionRoot
,并增加了
hasIpAddress
方法,用来判断请求的IP地址是否满足要求。
在spring security中,如果权限管理是基于URL地址的,那么使用的是WebSecurityExpressionRoot
,换句话说,这时可以使用
hasIpAddress
表达式。
*
MethodSecurityExpressionOperations
定义了基于方法的权限管理时一些必须实现的接口,主要是参数对象的
get/set
、返回对象的
get/set
以及返回受保护的对象。
*
MethodSecurityExpressionRoot
实现了
MethodSecurityExpressionOperations
接口,并对其定义的方法进行了实现。
MethodSecurityExpressionRoot
虽然也继承自
SecurityExpressionRoot
,但是并未扩展新的表达式,换句话说,
SecurityExpressionRoot
中定义的权限表达式在方法上也可以使用,但是
hasIpAddress
不可以在方法上使用。
13.4基于URL地址的权限管理
基于URL地址的权限管理主要是通过过滤器
FilterSecurityInterceptor
来实现的。如果开发者配置了基于URL地址的权限管理,那么
FilterSecurityInterceptor
就会被自动添加到spring security过滤器链中,在过滤器链中拦截下请求,然后分析当前用户是否具备请求所需要的权限,如果不具备,则抛出异常。
FilterSecurityInterceptor
将请求拦截下来之后,会交给
AccessDecisionManager
进行处理,
AccessDecisionManager
则会调用投票器进行投票,然后对投票结果进行决策,最终决定请求是否通过。
13.4.1基本用法
@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{/**
* 定义用户以及相应的角色和权限。
* 对于复杂的权限管理系统,用户和角色关联,角色和权限关联,权限和资源关联;对于简单的权限管理系统,
* 用户和权限关联,权限和资源关联。无论是哪种,用户都不会和角色以及权限同时直接关联。反应到代码上
* 就是roles方法和authorities方法不能同时调用,如果同时调用,后者会覆盖前者(可以自行查看源码,
* 最终都会调用authorities(Collection<? extends GrantedAuthority> authorities)方法)。
* 需要注意的是,spring security会自动给用户的角色加上ROLE_前缀。
*/@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.inMemoryAuthentication()// javaboy具有ADMIN角色.withUser("javaboy").password("{noop}123").roles("ADMIN").and()// zhangsan具有USER角色.withUser("zhangsan").password("{noop}123").roles("USER").and()// itboyhub具有READ_INFO权限.withUser("itboyhub").password("{noop}123").authorities("READ_INFO");}/**
* 配置拦截规则
*/@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests()// 用户必须具备ADMIN角色才可以访问/admin/**格式的地址.antMatchers("/admin/**").hasRole("ADMIN")// 用户必须具备USER和ADMIN任意一个角色,才可以访问/user/**格式的地址.antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")// 用户必须具备READ_INFO权限,才可以访问/getInfo接口.antMatchers("/getInfo").hasAuthority("READ_INFO")// 剩余的请求只要是认证后的用户就可以访问.anyRequest().access("isAuthenticated()").and().formLogin().and().csrf().disable();}}
配置其实很好理解,但是有一些需要注意的地方:
- 大部分的表达式都有对应的方法可以直接调用,例如
hasRole
方法对应的就是hasRole
表达式。开发者为了方便可以直接调用hasRole
方法,但是最终还是会被转为表达式,当表达式执行结果为true
,请求可以通过;否则请求不通过。- Spring security会为
hasRole
表达式自动添加ROLE_
前缀,例如hasRole("ADMIN")
方法转为表达式之后,就是hasRole('ROLE_ADMIN')
,所以用户的角色也必须有ROLE_
前缀,而基于内存创建的用户会自动加上该前缀;hasAuthority
方法并不会添加任何前缀,而在用户定义时设置的用户权限也不会添加任何前缀。一言以蔽之,基于内存定义的用户,会自动给角色添加ROLE_
前缀,而hasRole
也会自动添加ROLE_
前缀;基于内存定义的用户,不会给权限添加任何前缀,而hasAuthority
也不会添加任何前缀。如果用户信息是从数据库中读取的,则需要注意ROLE_
前缀的问题。- 可以通过
access
方法来使用权限表达式,access
方法的参数就是权限表达式。- 代码的顺序很关键,当请求到达后,按照从上往下的顺序依次进行匹配。
配置完成后,再提供相应的测试接口:
@RestControllerpublicclassHelloController{@GetMapping("/hello")publicStringhello(){return"Hello";}@GetMapping("/admin/hello")publicStringadmin(){return"Hello admin";}@GetMapping("/user/hello")publicStringuser(){return"Hello user";}@GetMapping("/getInfo")publicStringgetInfo(){return"GetInfo";}}
13.4.2角色继承
@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{/**
* 如果需要配置角色继承,则只需要提供一个RoleHierarchy的实例即可
*/@BeanRoleHierarchyroleHierarchy(){RoleHierarchyImpl hierarchy =newRoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");return hierarchy;}@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{// 省略}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")// ROLE_ADMIN继承自ROLE_USER,因此可以直接访问/user/**格式的地址.antMatchers("/user/**").access("hasRole('USER')").antMatchers("/getInfo").hasAuthority("READ_INFO").anyRequest().access("isAuthenticated()").and().formLogin().and().csrf().disable();}}
13.4.3自定义表达式
如果内置的表达式无法满足需求,开发者也可以自定义表达式。
假设现在有两个接口:
@RestControllerpublicclassHelloController{/**
* 第一个接口:参数userId必须是偶数方可请求成功。
*/@GetMapping("/hello/{userId}")publicStringhello(@PathVariableInteger userId){return"Hello "+ userId;}/**
* 第二个接口:参数username必须是javaboy方可请求成功,同时这两个接口必须认证后才能访问。
*/@GetMapping("/hi")publicStringhello2User(String username){return"Hello "+ username;}}/**
* 自定义PermissionExpression类并注册到spring容器中,然后定义相应的方法。
*/@ComponentpublicclassPermissionExpression{publicbooleancheckId(Authentication authentication,Integer userId){if(authentication.isAuthenticated()){return userId %2==0;}returnfalse;}publicbooleancheck(HttpServletRequest request){return"javaboy".equals(request.getParameter("username"));}}@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{// 省略}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests()// 省略其他// 在access方法中,可以通过@符号引用一个bean并调用其中的方法。在checkId方法调用时,// #userId表示前面的userId参数.antMatchers("/hello/{userId}").access("@permissionExpression.checkId(authentication, #userId)")// 需要同时满足isAuthenticated和check方法都为true,该请求才会通过.antMatchers("/hi").access("isAuthenticated() and @permissionExpression.check(request)")// 省略其他}}
13.4.4原理剖析
接下来简单梳理一下spring security中基于URL地址进行权限管理的一个大致原理。
*
AbstractSecurityInterceptor
该类统筹着关于权限处理的一切。方法很多,不过只需要关注其中的三个方法:
beforeInvocation
、
afterInvocation
以及
finallyInvocation
。
在这三个方法中,beforeInvocation
中会调用前置处理器完成权限校验,
afterInvocation
中调用后置处理器完成权限校验,
finallyInvocation
则主要做一些校验后的清理工作。
先来看下beforeInvocation
:
protectedInterceptorStatusTokenbeforeInvocation(Object object){if(!getSecureObjectClass().isAssignableFrom(object.getClass())){thrownewIllegalArgumentException("Security invocation attempted for object "+ object.getClass().getName()+" but AbstractSecurityInterceptor only configured to support secure objects of type: "+getSecureObjectClass());}// 首先调用obtainSecurityMetadataSource方法获取SecurityMetadataSource对象,然后调用其getAttributes方法获取// 受保护对象所需要的权限Collection<ConfigAttribute> attributes =this.obtainSecurityMetadataSource().getAttributes(object);// 如果获取到的权限值为空if(CollectionUtils.isEmpty(attributes)){// 则发布PublicInvocationEvent事件,该事件在调用公共安全对象(没有定义ConfigAttributes的对象)时生成publishEvent(newPublicInvocationEvent(object));// 此时直接返回null即可returnnull;// no further work post-invocation}// 查看当前用户的认证信息是否存在if(SecurityContextHolder.getContext().getAuthentication()==null){credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"), object, attributes);}// 检查当前用户是否已经登录Authentication authenticated =authenticateIfRequired();// 尝试授权attemptAuthorization(object, attributes, authenticated);if(this.publishAuthorizationSuccess){publishEvent(newAuthorizedEvent(object, attributes, authenticated));}// 临时替换用户身份,不过默认情况下,runAsManager的实例是NullRunAsManager,即不做任何替换,所以runAs为nullAuthentication runAs =this.runAsManager.buildRunAs(authenticated, object, attributes);if(runAs !=null){// 如果runAs不为空,则将SecurityContext中保存的用户信息修改为替换的用户对象,然后返回一个InterceptorStatusToken。// InterceptorStatusToken对象中保存了当前用户的SecurityContext对象,假如进行了临时用户替换,在替换完成后,最终// 还是要恢复成当前用户身份的SecurityContext origCtx =SecurityContextHolder.getContext();SecurityContext newCtx =SecurityContextHolder.createEmptyContext();
newCtx.setAuthentication(runAs);SecurityContextHolder.setContext(newCtx);// need to revert to token.Authenticated post-invocationreturnnewInterceptorStatusToken(origCtx,true, attributes, object);}// 如果runAs为空,则直接创建一个InterceptorStatusToken对象返回即可returnnewInterceptorStatusToken(SecurityContextHolder.getContext(),false, attributes, object);}privatevoidattemptAuthorization(Object object,Collection<ConfigAttribute> attributes,Authentication authenticated){try{// 核心功能:进行决策,该方法中会调用投票器进行投票,如果该方法执行抛出异常,则说明权限不足this.accessDecisionManager.decide(authenticated, object, attributes);}catch(AccessDeniedException ex){publishEvent(newAuthorizationFailureEvent(object, attributes, authenticated, ex));throw ex;}}
再来看下
finallyInvocation
方法:
/**
* 如果临时替换了用户身份,那么最终要将用户身份恢复,finallyInvocation方法所做的事情就是恢复用户身份。这里的参数token就是
* beforeInvocation方法的返回值,用户原始的身份信息都保存在token中,从token中取出用户身份信息,并设置到SecurityContextHolder
* 中去即可。
*/protectedvoidfinallyInvocation(InterceptorStatusToken token){if(token !=null&& token.isContextHolderRefreshRequired()){SecurityContextHolder.setContext(token.getSecurityContext());}}
最后再来看看
afterInvocation
方法:
/**
* 该方法接收两个参数,第一个参数token就是beforeInvocation方法的返回值,第二个参数returnedObject则是受保护对象的返回值,
* afterInvocation方法的核心工作就是调用afterInvocationManager.decide方法对returnedObject进行过滤,然后将过滤后的
* 结果返回。
*/protectedObjectafterInvocation(InterceptorStatusToken token,Object returnedObject){if(token ==null){// public objectreturn returnedObject;}finallyInvocation(token);// continue to clean in this method for passivityif(this.afterInvocationManager !=null){// Attempt after invocation handlingtry{
returnedObject =this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(),
token.getSecureObject(), token.getAttributes(), returnedObject);}catch(AccessDeniedException ex){publishEvent(newAuthorizationFailureEvent(token.getSecureObject(), token.getAttributes(),
token.getSecurityContext().getAuthentication(), ex));throw ex;}}return returnedObject;}
*
FilterSecurityInterceptor
基于URL地址的权限管理,此时最终使用的是
AbstractSecurityInterceptor
的子类
FilterSecurityInterceptor
,这是一个过滤器。当在
configure(HttpSecurity)
方法中调用
http.authorizeRequests()
开启URL路径拦截规则配置时,就会通过
AbstractInterceptUrlConfigurer#configure
方法将
FilterSecurityInterceptor
添加到spring security过滤器链中。
对过滤器而且,最重要的就是doFilter方法
:
publicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{// 构建受保护对象FilterInvocation,然后调用invoke方法invoke(newFilterInvocation(request, response, chain));}publicvoidinvoke(FilterInvocation filterInvocation)throwsIOException,ServletException{// 判断当前过滤器是否已经执行过,如果是,则继续执行剩下的过滤器if(isApplied(filterInvocation)&&this.observeOncePerRequest){// filter already applied to this request and user wants us to observe// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());return;}// first time this request being called, so perform security checkingif(filterInvocation.getRequest()!=null&&this.observeOncePerRequest){
filterInvocation.getRequest().setAttribute(FILTER_APPLIED,Boolean.TRUE);}// 调用父类的beforeInvocation方法进行权限校验InterceptorStatusToken token =super.beforeInvocation(filterInvocation);try{// 校验通过后,继续执行剩余的过滤器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());}finally{// 调用父类的finallyInvocation方法super.finallyInvocation(token);}// 最后调用父类的afterInvocation方法,可以看到,前置处理器和后置处理器都是在invoke方法中触发的super.afterInvocation(token,null);}
*
AbstractInterceptUrlConfigurer
该类主要负责创建
FilterSecurityInterceptor
对象,
AbstractInterceptUrlConfigurer
有两个不同的子类,两个子类创建出来的
FilterSecurityInterceptor
对象略有差异:
ExpressionUrlAuthorizationConfigurer
UrlAuthorizationConfigurer
通过
ExpressionUrlAuthorizationConfigurer
构建出来的
FilterSecurityInterceptor
,使用的投票器是
WebExpressionVoter
,使用的权限元数据对象是
ExpressionBasedFilterInvocationSecurityMetadataSource
,所以它支持权限表达式。
通过UrlAuthorizationConfigurer
构建出来的
FilterSecurityInterceptor
,使用的投票器是
RoleVoter
和
AuthenticatedVoter
,使用的权限元数据对象是
DefaultFilterInvocationSecurityMetadataSource
,所以它不支持权限表达式。
这是两者最主要的区别。
当在
configure(HttpSecurity)
方法中开启权限配置时,一般是通过如下方式:
@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")// ...}
http.authorizeRequests()
方法实际上就是通过
ExpressionUrlAuthorizationConfigurer
来配置基于URL的权限管理,所以在配置时可以使用权限表达式。使用
ExpressionUrlAuthorizationConfigurer
进行配置,有一个硬性要求,就是至少配置一对URL地址和权限之间的映射关系。如果写成下面这种,就会出错:
@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().and().formLogin().and().csrf().disable();}
如果使用
UrlAuthorizationConfigurer
去配置
FilterSecurityInterceptor
,则不存在此要求,即代码中可以不配置任何的映射关系,只需要URL路径和权限之间的映射关系完整即可,这在动态权限配置中非常有用。
不过在spring security中,使用UrlAuthorizationConfigurer
去配置
FilterSecurityInterceptor
并不像使用
ExpressionUrlAuthorizationConfigurer
去配置那么容易,没有现成的方法,需要手动创建:
@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);// 开发者手动创建一个UrlAuthorizationConfigurer对象出来,并调用其getRegistry方法去开启URL路径和权限之间映射关系的配置。// 由于UrlAuthorizationConfigurer中使用的投票器是RoleVoter和AuthenticatedVoter,所以这里的角色需要自带ROLE_前缀// (因为RoleVoter的supports方法中会判断角色是否带有ROLE_前缀)
http.apply(newUrlAuthorizationConfigurer<>(applicationContext)).getRegistry().mvcMatchers("/admin/**").access("ROLE_ADMIN").mvcMatchers("/user/**").access("ROLE_USER");
http.formLogin().and().csrf().disable();}
使用
UrlAuthorizationConfigurer
去配置
FilterSecurityInterceptor
时,需要确保映射关系完整,即必须成对出现。
另外需要注意的是,无论是
ExpressionUrlAuthorizationConfigurer
还是
UrlAuthorizationConfigurer
,对于
FilterSecurityInterceptor
的配置来说都是在其父类
AbstractInterceptUrlConfigurer#configure
方法中,该方法中并未配置后置处理器
afterInvocationManager
,所以在基于URL地址的权限管理中,主要是前置处理器在工作。
13.4.5动态管理权限规则
在前面的案例中,配置的URL拦截规则和请求URL所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个URL所需要的权限,就需要修改代码。
动态管理权限规则就是将URL拦截规则和访问URL所需要的权限都保存在数据库中,这样,在不改变源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。
13.4.5.1数据库设计
数据库脚本:
CREATETABLE`menu`(`id`int(11)NOTNULLAUTO_INCREMENT,`pattern`varchar(128)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`menu`(`id`,`pattern`)VALUES(1,'/admin/**'),(2,'/user/**'),(3,'/guest/**');CREATETABLE`menu_role`(`id`int(11)NOTNULLAUTO_INCREMENT,`mid`int(11)DEFAULTNULL,`rid`int(11)DEFAULTNULL,PRIMARYKEY(`id`),KEY`mid`(`mid`),KEY`rid`(`rid`),CONSTRAINT`menu_role_ibfk_1`FOREIGNKEY(`mid`)REFERENCES`menu`(`id`),CONSTRAINT`menu_role_ibfk_2`FOREIGNKEY(`rid`)REFERENCES`role`(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`menu_role`(`id`,`mid`,`rid`)VALUES(1,1,1),(2,2,2),(3,3,3),(4,3,2);CREATETABLE`role`(`id`int(11)NOTNULLAUTO_INCREMENT,`name`varchar(32)DEFAULTNULL,`nameZh`varchar(32)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`role`(`id`,`name`,`nameZh`)VALUES(1,'ROLE_ADMIN','系统管理员'),(2,'ROLE_USER','普通用户'),(3,'ROLE_GUEST','游客');CREATETABLE`user`(`id`int(11)NOTNULLAUTO_INCREMENT,`username`varchar(32)DEFAULTNULL,`password`varchar(255)DEFAULTNULL,`enabled`tinyint(1)DEFAULTNULL,`locked`tinyint(1)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`user`(`id`,`username`,`password`,`enabled`,`locked`)VALUES(1,'admin','{noop}123',1,0),(2,'user','{noop}123',1,0),(3,'javaboy','{noop}123',1,0);CREATETABLE`user_role`(`id`int(11)NOTNULLAUTO_INCREMENT,`uid`int(11)DEFAULTNULL,`rid`int(11)DEFAULTNULL,PRIMARYKEY(`id`),KEY`uid`(`uid`),KEY`rid`(`rid`),CONSTRAINT`user_role_ibfk_1`FOREIGNKEY(`uid`)REFERENCES`user`(`id`),CONSTRAINT`user_role_ibfk_2`FOREIGNKEY(`rid`)REFERENCES`role`(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8;INSERTINTO`user_role`(`id`,`uid`,`rid`)VALUES(1,1,1),(2,1,2),(3,2,2),(4,3,3);
13.4.5.2实战
项目创建
创建项目,在
pom.xml
文件中引入web、spring security、mysql以及mybatis依赖。接下来在
application.properties
中配置数据库连接信息。
创建实体类
publicclassRole{privateInteger id;privateString name;privateString nameZh;// 省略getter/setter}publicclassMenu{privateInteger id;privateString pattern;privateList<Role> roles;// 省略getter/setter}publicclassUserimplementsUserDetails{privateInteger id;privateString password;privateString username;privateboolean enabled;privateboolean locked;privateList<Role> roles;@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){return roles.stream().map(r ->newSimpleGrantedAuthority(r.getName())).collect(Collectors.toList());}@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){return!locked;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){return enabled;}// 省略其他getter/setter}
创建service
@ServicepublicclassUserServiceimplementsUserDetailsService{@AutowiredUserMapper userMapper;@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{User user = userMapper.loadUserByUsername(username);if(user ==null){thrownewUsernameNotFoundException("用户不存在");}
user.setRoles(userMapper.getUserRoleByUid(user.getId()));return user;}}@MapperpublicinterfaceUserMapper{List<Role>getUserRoleByUid(Integer uid);UserloadUserByUsername(String username);}
<!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.fuxiao.part13_4_5.mapper.UserMapper"><selectid="loadUserByUsername"resultType="com.fuxiao.part13_4_5.bean.User">
select * from user where username=#{username};
</select><selectid="getUserRoleByUid"resultType="com.fuxiao.part13_4_5.bean.Role">
select r.* from role r,user_role ur where ur.uid=#{uid} and ur.rid=r.id
</select></mapper>
@ServicepublicclassMenuService{@AutowiredMenuMapper menuMapper;publicList<Menu>getAllMenu(){return menuMapper.getAllMenu();}}@MapperpublicinterfaceMenuMapper{List<Menu>getAllMenu();}
<!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.fuxiao.part13_4_5.mapper.MenuMapper"><resultMapid="MenuResultMap"type="com.fuxiao.part13_4_5.bean.Menu"><idproperty="id"column="id"/><resultproperty="pattern"column="pattern"></result><collectionproperty="roles"ofType="com.fuxiao.part13_4_5.bean.Role"><idcolumn="rid"property="id"/><resultcolumn="rname"property="name"/><resultcolumn="rnameZh"property="nameZh"/></collection></resultMap><selectid="getAllMenu"resultMap="MenuResultMap">
select m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh from menu m left join menu_role mr on m.`id`=mr.`mid` left join role r on r.`id`=mr.`rid`
</select></mapper>
配置spring security
/**
* SecurityMetadataSource接口负责提供受保护对象所需要的权限。由于该案例中,受保护对象所需要的权限保存在数据库中,所以可以通过自定义类继承自
* FilterInvocationSecurityMetadataSource,并重写getAttributes方法来提供受保护对象所需要的权限。
*/@ComponentpublicclassCustomSecurityMetadataSourceimplementsFilterInvocationSecurityMetadataSource{@AutowiredMenuService menuService;AntPathMatcher antPathMatcher =newAntPathMatcher();/**
* 在基于URL地址的权限控制中,受保护对象就是FilterInvocation。
* @param object 受保护对象
* @return 受保护对象所需要的权限
*/@OverridepublicCollection<ConfigAttribute>getAttributes(Object object)throwsIllegalArgumentException{// 从受保护对象FilterInvocation中提取出当前请求的URI地址,例如/admin/helloString requestURI =((FilterInvocation) object).getRequest().getRequestURI();// 查询所有菜单数据(每条数据中都包含访问该条记录所需要的权限)List<Menu> allMenu = menuService.getAllMenu();// 遍历菜单数据,如果当前请求的URL地址和菜单中某一条记录的pattern属性匹配上了(例如/admin/hello匹配上/admin/**)// 那么就可以获取当前请求所需要的权限;如果没有匹配上,则返回null。需要注意的是,如果AbstractSecurityInterceptor// 中的rejectPublicInvocations属性为false(默认值)时,则表示当getAttributes返回null时,允许访问受保护对象for(Menu menu : allMenu){if(antPathMatcher.match(menu.getPattern(), requestURI)){String[] roles = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);returnSecurityConfig.createList(roles);}}returnnull;}/**
* 方便在项目启动阶段做校验,如果不需要校验,则直接返回null即可。
* @return 所有的权限属性
*/@OverridepublicCollection<ConfigAttribute>getAllConfigAttributes(){returnnull;}/**
* 表示当前对象支持处理的受保护对象是FilterInvocation。
*/@Overridepublicbooleansupports(Class<?> clazz){returnFilterInvocation.class.isAssignableFrom(clazz);}}@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredCustomSecurityMetadataSource customSecurityMetadataSource;@AutowiredUserService userService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.userDetailsService(userService);}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
http.apply(newUrlAuthorizationConfigurer<>(applicationContext)).withObjectPostProcessor(newObjectPostProcessor<FilterSecurityInterceptor>(){@Overridepublic<OextendsFilterSecurityInterceptor>OpostProcess(O object){// 使用配置好的CustomSecurityMetadataSource来代替默认的SecurityMetadataSource对象
object.setSecurityMetadataSource(customSecurityMetadataSource);// 将rejectPublicInvocations设置为true,表示当getAttributes返回null时,不允许访问受保护对象
object.setRejectPublicInvocations(true);return object;}});
http.formLogin().and().csrf().disable();}}
测试
@RestControllerpublicclassHelloController{@GetMapping("/admin/hello")publicStringadmin(){return"Hello admin";}@GetMapping("/user/hello")publicStringuser(){return"Hello user";}@GetMapping("/guest/hello")publicStringguest(){return"Hello guest";}/**
* 由于rejectPublicInvocations设置为true,因此,只有具备该接口权限的用户才能访问
*/@GetMapping("/hello")publicStringhello(){return"Hello";}}
13.5基于方法的权限管理
基于方法的权限管理主要是通过AOP来实现的,spring security中通过
MethodSecurityInterceptor
来提供相关的实现。不同在于,
FilterSecurityInterceptor
只是在请求之前进行前置处理,
MethodSecurityInterceptor
在此基础上还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。
13.5.1注解介绍
目前在spring boot中基于方法的权限管理主要是通过注解来实现,需要通过
@EnableGlobalMethodSecurity
注解开启权限注解的使用:
@Configuration@EnableGlobalMethodSecurity(prePostEnabled =true, securedEnabled =true, jsr250Enabled =true)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{}
这个注解中,设置了三个属性:
prePostEnabled
:开启spring security提供的四个权限注解,@PostAuthorize
、@PostFilter
、@PreAuthorize
以及@PreFilter
,这四个注解支持权限表达式,功能比较丰富。securedEnabled
:开启spring security提供的@Secured
注解,该注解不支持权限表达式。jsr250Enabled
:开始JSR-250提供的注解,主要包括@DenyAll
、@PermitAll
以及@RolesAllowed
三个注解,这些注解也不支持权限表达式。这些注解的含义分别如下:
@PostAuthorize
:在目标方法执行之后进行权限校验。@PostFilter
:在目标方法执行之后对方法的返回结果进行过滤。@PreAuthorize
:在目标方法执行之前进行权限校验。@PreFilter
:在目标方法执行之前对方法参数结果进行过滤。@Secured
:访问目标方法必须具备相应的角色。@DenyAll
:拒绝所有访问。@PermitAll
:允许所有访问。@RolesAllowed
:访问目标方法必须具备相应的角色。一般来说,只要设置
prePostEnabled = true
就够用了。
13.5.2基本用法
创建spring boot项目,引入spring security和web依赖,项目创建完成后,添加配置文件:
@EnableGlobalMethodSecurity(prePostEnabled =true, securedEnabled =true, jsr250Enabled =true)publicclassSecurityConfig{}
创建
User
类:
publicclassUser{privateInteger id;privateString username;// 省略构造器、toString和getter/setter}
*
@PreAuthorize
@ServicepublicclassHelloService{// 执行该方法必须具备ADMIN角色才可以访问@PreAuthorize("hasRole('ADMIN')")publicStringpreAuthorizeTest01(){return"Hello";}// 访问者名称必须是javaboy,而且还需要同事具备ADMIN角色才可以访问@PreAuthorize("hasRole('ADMIN') and authentication.name == 'javaboy'")publicStringpreAuthorizeTest02(){return"Hello";}// 通过#引用方法参数,并对其进行校验,表示请求者的用户名必须等于方法参数name的值,方法才可以被执行@PreAuthorize("authentication.name == #name")publicStringpreAuthorizeTest03(String name){return"Hello: "+ name;}}@SpringBootTestclassBasedOnMethodApplicationTests{@AutowiredHelloService helloService;@Test// 通过该注解设定当前执行的用户角色是ADMIN@WithMockUser(roles ="ADMIN")voidpreAuthorizeTest01(){String hello = helloService.preAuthorizeTest01();Assertions.assertNotNull(hello);Assertions.assertEquals("Hello", hello);}@Test@WithMockUser(roles ="ADMIN", username ="javaboy")voidpreAuthorizeTest02(){String hello = helloService.preAuthorizeTest02();Assertions.assertNotNull(hello);Assertions.assertEquals("Hello", hello);}@Test@WithMockUser(username ="javaboy")voidpreAuthorizeTest03(){String hello = helloService.preAuthorizeTest03("javaboy");Assertions.assertNotNull(hello);Assertions.assertEquals("Hello: javaboy", hello);}}
*
@PreFilter
@ServicepublicclassHelloService{/**
* PreFilter主要是对方法的请求参数进行过滤,它里边包含了一个内置对象filterObject,表示具体元素。
* 如果方法只有一个参数,则内置的filterObject对象就代表该参数;如果有多个参数,则需要通过filterTarget
* 来指定filterObject到底代表哪个对象。
* 表示只保留id为奇数的user对象。
*/@PreFilter(filterTarget ="users", value ="filterObject.id % 2 != 0")publicvoidpreFilterTest(List<User> users){System.out.println("users = "+ users);}}
@Test@WithMockUser(username ="javaboy")voidpreFilterTest(){List<User> users =newArrayList<>();for(int i =0; i <10; i++){
users.add(newUser(i,"javaboy: "+ i));}
helloService.preFilterTest(users);}
*
@PostAuthorize
@ServicepublicclassHelloService{/**
* PostAuthorize是在目标方法执行之后进行权限校验,其实这个主要是在ACL权限模型中会用到,目标方法执行完毕后,
* 通过该注解去校验目标方法的返回值是否满足相应的权限要求。从技术角度来讲,该注解中也可以使用权限表达式,但是
* 在实际开发中权限表达式一般都是结合PreAuthorize注解一起使用的。PostAuthorize包含一个内置对象returnObject,
* 表示方法的返回值,开发中可以对返回值进行校验。
*/@PostAuthorize("returnObject.id == 1")publicUserpostAuthorizeTest(Integer id){returnnewUser(id,"javaboy");}}
@Test@WithMockUser(username ="javaboy")voidpostAuthorizeTest(){User user = helloService.postAuthorizeTest(1);Assertions.assertNotNull(user);Assertions.assertEquals(1, user.getId());Assertions.assertEquals("javaboy", user.getUsername());}
*
@PostFilter
@ServicepublicclassHelloService{/**
* PostFilter注解在目标方法执行之后,对目标方法的返回结果进行过滤,该注解中包含了一个内置对象filterObject,
* 表示目标方法返回的集合/数组中的具体元素。
*/@PostFilter("filterObject.id % 2 == 0")publicList<User>postFilterTest(){List<User> users =newArrayList<>();for(int i =0; i <10; i++){
users.add(newUser(i,"javaboy: "+ i));}return users;}}
@Test@WithMockUser(roles ="ADMIN")voidpostFilterTest(){List<User> all = helloService.postFilterTest();Assertions.assertNotNull(all);Assertions.assertEquals(5, all.size());Assertions.assertEquals(2, all.get(1).getId());}
*
@Secured
@ServicepublicclassHelloService{// 该注解不支持权限表达式,只能做一些简单的权限描述@Secured({"ROLE_ADMIN","ROLE_USER"})publicUsersecuredTest(String username){returnnewUser(99, username);}}
@Test@WithMockUser(roles ="ADMIN")voidsecuredTest(){User user = helloService.securedTest("javaboy");Assertions.assertNotNull(user);Assertions.assertEquals(99, user.getId());Assertions.assertEquals("javaboy", user.getUsername());}
*
@DenyAll
@ServicepublicclassHelloService{// JSR-250:拒绝所有访问@DenyAllpublicvoiddenyAllTest(){}}
@Test@WithMockUser(username ="javaboy")voiddenyAllTest(){
helloService.denyAllTest();}
*
@PermitAll
@ServicepublicclassHelloService{// JSR-250:允许所有访问@PermitAllpublicvoidpermitAllTest(){}}
@Test@WithMockUser(username ="javaboy")voidpermitAllTest(){
helloService.permitAllTest();}
*
@RolesAllowed
@ServicepublicclassHelloService{/**
* JSR-250:可以添加在方法上或类上,当添加在类上时,表示该注解对类中的所有方法生效;如果类上和方法上都有该注解,
* 并且起冲突,则以方法上的注解为准。
*/@RolesAllowed({"ADMIN","USER"})publicStringrolesAllowedTest(){return"RolesAllowed";}}
@Test@WithMockUser(roles ="ADMIN")voidrolesAllowedTest(){String s = helloService.rolesAllowedTest();Assertions.assertNotNull(s);Assertions.assertEquals("RolesAllowed", s);}
13.5.3原理剖析
*
MethodSecurityInterceptor
当基于URL请求地址进行权限控制时,使用的
AbstractSecurityInterceptor
实现类是
FilterSecurityInterceptor
,而当基于方法进行权限控制时,使用的实现类则是
MethodSecurityInterceptor
。
MethodSecurityInterceptor
提供了基于AOP Alliance的方法拦截,该拦截器中所使用的
SecurityMetadataSource
类型为
MethodSecurityMetadataSource
。
MethodSecurityInterceptor
中最重要的就是
invoke
方法:
@OverridepublicObjectinvoke(MethodInvocation mi)throwsThrowable{// 调用父类的beforeInvocation方法进行权限校验InterceptorStatusToken token =super.beforeInvocation(mi);Object result;try{// 校验通过后,调用mi.proceed()方法继续执行目标方法
result = mi.proceed();}finally{// 在finally块中调用finallyInvocation方法完成一些清理工作super.finallyInvocation(token);}// 最后调用父类的afterInvocation方法进行请求结果的过滤returnsuper.afterInvocation(token, result);}
*
EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity
用来开启方法的权限注解:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented// 引入了一个配置GlobalMethodSecuritySelector,该类的作用主要是用来导入外部配置类@Import({GlobalMethodSecuritySelector.class})@EnableGlobalAuthentication@Configurationpublic@interfaceEnableGlobalMethodSecurity{// 省略其他}
finalclassGlobalMethodSecuritySelectorimplementsImportSelector{// importingClassMetadata中保存了@EnableGlobalMethodSecurity注解的元数据,包括各个属性的值、注解是加在哪个配置类上等@OverridepublicString[]selectImports(AnnotationMetadata importingClassMetadata){Class<EnableGlobalMethodSecurity> annoType =EnableGlobalMethodSecurity.class;Map<String,Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(annoType.getName(),false);AnnotationAttributes attributes =AnnotationAttributes.fromMap(annotationAttributes);// TODO would be nice if could use BeanClassLoaderAware (does not work)Class<?> importingClass =ClassUtils.resolveClassName(importingClassMetadata.getClassName(),ClassUtils.getDefaultClassLoader());boolean skipMethodSecurityConfiguration =GlobalMethodSecurityConfiguration.class.isAssignableFrom(importingClass);AdviceMode mode = attributes.getEnum("mode");boolean isProxy =AdviceMode.PROXY == mode;String autoProxyClassName = isProxy ?AutoProxyRegistrar.class.getName():GlobalMethodSecurityAspectJAutoProxyRegistrar.class.getName();boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");List<String> classNames =newArrayList<>(4);if(isProxy){
classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());}
classNames.add(autoProxyClassName);if(!skipMethodSecurityConfiguration){
classNames.add(GlobalMethodSecurityConfiguration.class.getName());}if(jsr250Enabled){
classNames.add(Jsr250MetadataSourceConfiguration.class.getName());}return classNames.toArray(newString[0]);}}
selectImports
方法的逻辑比较简单,要导入的外部配置类有以下几种:
MethodSecurityMetadataSourceAdvisorRegistrar
:如果使用的是spring自带的AOP,则该配置类会被导入。该类主要用来向spring容器中注册一个MethodSecurityMetadataSourceAdvisor
对象,这个对象中定义了AOP中的pointcut和advice。autoProxyClassName
:注册自动代理创建者,根据不同的代理模式而定。GlobalMethodSecurityConfiguration
:这个配置类用来提供MethodSecurityMetadataSource
和MethodInterceptor
两个关键对象。如果开发者自定义配置类继承自GlobalMethodSecurityConfiguration
,则这里不会导入这个外部配置类。Jsr250MetadataSourceConfiguration
:如果开启了JSR-250注解,这会导入该配置类。该配置类主要用来提供JSR-250注解所需的Jsr250MethodSecurityMetadataSource
对象。
先来看
MethodSecurityMetadataSourceAdvisorRegistrar
:
classMethodSecurityMetadataSourceAdvisorRegistrarimplementsImportBeanDefinitionRegistrar{@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry){// 首先定义BeanDefinitionBuilderBeanDefinitionBuilder advisor =BeanDefinitionBuilder.rootBeanDefinition(MethodSecurityMetadataSourceAdvisor.class);// 然后给目标对象MethodSecurityMetadataSourceAdvisor的构造方法设置参数
advisor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);// 要引用的MethodInterceptor对象名
advisor.addConstructorArgValue("methodSecurityInterceptor");// 要引用的MethodSecurityMetadataSource对象名
advisor.addConstructorArgReference("methodSecurityMetadataSource");// 和第二个一样,只不过一个是引用,一个是字符串
advisor.addConstructorArgValue("methodSecurityMetadataSource");MultiValueMap<String,Object> attributes = importingClassMetadata
.getAllAnnotationAttributes(EnableGlobalMethodSecurity.class.getName());Integer order =(Integer) attributes.getFirst("order");if(order !=null){
advisor.addPropertyValue("order", order);}// 所有属性都配置好之后,将其注册到spring容器中
registry.registerBeanDefinition("metaDataSourceAdvisor", advisor.getBeanDefinition());}}
再来看
MethodSecurityMetadataSourceAdvisor
:
/**
* 继承自AbstractPointcutAdvisor,主要定义了AOP的pointcut和advice,poincut也就是切点,可以简单理解为方法的拦截规则,
* 即哪些方法需要拦截,哪些方法不需要拦截;advice也就是增强/通知,就是将方法拦截下来之后要增强的功能
*/publicclassMethodSecurityMetadataSourceAdvisorextendsAbstractPointcutAdvisorimplementsBeanFactoryAware{privatetransientMethodSecurityMetadataSource attributeSource;privatetransientMethodInterceptor interceptor;// 这里的pointcut对象就是内部类MethodSecurityMetadataSourcePointcut,在它的matches方法中,定义了具体的拦截规则privatefinalPointcut pointcut =newMethodSecurityMetadataSourcePointcut();privateBeanFactory beanFactory;privatefinalString adviceBeanName;privatefinalString metadataSourceBeanName;privatetransientvolatileObject adviceMonitor =newObject();// 构造方法所需的三个参数就是MethodSecurityMetadataSourceAdvisorRegistrar类中提供的三个参数publicMethodSecurityMetadataSourceAdvisor(String adviceBeanName,MethodSecurityMetadataSource attributeSource,String attributeSourceBeanName){this.adviceBeanName = adviceBeanName;this.attributeSource = attributeSource;this.metadataSourceBeanName = attributeSourceBeanName;}@OverridepublicPointcutgetPointcut(){returnthis.pointcut;}/**
* Advice由getAdvice方法返回,在该方法内部,就是去spring容器中查找一个名为
* methodSecurityInterceptor的MethodInterceptor对象。
*/@OverridepublicAdvicegetAdvice(){synchronized(this.adviceMonitor){if(this.interceptor ==null){this.interceptor =this.beanFactory.getBean(this.adviceBeanName,MethodInterceptor.class);}returnthis.interceptor;}}@OverridepublicvoidsetBeanFactory(BeanFactory beanFactory)throwsBeansException{this.beanFactory = beanFactory;}privatevoidreadObject(ObjectInputStream ois)throwsIOException,ClassNotFoundException{
ois.defaultReadObject();this.adviceMonitor =newObject();this.attributeSource =this.beanFactory.getBean(this.metadataSourceBeanName,MethodSecurityMetadataSource.class);}classMethodSecurityMetadataSourcePointcutextendsStaticMethodMatcherPointcutimplementsSerializable{@Overridepublicbooleanmatches(Method m,Class<?> targetClass){MethodSecurityMetadataSource source =MethodSecurityMetadataSourceAdvisor.this.attributeSource;// 通过attributeSource.getAttributes方法去查看目标方法上有没有相应的权限注解,// 如果有,则返回true,目标方法就被拦截下来;如果没有,则返回false,目标方法就// 不会被拦截,这里的attributeSource实际上就是MethodSecurityMetadataSource对象,// 也就是提供权限元数据的类return!CollectionUtils.isEmpty(source.getAttributes(m, targetClass));}}}
此时,应该已经明白AOP的切点和增强/通知是如何定义的了,这里涉及两个关键的对象:一个名为
methodSecurityInterceptor
的
MethodInterceptor
对象和一个名为
methodSecurityMetadataSource
的
MethodSecurityMetadataSource
对象。
这两个关键的对象在GlobalMethodSecurityConfiguration
类中定义,相关的方法比较长,先来看
MethodSecurityMetadataSource
对象的定义:
@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)publicMethodSecurityMetadataSourcemethodSecurityMetadataSource(){// 创建List集合,用来保存所有的MethodSecurityMetadataSource对象List<MethodSecurityMetadataSource> sources =newArrayList<>();ExpressionBasedAnnotationAttributeFactory attributeFactory =newExpressionBasedAnnotationAttributeFactory(getExpressionHandler());// 然后调用customMethodSecurityMetadataSource方法去获取自定义的MethodSecurityMetadataSource,// 默认情况下该方法返回null,如果想买有需要,开发者可以重写该方法来提供自定义的MethodSecurityMetadataSource对象MethodSecurityMetadataSource customMethodSecurityMetadataSource =customMethodSecurityMetadataSource();if(customMethodSecurityMetadataSource !=null){
sources.add(customMethodSecurityMetadataSource);}// 接下来就是根据注解中配置的属性值,来向sources集合中添加相应的MethodSecurityMetadataSource对象boolean hasCustom = customMethodSecurityMetadataSource !=null;boolean isPrePostEnabled =prePostEnabled();boolean isSecuredEnabled =securedEnabled();boolean isJsr250Enabled =jsr250Enabled();// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,// 则加入PrePostAnnotationSecurityMetadataSource对象来解析相应的注解if(isPrePostEnabled){
sources.add(newPrePostAnnotationSecurityMetadataSource(attributeFactory));}// 如果@EnableGlobalMethodSecurity注解配置了securedEnabled=true,// 则加入SecuredAnnotationSecurityMetadataSource对象来解析相应的注解if(isSecuredEnabled){
sources.add(newSecuredAnnotationSecurityMetadataSource());}// 如果@EnableGlobalMethodSecurity注解配置了jsr250Enabled=true,// 则加入Jsr250MethodSecurityMetadataSource对象来解析相应的注解if(isJsr250Enabled){GrantedAuthorityDefaults grantedAuthorityDefaults =getSingleBeanOrNull(GrantedAuthorityDefaults.class);Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource =this.context
.getBean(Jsr250MethodSecurityMetadataSource.class);if(grantedAuthorityDefaults !=null){
jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());}
sources.add(jsr250MethodSecurityMetadataSource);}// 最后构建一个代理对象DelegatingMethodSecurityMetadataSource返回即可returnnewDelegatingMethodSecurityMetadataSource(sources);}
可以看到,默认提供的
MethodSecurityMetadataSource
对象实际上是一个代理对象,它包含多个不同的
MethodSecurityMetadataSource
实例。在判断一个方法是否需要被拦截下来时,由这些被代理的对象逐个去解析目标方法是否含有相应的注解(例如
PrePostAnnotationSecurityMetadataSource
可以检查出目标方法是否含有
@PostAuthorize
、
@PostFilter
、
@PreAuthorize
以及
@PreFilter
),如果有,则请求就会被拦截下来。
再来看
MethodInterceptor
的定义:
@BeanpublicMethodInterceptormethodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource){// 查看代理的方式,默认使用spring自带的AOP,所以使用MethodSecurityInterceptor来创建对应的MethodInterceptor实例this.methodSecurityInterceptor =isAspectJ()?newAspectJMethodSecurityInterceptor():newMethodSecurityInterceptor();// 然后给methodSecurityInterceptor设置AccessDecisionManager决策管理器this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());// 接下来给methodSecurityInterceptor配置后置处理器this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());// 最后再把前面创建好的MethodSecurityMetadataSource对象配置给methodSecurityInterceptorthis.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);RunAsManager runAsManager =runAsManager();if(runAsManager !=null){this.methodSecurityInterceptor.setRunAsManager(runAsManager);}returnthis.methodSecurityInterceptor;}protectedAccessDecisionManageraccessDecisionManager(){// 默认的决策管理器是AffirmativeBasedList<AccessDecisionVoter<?>> decisionVoters =newArrayList<>();// 根据@EnableGlobalMethodSecurity注解的配置,配置不同的投票器if(prePostEnabled()){ExpressionBasedPreInvocationAdvice expressionAdvice =newExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters.add(newPreInvocationAuthorizationAdviceVoter(expressionAdvice));}if(jsr250Enabled()){
decisionVoters.add(newJsr250Voter());}RoleVoter roleVoter =newRoleVoter();GrantedAuthorityDefaults grantedAuthorityDefaults =getSingleBeanOrNull(GrantedAuthorityDefaults.class);if(grantedAuthorityDefaults !=null){
roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());}
decisionVoters.add(roleVoter);
decisionVoters.add(newAuthenticatedVoter());returnnewAffirmativeBased(decisionVoters);}protectedAfterInvocationManagerafterInvocationManager(){// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,则添加一个后置处理器// PostInvocationAdviceProvider,该类用来处理@PostAuthorize和@PostFilter两个注解if(prePostEnabled()){AfterInvocationProviderManager invocationProviderManager =newAfterInvocationProviderManager();ExpressionBasedPostInvocationAdvice postAdvice =newExpressionBasedPostInvocationAdvice(getExpressionHandler());PostInvocationAdviceProvider postInvocationAdviceProvider =newPostInvocationAdviceProvider(postAdvice);List<AfterInvocationProvider> afterInvocationProviders =newArrayList<>();
afterInvocationProviders.add(postInvocationAdviceProvider);
invocationProviderManager.setProviders(afterInvocationProviders);return invocationProviderManager;}returnnull;}
版权归原作者 EdSheeran乀 所有, 如有侵权,请联系我们删除。