权限管理
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
。据此,定义角色类如下:
public class Role implements GrantedAuthority {
private String name;
private List<SimpleGrantedAuthority> allowedOperations = new ArrayList<>();
@Override
public String getAuthority() {
return name;
}
// 省略getter/setter
}
角色继承自
GrantedAuthority
,一个角色对应多个权限。然后在定义用户类的时候,将角色转换为权限即可:
public class User implements UserDetails {
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities= new ArrayList<>();
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
类对角色继承提供支持:
public interface RoleHierarchy {
/**
* 该方法返回用户真正可触达的权限。例如,假设用户定义了ROLE_ADMIN继承自ROLE_USER,ROLE_USER继承自ROLE_GUEST,
* 现在当前用户角色是ROLE_ADMIN,但是它实际可访问的资源也包含ROLE_USER和ROLE_GUEST能访问的资源。该方法就是根据
* 当前用户所具有的角色,从角色层级映射中解析出用户真正可触达的权限。
*/
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
}
RoleHierarchy
只有一个实现类RoleHierarchyImpl
,开发者一般通过RoleHierarchyImpl
类来定义角色的层级关系:
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
// 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
来定义:
public interface AccessDecisionVoter<S> {
// 表示投票通过
int ACCESS_GRANTED = 1;
// 表示弃权
int ACCESS_ABSTAIN = 0;
// 表示拒绝
int ACCESS_DENIED = -1;
// 用来判断是否支持处理ConfigAttribute对象
boolean supports(ConfigAttribute attribute);
// 用来判断是否支持处理受保护的安全对象
boolean supports(Class<?> clazz);
/**
* 具体的投票方法,根据用户所具有的权限以及当前请求需要的权限进行投票。
* @param authentication 进行调用的调用者,可以提取出来当前用户所具备的权限
* @param object 受保护的安全对象,如果受保护的是URL地址,则object就是一个FilterInvocation对象;如果受保护的是一个
* 方法,则object就是一个MethodInvocation对象
* @param attributes 访问受保护对象所需要的权限
* @return 定义的三个常量之一
*/
int vote(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
也称作是一个决策管理器:
public interface AccessDecisionManager {
// 核心的决策方法,在这个方法中判断是否允许当前URL或者方法的调用,如果不允许,则抛出AccessDeniedException异常
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException;
// 用来判断是否支持处理ConfigAttribute对象
boolean supports(ConfigAttribute attribute);
// 用来判断是否支持当前安全对象
boolean supports(Class<?> clazz);
}
可以看出,
AccessDecisionManager
有一个实现类AbstractAccessDecisionManager
,一个AbstractAccessDecisionManager
对应多个投票器。多个投票器针对同一个请求可能会给出不同的结果,那么听谁的呢,这就要看决策器了。
AffirmativeBased
:一票通过机制,即只要有一个投票器通过就可以访问(默认即此)。UnanimousBased
:一票否决机制,即只要有一个投票器反对就不可以访问。ConsensusBased
:少数服从多数机制。如果是平局并且至少有一张赞同票,则根据allowIfEqualGrantedDeniedDecisions
参数的取值来决定,如果该参数的取值为true
,则可以访问,否则不可以访问。如果这三个决策器无法满足需求,开发者也可以自定义类继承自
AbstractAccessDecisionManager
实现自己的决策器。
这就是前置处理器中的大致逻辑,无论是基于URL地址的权限管理,还是基于方法的权限管理,都是在前置处理器中通过
AccessDecisionManager
调用AccessDecisionVoter
进行投票,进而做出相应的决策。
13.3.5后置处理器
后置处理器一般只在基于方法的权限控制中会用到,当目标方法执行完毕后,通过后置处理器可以对目标方法的返回值进行权限校验或者过滤。
后期处理器由AfterInvocationManager
负责:
// 和AccessDecisionManager高度相似
public interface AfterInvocationManager {
/**
* 主要的区别在于decide方法的参数和返回值。当后置处理器执行时,被权限保护的方法以及执行完毕,后置处理器主要是对执行的结果
* 进行过滤,所以decide方法中有一个returnedObject参数,这就是目标方法的执行结果,decide方法的返回值就是对returnedObject
* 对象进行过滤/鉴权后的结果
*/
Object decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes,
Object returnedObject) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(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
来存储:
public interface ConfigAttribute extends Serializable {
String getAttribute();
}
该接口只有一个
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
来提供。
public interface SecurityMetadataSource extends AopInfrastructureBean {
/**
* 根据传入的安全对象参数返回其所需要的权限。如果受保护的对象是一个URL地址,那么传入的参数object就是一个FilterInvocation
* 对象;如果受保护的是一个方法,那么object就是一个MethodInvocation对象。
*/
Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;
// 返回所有的角色/权限,以便验证是否支持。不过这个方法不是必须的,也可以直接返回null
Collection<ConfigAttribute> getAllConfigAttributes();
// 返回当前的SecurityMetadataSource是否支持受保护的对象如FilterInvocation或者MethodInvocation
boolean supports(Class<?> clazz);
}
可以看到,直接继承自
SecurityMetadataSource
的接口主要有两个:FilterInvocationSecurityMetadataSource
和MethodSecurityMetadataSource
。
FilterInvocationSecurityMetadataSource
:这是一个空接口,更像是一个标记。如果被保护的对象是一个URL地址,那么将由该接口的实现类提供访问该URL地址所需要的权限。MethodSecurityMetadataSource
:也是一个接口,如果受保护的对象是一个方法,那么将通过该接口的实现类来获取受保护对象所需要的权限。
FilterInvocationSecurityMetadataSource
有一个子类DefaultFilterInvocationSecurityMetadataSource
,该类中定义了一个如下格式的Map
集合:
private final Map<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
接口定义了基本的权限表达式:
public interface SecurityExpressionOperations {
Authentication getAuthentication();
boolean hasAuthority(String authority);
boolean hasAnyAuthority(String... authorities);
boolean hasRole(String role);
boolean hasAnyRole(String... roles);
boolean permitAll();
boolean denyAll();
boolean isAnonymous();
boolean isAuthenticated();
boolean isRememberMe();
boolean isFullyAuthenticated();
boolean hasPermission(Object target, Object permission);
boolean hasPermission(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基本用法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 定义用户以及相应的角色和权限。
* 对于复杂的权限管理系统,用户和角色关联,角色和权限关联,权限和资源关联;对于简单的权限管理系统,
* 用户和权限关联,权限和资源关联。无论是哪种,用户都不会和角色以及权限同时直接关联。反应到代码上
* 就是roles方法和authorities方法不能同时调用,如果同时调用,后者会覆盖前者(可以自行查看源码,
* 最终都会调用authorities(Collection<? extends GrantedAuthority> authorities)方法)。
* 需要注意的是,spring security会自动给用户的角色加上ROLE_前缀。
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
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");
}
/**
* 配置拦截规则
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
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
方法的参数就是权限表达式。- 代码的顺序很关键,当请求到达后,按照从上往下的顺序依次进行匹配。
配置完成后,再提供相应的测试接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "Hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "Hello user";
}
@GetMapping("/getInfo")
public String getInfo() {
return "GetInfo";
}
}
13.4.2角色继承
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 如果需要配置角色继承,则只需要提供一个RoleHierarchy的实例即可
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 省略
}
@Override
protected void configure(HttpSecurity http) throws Exception {
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自定义表达式
如果内置的表达式无法满足需求,开发者也可以自定义表达式。
假设现在有两个接口:
@RestController
public class HelloController {
/**
* 第一个接口:参数userId必须是偶数方可请求成功。
*/
@GetMapping("/hello/{userId}")
public String hello(@PathVariable Integer userId) {
return "Hello " + userId;
}
/**
* 第二个接口:参数username必须是javaboy方可请求成功,同时这两个接口必须认证后才能访问。
*/
@GetMapping("/hi")
public String hello2User(String username) {
return "Hello " + username;
}
}
/**
* 自定义PermissionExpression类并注册到spring容器中,然后定义相应的方法。
*/
@Component
public class PermissionExpression {
public boolean checkId(Authentication authentication, Integer userId) {
if (authentication.isAuthenticated()) {
return userId % 2 == 0;
}
return false;
}
public boolean check(HttpServletRequest request) {
return "javaboy".equals(request.getParameter("username"));
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 省略
}
@Override
protected void configure(HttpSecurity http) throws Exception {
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
:
protected InterceptorStatusToken beforeInvocation(Object object) {
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("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(new PublicInvocationEvent(object));
// 此时直接返回null即可
return null; // 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(new AuthorizedEvent(object, attributes, authenticated));
}
// 临时替换用户身份,不过默认情况下,runAsManager的实例是NullRunAsManager,即不做任何替换,所以runAs为null
Authentication 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-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
// 如果runAs为空,则直接创建一个InterceptorStatusToken对象返回即可
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
// 核心功能:进行决策,该方法中会调用投票器进行投票,如果该方法执行抛出异常,则说明权限不足
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException ex) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
throw ex;
}
}
再来看下
finallyInvocation
方法:
/**
* 如果临时替换了用户身份,那么最终要将用户身份恢复,finallyInvocation方法所做的事情就是恢复用户身份。这里的参数token就是
* beforeInvocation方法的返回值,用户原始的身份信息都保存在token中,从token中取出用户身份信息,并设置到SecurityContextHolder
* 中去即可。
*/
protected void finallyInvocation(InterceptorStatusToken token) {
if (token != null && token.isContextHolderRefreshRequired()) {
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
最后再来看看
afterInvocation
方法:
/**
* 该方法接收两个参数,第一个参数token就是beforeInvocation方法的返回值,第二个参数returnedObject则是受保护对象的返回值,
* afterInvocation方法的核心工作就是调用afterInvocationManager.decide方法对returnedObject进行过滤,然后将过滤后的
* 结果返回。
*/
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
if (token == null) {
// public object
return returnedObject;
}
finallyInvocation(token); // continue to clean in this method for passivity
if (this.afterInvocationManager != null) {
// Attempt after invocation handling
try {
returnedObject = this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(),
token.getSecureObject(), token.getAttributes(), returnedObject);
}
catch (AccessDeniedException ex) {
publishEvent(new AuthorizationFailureEvent(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方法
:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 构建受保护对象FilterInvocation,然后调用invoke方法
invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, 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 checking
if (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)
方法中开启权限配置时,一般是通过如下方式:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")
// ...
}
http.authorizeRequests()
方法实际上就是通过ExpressionUrlAuthorizationConfigurer
来配置基于URL的权限管理,所以在配置时可以使用权限表达式。使用ExpressionUrlAuthorizationConfigurer
进行配置,有一个硬性要求,就是至少配置一对URL地址和权限之间的映射关系。如果写成下面这种,就会出错:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.and()
.formLogin()
.and()
.csrf().disable();
}
如果使用
UrlAuthorizationConfigurer
去配置FilterSecurityInterceptor
,则不存在此要求,即代码中可以不配置任何的映射关系,只需要URL路径和权限之间的映射关系完整即可,这在动态权限配置中非常有用。
不过在spring security中,使用UrlAuthorizationConfigurer
去配置FilterSecurityInterceptor
并不像使用ExpressionUrlAuthorizationConfigurer
去配置那么容易,没有现成的方法,需要手动创建:
@Override
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
// 开发者手动创建一个UrlAuthorizationConfigurer对象出来,并调用其getRegistry方法去开启URL路径和权限之间映射关系的配置。
// 由于UrlAuthorizationConfigurer中使用的投票器是RoleVoter和AuthenticatedVoter,所以这里的角色需要自带ROLE_前缀
// (因为RoleVoter的supports方法中会判断角色是否带有ROLE_前缀)
http.apply(new UrlAuthorizationConfigurer<>(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数据库设计
数据库脚本:
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `menu` (`id`, `pattern`)
VALUES
(1,'/admin/**'),
(2,'/user/**'),
(3,'/guest/**');
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mid` (`mid`),
KEY `rid` (`rid`),
CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`),
CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `menu_role` (`id`, `mid`, `rid`)
VALUES
(1,1,1),
(2,2,2),
(3,3,3),
(4,3,2);
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
(1,'ROLE_ADMIN','系统管理员'),
(2,'ROLE_USER','普通用户'),
(3,'ROLE_GUEST','游客');
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `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);
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`),
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `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
中配置数据库连接信息。
创建实体类
public class Role {
private Integer id;
private String name;
private String nameZh;
// 省略getter/setter
}
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
// 省略getter/setter
}
public class User implements UserDetails {
private Integer id;
private String password;
private String username;
private boolean enabled;
private boolean locked;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
// 省略其他getter/setter
}
创建service
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
user.setRoles(userMapper.getUserRoleByUid(user.getId()));
return user;
}
}
@Mapper
public interface UserMapper {
List<Role> getUserRoleByUid(Integer uid);
User loadUserByUsername(String username);
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fuxiao.part13_4_5.mapper.UserMapper">
<select id="loadUserByUsername" resultType="com.fuxiao.part13_4_5.bean.User">
select * from user where username=#{username};
</select>
<select id="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>
@Service
public class MenuService {
@Autowired
MenuMapper menuMapper;
public List<Menu> getAllMenu() {
return menuMapper.getAllMenu();
}
}
@Mapper
public interface MenuMapper {
List<Menu> getAllMenu();
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fuxiao.part13_4_5.mapper.MenuMapper">
<resultMap id="MenuResultMap" type="com.fuxiao.part13_4_5.bean.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"></result>
<collection property="roles" ofType="com.fuxiao.part13_4_5.bean.Role">
<id column="rid" property="id"/>
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="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方法来提供受保护对象所需要的权限。
*/
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 在基于URL地址的权限控制中,受保护对象就是FilterInvocation。
* @param object 受保护对象
* @return 受保护对象所需要的权限
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 从受保护对象FilterInvocation中提取出当前请求的URI地址,例如/admin/hello
String 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);
return SecurityConfig.createList(roles);
}
}
return null;
}
/**
* 方便在项目启动阶段做校验,如果不需要校验,则直接返回null即可。
* @return 所有的权限属性
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 表示当前对象支持处理的受保护对象是FilterInvocation。
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 使用配置好的CustomSecurityMetadataSource来代替默认的SecurityMetadataSource对象
object.setSecurityMetadataSource(customSecurityMetadataSource);
// 将rejectPublicInvocations设置为true,表示当getAttributes返回null时,不允许访问受保护对象
object.setRejectPublicInvocations(true);
return object;
}
});
http.formLogin()
.and()
.csrf().disable();
}
}
测试
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin() {
return "Hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "Hello user";
}
@GetMapping("/guest/hello")
public String guest() {
return "Hello guest";
}
/**
* 由于rejectPublicInvocations设置为true,因此,只有具备该接口权限的用户才能访问
*/
@GetMapping("/hello")
public String hello() {
return "Hello";
}
}
13.5基于方法的权限管理
基于方法的权限管理主要是通过AOP来实现的,spring security中通过
MethodSecurityInterceptor
来提供相关的实现。不同在于,FilterSecurityInterceptor
只是在请求之前进行前置处理,MethodSecurityInterceptor
在此基础上还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。
13.5.1注解介绍
目前在spring boot中基于方法的权限管理主要是通过注解来实现,需要通过
@EnableGlobalMethodSecurity
注解开启权限注解的使用:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
这个注解中,设置了三个属性:
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)
public class SecurityConfig {
}
创建
User
类:
public class User {
private Integer id;
private String username;
// 省略构造器、toString和getter/setter
}
@PreAuthorize
@Service
public class HelloService {
// 执行该方法必须具备ADMIN角色才可以访问
@PreAuthorize("hasRole('ADMIN')")
public String preAuthorizeTest01() {
return "Hello";
}
// 访问者名称必须是javaboy,而且还需要同事具备ADMIN角色才可以访问
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'javaboy'")
public String preAuthorizeTest02() {
return "Hello";
}
// 通过#引用方法参数,并对其进行校验,表示请求者的用户名必须等于方法参数name的值,方法才可以被执行
@PreAuthorize("authentication.name == #name")
public String preAuthorizeTest03(String name) {
return "Hello: " + name;
}
}
@SpringBootTest
class BasedOnMethodApplicationTests {
@Autowired
HelloService helloService;
@Test
// 通过该注解设定当前执行的用户角色是ADMIN
@WithMockUser(roles = "ADMIN")
void preAuthorizeTest01() {
String hello = helloService.preAuthorizeTest01();
Assertions.assertNotNull(hello);
Assertions.assertEquals("Hello", hello);
}
@Test
@WithMockUser(roles = "ADMIN", username = "javaboy")
void preAuthorizeTest02() {
String hello = helloService.preAuthorizeTest02();
Assertions.assertNotNull(hello);
Assertions.assertEquals("Hello", hello);
}
@Test
@WithMockUser(username = "javaboy")
void preAuthorizeTest03() {
String hello = helloService.preAuthorizeTest03("javaboy");
Assertions.assertNotNull(hello);
Assertions.assertEquals("Hello: javaboy", hello);
}
}
@PreFilter
@Service
public class HelloService {
/**
* PreFilter主要是对方法的请求参数进行过滤,它里边包含了一个内置对象filterObject,表示具体元素。
* 如果方法只有一个参数,则内置的filterObject对象就代表该参数;如果有多个参数,则需要通过filterTarget
* 来指定filterObject到底代表哪个对象。
* 表示只保留id为奇数的user对象。
*/
@PreFilter(filterTarget = "users", value = "filterObject.id % 2 != 0")
public void preFilterTest(List<User> users) {
System.out.println("users = " + users);
}
}
@Test
@WithMockUser(username = "javaboy")
void preFilterTest() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User(i, "javaboy: " + i));
}
helloService.preFilterTest(users);
}
@PostAuthorize
@Service
public class HelloService {
/**
* PostAuthorize是在目标方法执行之后进行权限校验,其实这个主要是在ACL权限模型中会用到,目标方法执行完毕后,
* 通过该注解去校验目标方法的返回值是否满足相应的权限要求。从技术角度来讲,该注解中也可以使用权限表达式,但是
* 在实际开发中权限表达式一般都是结合PreAuthorize注解一起使用的。PostAuthorize包含一个内置对象returnObject,
* 表示方法的返回值,开发中可以对返回值进行校验。
*/
@PostAuthorize("returnObject.id == 1")
public User postAuthorizeTest(Integer id) {
return new User(id, "javaboy");
}
}
@Test
@WithMockUser(username = "javaboy")
void postAuthorizeTest() {
User user = helloService.postAuthorizeTest(1);
Assertions.assertNotNull(user);
Assertions.assertEquals(1, user.getId());
Assertions.assertEquals("javaboy", user.getUsername());
}
@PostFilter
@Service
public class HelloService {
/**
* PostFilter注解在目标方法执行之后,对目标方法的返回结果进行过滤,该注解中包含了一个内置对象filterObject,
* 表示目标方法返回的集合/数组中的具体元素。
*/
@PostFilter("filterObject.id % 2 == 0")
public List<User> postFilterTest() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User(i, "javaboy: " + i));
}
return users;
}
}
@Test
@WithMockUser(roles = "ADMIN")
void postFilterTest() {
List<User> all = helloService.postFilterTest();
Assertions.assertNotNull(all);
Assertions.assertEquals(5, all.size());
Assertions.assertEquals(2, all.get(1).getId());
}
@Secured
@Service
public class HelloService {
// 该注解不支持权限表达式,只能做一些简单的权限描述
@Secured({"ROLE_ADMIN", "ROLE_USER"})
public User securedTest(String username) {
return new User(99, username);
}
}
@Test
@WithMockUser(roles = "ADMIN")
void securedTest() {
User user = helloService.securedTest("javaboy");
Assertions.assertNotNull(user);
Assertions.assertEquals(99, user.getId());
Assertions.assertEquals("javaboy", user.getUsername());
}
@DenyAll
@Service
public class HelloService {
// JSR-250:拒绝所有访问
@DenyAll
public void denyAllTest() {
}
}
@Test
@WithMockUser(username = "javaboy")
void denyAllTest() {
helloService.denyAllTest();
}
@PermitAll
@Service
public class HelloService {
// JSR-250:允许所有访问
@PermitAll
public void permitAllTest() {
}
}
@Test
@WithMockUser(username = "javaboy")
void permitAllTest() {
helloService.permitAllTest();
}
@RolesAllowed
@Service
public class HelloService {
/**
* JSR-250:可以添加在方法上或类上,当添加在类上时,表示该注解对类中的所有方法生效;如果类上和方法上都有该注解,
* 并且起冲突,则以方法上的注解为准。
*/
@RolesAllowed({"ADMIN", "USER"})
public String rolesAllowedTest() {
return "RolesAllowed";
}
}
@Test
@WithMockUser(roles = "ADMIN")
void rolesAllowedTest() {
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
方法:
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 调用父类的beforeInvocation方法进行权限校验
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
// 校验通过后,调用mi.proceed()方法继续执行目标方法
result = mi.proceed();
}
finally {
// 在finally块中调用finallyInvocation方法完成一些清理工作
super.finallyInvocation(token);
}
// 最后调用父类的afterInvocation方法进行请求结果的过滤
return super.afterInvocation(token, result);
}
EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity
用来开启方法的权限注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 引入了一个配置GlobalMethodSecuritySelector,该类的作用主要是用来导入外部配置类
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
// 省略其他
}
final class GlobalMethodSecuritySelector implements ImportSelector {
// importingClassMetadata中保存了@EnableGlobalMethodSecurity注解的元数据,包括各个属性的值、注解是加在哪个配置类上等
@Override
public String[] 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 = new ArrayList<>(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(new String[0]);
}
}
selectImports
方法的逻辑比较简单,要导入的外部配置类有以下几种:
MethodSecurityMetadataSourceAdvisorRegistrar
:如果使用的是spring自带的AOP,则该配置类会被导入。该类主要用来向spring容器中注册一个MethodSecurityMetadataSourceAdvisor
对象,这个对象中定义了AOP中的pointcut和advice。autoProxyClassName
:注册自动代理创建者,根据不同的代理模式而定。GlobalMethodSecurityConfiguration
:这个配置类用来提供MethodSecurityMetadataSource
和MethodInterceptor
两个关键对象。如果开发者自定义配置类继承自GlobalMethodSecurityConfiguration
,则这里不会导入这个外部配置类。Jsr250MetadataSourceConfiguration
:如果开启了JSR-250注解,这会导入该配置类。该配置类主要用来提供JSR-250注解所需的Jsr250MethodSecurityMetadataSource
对象。
先来看
MethodSecurityMetadataSourceAdvisorRegistrar
:
class MethodSecurityMetadataSourceAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 首先定义BeanDefinitionBuilder
BeanDefinitionBuilder 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也就是增强/通知,就是将方法拦截下来之后要增强的功能
*/
public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private transient MethodSecurityMetadataSource attributeSource;
private transient MethodInterceptor interceptor;
// 这里的pointcut对象就是内部类MethodSecurityMetadataSourcePointcut,在它的matches方法中,定义了具体的拦截规则
private final Pointcut pointcut = new MethodSecurityMetadataSourcePointcut();
private BeanFactory beanFactory;
private final String adviceBeanName;
private final String metadataSourceBeanName;
private transient volatile Object adviceMonitor = new Object();
// 构造方法所需的三个参数就是MethodSecurityMetadataSourceAdvisorRegistrar类中提供的三个参数
public MethodSecurityMetadataSourceAdvisor(String adviceBeanName, MethodSecurityMetadataSource attributeSource,
String attributeSourceBeanName) {
this.adviceBeanName = adviceBeanName;
this.attributeSource = attributeSource;
this.metadataSourceBeanName = attributeSourceBeanName;
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
/**
* Advice由getAdvice方法返回,在该方法内部,就是去spring容器中查找一个名为
* methodSecurityInterceptor的MethodInterceptor对象。
*/
@Override
public Advice getAdvice() {
synchronized (this.adviceMonitor) {
if (this.interceptor == null) {
this.interceptor = this.beanFactory.getBean(this.adviceBeanName, MethodInterceptor.class);
}
return this.interceptor;
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.adviceMonitor = new Object();
this.attributeSource = this.beanFactory.getBean(this.metadataSourceBeanName,
MethodSecurityMetadataSource.class);
}
class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
@Override
public boolean matches(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)
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
// 创建List集合,用来保存所有的MethodSecurityMetadataSource对象
List<MethodSecurityMetadataSource> sources = new ArrayList<>();
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
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(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
}
// 如果@EnableGlobalMethodSecurity注解配置了securedEnabled=true,
// 则加入SecuredAnnotationSecurityMetadataSource对象来解析相应的注解
if (isSecuredEnabled) {
sources.add(new SecuredAnnotationSecurityMetadataSource());
}
// 如果@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返回即可
return new DelegatingMethodSecurityMetadataSource(sources);
}
可以看到,默认提供的
MethodSecurityMetadataSource
对象实际上是一个代理对象,它包含多个不同的MethodSecurityMetadataSource
实例。在判断一个方法是否需要被拦截下来时,由这些被代理的对象逐个去解析目标方法是否含有相应的注解(例如PrePostAnnotationSecurityMetadataSource
可以检查出目标方法是否含有@PostAuthorize
、@PostFilter
、@PreAuthorize
以及@PreFilter
),如果有,则请求就会被拦截下来。文章来源:https://www.toymoban.com/news/detail-411740.html
再来看
MethodInterceptor
的定义:文章来源地址https://www.toymoban.com/news/detail-411740.html
@Bean
public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
// 查看代理的方式,默认使用spring自带的AOP,所以使用MethodSecurityInterceptor来创建对应的MethodInterceptor实例
this.methodSecurityInterceptor = isAspectJ() ? new AspectJMethodSecurityInterceptor()
: new MethodSecurityInterceptor();
// 然后给methodSecurityInterceptor设置AccessDecisionManager决策管理器
this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
// 接下来给methodSecurityInterceptor配置后置处理器
this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
// 最后再把前面创建好的MethodSecurityMetadataSource对象配置给methodSecurityInterceptor
this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
RunAsManager runAsManager = runAsManager();
if (runAsManager != null) {
this.methodSecurityInterceptor.setRunAsManager(runAsManager);
}
return this.methodSecurityInterceptor;
}
protected AccessDecisionManager accessDecisionManager() {
// 默认的决策管理器是AffirmativeBased
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
// 根据@EnableGlobalMethodSecurity注解的配置,配置不同的投票器
if (prePostEnabled()) {
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
}
if (jsr250Enabled()) {
decisionVoters.add(new Jsr250Voter());
}
RoleVoter roleVoter = new RoleVoter();
GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
if (grantedAuthorityDefaults != null) {
roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
decisionVoters.add(roleVoter);
decisionVoters.add(new AuthenticatedVoter());
return new AffirmativeBased(decisionVoters);
}
protected AfterInvocationManager afterInvocationManager() {
// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,则添加一个后置处理器
// PostInvocationAdviceProvider,该类用来处理@PostAuthorize和@PostFilter两个注解
if (prePostEnabled()) {
AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
getExpressionHandler());
PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice);
List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
afterInvocationProviders.add(postInvocationAdviceProvider);
invocationProviderManager.setProviders(afterInvocationProviders);
return invocationProviderManager;
}
return null;
}
到了这里,关于13.Spring security权限管理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!