1. 程式人生 > >spring security基於方法的許可權控制

spring security基於方法的許可權控制

1.1     intercept-methods定義方法許可權控制

       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錯誤碼到客戶端。

1.2     使用pointcut定義方法許可權控制

       基於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匹配的時候是按照宣告順序進行匹配的,一旦匹配上了後續的將不再進行匹配了。

1.3     使用註解定義方法許可權控制

       基於註解的方法許可權控制也是需要通過global-method-security元素定義來進行啟用的。Spring Security在方法的許可權控制上支援三種類型的註解,JSR-250註解、@Secured註解和支援表示式的註解。這三種註解預設都是沒有啟用的,需要單獨通過global-method-security元素的對應屬性進行啟用。

1.3.1   JSR-250註解

       要使用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定義的方法只是在我們的許可權控制中不能訪問,脫離了許可權控制還是可以訪問的。

1.3.2  @Secured註解

       @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()。

1.3.3   支援表示式的註解

       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) {

      ...

   }

1.4     方法許可權控制的攔截器

       關於方法許可權控制,Spring Security提供了兩類AbstractSecurityInterceptor,基於AOP Alliance的MethodSecurityInterceptor,和基於Aspectj繼承自MethodSecurityInterceptor的AspectJMethodSecurityInterceptor。

1.4.1   MethodSecurityInterceptor

       當我們在使用基於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許可權。

1.4.2   AspectJMethodSecurityInterceptor

       AspectJMethodSecurityInterceptor是繼承自MethodSecurityInterceptor的,不同的是AspectJMethodSecurityInterceptor是用來支援AspectJ的JointPoint的,但在底層還是會把它封裝成一個MethodInvocation進行呼叫。

(注:本文是基於Spring Security3.1.6所寫)