1.1 intercept-methods 定义方法权限控制
1.2 使用 pointcut 定义方法权限控制
1.3 使用注解定义方法权限控制
1.3.1 JSR-250 注解
1.3.2 @Secured 注解
1.3.3 支持表达式的注解
1.4 方法权限控制的拦截器
1.4.1 MethodSecurityInterceptor
1.4.2 AspectJMethodSecurityInterceptor
之前介绍的都是基于 URL 的权限控制, Spring Security 同样支持对于方法的权限控制。可以通过 intercept-methods 对某个 bean 下面的方法进行权限控制,也可以通过 pointcut 对整个 Service 层的方法进行统一的权限控制,还可以通过注解定义对单独的某一个方法进行权限控制。
intercept-methods 是需要定义在 bean 元素下的,通过它可以定义对当前的 bean 的某些方法进行权限控制,具体方法是使用其下的子元素 protect 进行定义的。 protect 元素需要指定两个属性, access 和 method , method 表示需要拦截的方法名称,可以使用通配符, access 表示执行对应的方法需要拥有的权限,多个权限之间可以使用逗号分隔。
< bean id = "userService" class = "com.xxx.service.impl.UserServiceImpl" >
< security:intercept-methods >
< security:protect access = "ROLE_USER" method = "find*" />
< security:protect access = "ROLE_ADMIN" method = "add*" />
< security:protect access = "ROLE_ADMIN" method = "update*" />
< security:protect access = "ROLE_ADMIN" method = "delete*" />
</ security:intercept-methods >
</ bean >
在上面的配置中表示在执行 UserServiceImpl 的方法名以 find 开始的方法时需要当前用户拥有 ROLE_USER 的权限,在执行方法名以 add 、 update 或 delete 开始的方法时需要拥有 ROLE_ADMIN 的权限。当访问被拒绝时还是交由 ExceptionTranslationFilter 处理,这也就意味着如果用户未登录则会引导用户进行登录,否则默认将返回 403 错误码到客户端。
基于 pointcut 的方法权限控制是通过 global-method-security 下的 protect-pointcut 来定义的。可以在 global-method-security 元素下定义多个 protect-pointcut 以对不同的 pointcut 使用不同的权限控制。
< security:global-method-security >
< security:protect-pointcut access = "ROLE_READ" expression = "execution(* com.elim.*..*Service.find*(..))" />
< security:protect-pointcut access = "ROLE_WRITE" expression = "execution(* com.elim.*..*Service.*(..))" />
</ security:global-method-security >
上面的定义表示我们在执行 com.elim 包或其子包下任意以 Service 结尾的类,其方法名以 find 开始的所有方法时都需要用户拥有 ROLE_READ 的权限,对于 com.elim 包或其子包下任意以 Service 结尾的类的其它方法在执行时都需要 ROLE_WRITE 的权限。需要注意的是对应的类需要是定义在 ApplicationContext 中的 bean 才行。此外同对于 URL 的权限控制一样,当定义多个 protect-pointcut 时更具有特性的应当先定义,因为在 pointcut 匹配的时候是按照声明顺序进行匹配的,一旦匹配上了后续的将不再进行匹配了。
基于注解的方法权限控制也是需要通过 global-method-security 元素定义来进行启用的。 Spring Security 在方法的权限控制上支持三种类型的注解, JSR-250 注解、 @Secured 注解和支持表达式的注解。这三种注解默认都是没有启用的,需要单独通过 global-method-security 元素的对应属性进行启用。
要使用 JSR-250 注解,首先我们需要通过设置 global-method-security 元素的 jsr250-annotation=”enabled” 来启用基于 JSR-250 注解的支持,默认为 disabled 。
<security:global-method-security jsr250-annotations= "enabled" />
此外,还需要确保添加了 jsr250-api 到我们的类路径下。之后就可以在我们的 Service 方法上使用 JSR-250 注解进行权限控制了。
@Service
@RolesAllowed ( "ROLE_ADMIN" )
public class UserServiceImpl implements UserService {
public void addUser(User user) {
System. out .println( "addUser................" + user);
}
public void updateUser(User user) {
System. out .println( "updateUser.............." + user);
}
@RolesAllowed ({ "ROLE_USER" , "ROLE_ADMIN" })
public User find( int id) {
System. out .println( "find user by id............." + id);
return null ;
}
public void delete( int id) {
System. out .println( "delete user by id................" );
}
@RolesAllowed ( "ROLE_USER" )
public List<User> findAll() {
System. out .println( "find all user..............." );
return null ;
}
}
上面的代码表示执行 UserServiceImpl 里面所有的方法都需要角色 ROLE_ADMIN ,其中 findAll() 方法的执行需要 ROLE_USER 角色,而 find() 方法的执行对于 ROLE_USER 或者 ROLE_ADMIN 角色都可以。
顺便介绍一下 JSR-250 中对权限支持的注解。
RolesAllowed 表示访问对应方法时所应该具有的角色。其可以标注在类上,也可以标注在方法上,当标注在类上时表示其中所有方法的执行都需要对应的角色,当标注在方法上表示执行该方法时所需要的角色,当方法和类上都使用了 @RolesAllowed 进行标注,则方法上的 @RolesAllowed 将覆盖类上的 @RolesAllowed ,即方法上的 @RolesAllowed 将对当前方法起作用。 @RolesAllowed 的值是由角色名称组成的数组。
PermitAll 表示允许所有的角色进行访问,也就是说不进行权限控制。 @PermitAll 可以标注在方法上也可以标注在类上,当标注在方法上时则只对对应方法不进行权限控制,而标注在类上时表示对类里面所有的方法都不进行权限控制。( 1 )当 @PermitAll 标注在类上,而 @RolesAllowed 标注在方法上时则按照 @RolesAllowed 将覆盖 @PermitAll ,即需要 @RolesAllowed 对应的角色才能访问。( 2 )当 @RolesAllowed 标注在类上,而 @PermitAll 标注在方法上时则对应的方法也是不进行权限控制的。( 3 )当在方法上同时使用了 @PermitAll 和 @RolesAllowed 时先定义的将发生作用,而都定义在类上时则是反过来的,即后定义的将发生作用(这个没多大的实际意义,实际应用中不会有这样的定义)。
DenyAll 是和 PermitAll 相反的,表示无论什么角色都不能访问。 @DenyAll 只能定义在方法上。你可能会有疑问使用 @DenyAll 标注的方法无论拥有什么权限都不能访问,那还定义它干啥呢?使用 @DenyAll 定义的方法只是在我们的权限控制中不能访问,脱离了权限控制还是可以访问的。
@Secured 是由 Spring Security 定义的用来支持方法权限控制的注解。它的使用也是需要启用对应的支持才会生效的。通过设置 global-method-security 元素的 secured-annotations=”enabled” 可以启用 Spring Security 对使用 @Secured 注解标注的方法进行权限控制的支持,其值默认为 disabled 。
< security:global-method-security secured-annotations = "enabled" />
@Service
public class UserServiceImpl implements UserService {
@Secured ( "ROLE_ADMIN" )
public void addUser(User user) {
System. out .println( "addUser................" + user);
}
@Secured ( "ROLE_USER" )
public List<User> findAll() {
System. out .println( "find all user..............." );
return null ;
}
}
在上面的代码中我们使用 @Secured 定义了只有拥有 ROLE_ADMIN 角色的用户才能调用方法 addUser() ,只有拥有 ROLE_USER 角色的用户才能调用方法 findAll() 。
Spring Security 中定义了四个支持使用表达式的注解,分别是 @PreAuthorize 、 @PostAuthorize 、 @PreFilter 和 @PostFilter 。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。要使它们的定义能够对我们的方法的调用产生影响我们需要设置 global-method-security 元素的 pre-post-annotations=”enabled” ,默认为 disabled 。
< security:global-method-security pre-post-annotations = "disabled" />
使用 @PreAuthorize 和 @PostAuthorize 进行访问控制
@PreAuthorize 可以用来控制一个方法是否能够被调用。
@Service
public class UserServiceImpl implements UserService {
@PreAuthorize ( "hasRole('ROLE_ADMIN')" )
public void addUser(User user) {
System. out .println( "addUser................" + user);
}
@PreAuthorize ( "hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')" )
public User find( int id) {
System. out .println( "find user by id............." + id);
return null ;
}
}
在上面的代码中我们定义了只有拥有角色 ROLE_ADMIN 的用户才能访问 adduser() 方法,而访问 find() 方法需要有 ROLE_USER 角色或 ROLE_ADMIN 角色。使用表达式时我们还可以在表达式中使用方法参数。
public class UserServiceImpl implements UserService {
/**
* 限制只能查询 Id 小于 10 的用户
*/
@PreAuthorize ( "#id<10" )
public User find( int id) {
System. out .println( "find user by id........." + id);
return null ;
}
/**
* 限制只能查询自己的信息
*/
@PreAuthorize ( "principal.username.equals(#username)" )
public User find(String username) {
System. out .println( "find user by username......" + username);
return null ;
}
/**
* 限制只能新增用户名称为 abc 的用户
*/
@PreAuthorize ( "#user.name.equals('abc')" )
public void add(User user) {
System. out .println( "addUser............" + user);
}
}
在上面代码中我们定义了调用 find(int id) 方法时,只允许参数 id 小于 10 的调用;调用 find(String username) 时只允许 username 为当前用户的用户名;定义了调用 add() 方法时只有当参数 user 的 name 为 abc 时才可以调用。
有时候可能你会想在方法调用完之后进行权限检查,这种情况比较少,但是如果你有的话, Spring Security 也为我们提供了支持,通过 @PostAuthorize 可以达到这一效果。使用 @PostAuthorize 时我们可以使用内置的表达式 returnObject 表示方法的返回值。我们来看下面这一段示例代码。
@PostAuthorize ( "returnObject.id%2==0" )
public User find( int id) {
User user = new User();
user.setId(id);
return user;
}
上面这一段代码表示将在方法 find() 调用完成后进行权限检查,如果返回值的 id 是偶数则表示校验通过,否则表示校验失败,将抛出 AccessDeniedException 。 需要注意的是 @PostAuthorize 是在方法调用完成后进行权限检查,它不能控制方法是否能被调用,只能在方法调用完成后检查权限决定是否要抛出 AccessDeniedException 。
使用 @PreFilter 和 @PostFilter 进行过滤
使用 @PreFilter 和 @PostFilter 可以对集合类型的参数或返回值进行过滤。使用 @PreFilter 和 @PostFilter 时, Spring Security 将移除使对应表达式的结果为 false 的元素。
@PostFilter ( "filterObject.id%2==0" )
public List<User> findAll() {
List<User> userList = new ArrayList<User>();
User user;
for ( int i=0; i<10; i++) {
user = new User();
user.setId(i);
userList.add(user);
}
return userList;
}
上述代码表示将对返回结果中 id 不为偶数的 user 进行移除。 filterObject 是使用 @PreFilter 和 @PostFilter 时的一个内置表达式,表示集合中的当前对象。当 @PreFilter 标注的方法拥有多个集合类型的参数时,需要通过 @PreFilter 的 filterTarget 属性指定当前 @PreFilter 是针对哪个参数进行过滤的。如下面代码就通过 filterTarget 指定了当前 @PreFilter 是用来过滤参数 ids 的。
@PreFilter (filterTarget= "ids" , value= "filterObject%2==0" )
public void delete(List<Integer> ids, List<String> usernames) {
...
}
关于方法权限控制, Spring Security 提供了两类 AbstractSecurityInterceptor ,基于 AOP Alliance 的 MethodSecurityInterceptor ,和基于 Aspectj 继承自 MethodSecurityInterceptor 的 AspectJMethodSecurityInterceptor 。
当我们在使用基于 NameSpace 进行方法保护的配置时, Spring Security 默认配置的就是 MethodSecurityInterceptor 。根据配置的不同,一个拦截器可能只是针对于一个 bean ,也可能是针对于多个 bean 的。 MethodSecurityInterceptor 使用一个 MethodSecurityMetadataSource 的实例来获取特定方法调用配置的 ConfigAttribute 。当我们在 ApplicationContext 配置文件中使用 intercept-methods 元素或 protect-point 元素定义需要保护的方法调用时, Spring Security 内部默认会使用一个 MapBasedMethodSecurityMetadataSource 来保存在这些元素上定义的配置信息,保存的 key 是对应的方法名(可以是含有通配符的)。类似的使用 JSR-250 注解时将使用 Jsr250MethodSecurityMetadataSource 解析配置属性;使用 @Secured 注解时将使用 SecuredAnnotationSecurityMetadataSource 解析配置属性;使用 pre-post-annotations 时将使用 PrePostAnnotationSecurityMetadataSource 解析配置属性。
MethodSecurityInterceptor 是实现了 MethodInterceptor 接口的,所以我们在使用 Spring Aop 时,可以自己配置一个 MethodSecurityInterceptor 的 bean 。
<!-- 自定义 MethodSecurityInterceptor -->
< bean id = "methodSecurityInterceptor"
class = "org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor" >
< property name = "authenticationManager" ref = "authenticationManager" />
< property name = "accessDecisionManager" ref = "accessDecisionManager" />
< property name = "afterInvocationManager" ref = "afterInvocationManager" />
< property name = "securityMetadataSource" >
< security:method-security-metadata-source >
<!-- 指定需要受保护的方法和需要的权限 -->
< security:protect method = "com.xxx.service.UserService.find*"
access = "ROLE_USER" />
< security:protect method = "com.xxx.service.UserService.delete*"
access = "ROLE_ADMIN" />
</ security:method-security-metadata-source >
</ property >
</ bean >
定义了 MethodSecurityInterceptor 以后,我们需要类似 AOP 配置那样,配置哪些该 MethodInterceptor 需要拦截哪些方法的执行。这种可选配置是很多种的,因为我们这里只是拦截 UserService 中的具体方法,所以就采用基于 bean name 的自动代理。
<!-- 基于 bean 的拦截 -->
< bean class = "org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >
< property name = "interceptorNames" >
< list >
< value > methodSecurityInterceptor </ value >
</ list >
</ property >
< property name = "beanNames" >
< list >
< value > userService </ value >
</ list >
</ property >
</ bean >
按照上面的配置,我们在访问 UserService 的 find 方法时就需要 ROLE_USER 的权限,而访问 delete 方法时则需要 ROLE_ADMIN 权限。
AspectJMethodSecurityInterceptor 是继承自 MethodSecurityInterceptor 的,不同的是 AspectJMethodSecurityInterceptor 是用来支持 AspectJ 的 JointPoint 的,但在底层还是会把它封装成一个 MethodInvocation 进行调用。
(注:本文是基于 Spring Security3.1.6 所写)