SpringSide 3 中的安全框架

ljx1619發表於2009-09-14

在SpringSide 3的官方文件中,說安全框架使用的是Spring Security 2.0。乍一看,嚇了我一跳,以為Acegi這麼快就被淘汰了呢。上搜尋引擎一搜,發現原來Spring Security 2.0就是Acegi 2.0。懸著的心放下來了。雖然SpringSide 3中關於Acegi的配置檔案看起來很不熟悉,但是讀了Acegi 2.0的官方文件後,一切都釋然了。

先來談一談Acegi的基礎知識,Acegi的架構比較複雜,但是我希望我下面的隻言片語能夠把它說清楚。大家都知道,如果要對Web資源進行保護,最好的辦法莫過於Filter,要想對方法呼叫進行保護,最好的辦法莫過於AOP。Acegi對Web資源的保護,就是靠Filter實現的。如下圖:

 




一般來說,我們的Filter都是配置在web.xml中,但是Acegi不一樣,它在web.xml中配置的只是一個代理,而真正起作用的Filter是作為Bean配置在Spring中的。web.xml中的代理依次呼叫這些Bean,就實現了對Web資源的保護,同時這些Filter作為Bean被Spring管理,所以實現AOP也很簡單,真的是一舉兩得啊。

Acegi中提供的Filter不少,有十多個,一個一個學起來比較複雜。但是對於我們Web開發者來說,常用的就那麼幾個,如下圖中的被紅圈圈標記出來的:



從上到下,它們實現的功能依次是1、制定必須為https連線;2、從Session中提取使用者的認證資訊;3、退出登入;4、登入;5、記住使用者;6、所有的應用必須配置這個Filter。

一般來說,我們寫Web應用只需要熟悉這幾個Filter就可以了,如果不需要https連線,連第一個也不用熟悉。但是有人肯定會想,這些Filter怎麼和我的資料庫聯絡起來呢?不用著急,這些Filter並不直接處理使用者的認證,也不直接處理使用者的授權,而是把它們交給了認證管理器和決策管理器。如下圖:



對於這兩種管理器,那也是不需要我們寫程式碼的,Acegi也提供了現成的類。那麼大家又奇怪了:又是現成的,那怎麼和我的資料庫關聯起來呢?彆著急,其實這兩個管理器自己也不做事,認證管理器把任務交給了Provider,而決策管理器則把任務交給了Voter,如下圖:




現在我要告訴你們,這裡的Provider和Voter也是不需要我們寫程式碼的。不要崩潰,快到目標了。Acegi提供了多個Provider的實現類,如果我們想用資料庫來儲存使用者的認證資料,那麼我們就選擇DaoAuthenticationProvider。對於Voter,我們一般選擇RoleVoter就夠用了,它會根據我們配置檔案中的設定來決定是否允許某一個使用者訪問制定的Web資源。

而DaoAuthenticationProvider也是不直接運算元據庫的,它把任務委託給了UserDetailService,如下圖:




而我們要做的,就是實現這個UserDetailService。圖畫得不好,大家不要見笑,但是說了這麼多總算是引出了我們開發中的關鍵,那就是我們要實現自己的UserDetailService,它就是連線我們的資料庫和Acegi的橋樑。UserDetailService的要求也很簡單,只需要一個返回org.springframework.security.userdetails.User物件的loadUserByUsername(String userName)方法。因此,怎麼設計資料庫都可以,不管我們是用一個表還是兩個表還是三個表,也不管我們是使用者-授權,還是使用者-角色-授權,還是使用者-使用者組-角色-授權,這些具體的東西Acegi統統不關心,它只關心返回的那個User物件,至於怎麼從資料庫中讀取資料,那就是我們自己的事了。

反過來再看看上面的過程,我們發現,即使我們要做的只是實現自己的UserDetailService類,但是我們不得不在Spring中配置那一大堆的Bean,包括幾個Filter,幾個Manager,幾個Provider和Voter,而這些配置往往都是重複的無謂的。好在Acegi 2.0也認識到了這個問題,所以,它設計了一個<http>標籤,讓Acegi的配置得到了簡化。下面是SpringSide 3中的配置的截圖,大家可以看看:


下圖是官方文章中的傳統Filter設定和<http>元素之間的對應關係:
 


下面的程式碼是SpringSide 3中實現UserDetailService的範例,在SpringSide 3的範例中,白衣使用了三個表User、Role、Authority。但是Acegi不關心你用了幾個表,它只關心UserDetails物件。而決定使用者能否訪問指定Web資源的,是RoleVoter類,無需任何修改它可以工作得很好,唯一的缺點是它只認ROLE_字首,所以搞得白衣的Authority看起來都象角色,不倫不類。

package  personal.youxia.service.security;

import  java.util.ArrayList;
import  java.util.List;

import  org.springframework.beans.factory.annotation.Required;
import  org.springframework.dao.DataAccessException;
import  org.springframework.security.GrantedAuthority;
import  org.springframework.security.GrantedAuthorityImpl;
import  org.springframework.security.userdetails.UserDetails;
import  org.springframework.security.userdetails.UserDetailsService;
import  org.springframework.security.userdetails.UsernameNotFoundException;
import  personal.youxia.entity.user.Authority;
import  personal.youxia.entity.user.Role;
import  personal.youxia.entity.user.User;
import  personal.youxia.service.user.UserManager;

/**
* 實現SpringSecurity的UserDetailsService介面,獲取使用者Detail資訊.
*
*  @author  calvin
  */
public   class  UserDetailServiceImpl  implements  UserDetailsService {

     private  UserManager userManager;

     public  UserDetails loadUserByUsername(String userName)  throws  UsernameNotFoundException, DataAccessException {
        User user  =  userManager.getUserByLoginName(userName);
         if  (user  ==   null )
             throw   new  UsernameNotFoundException(userName  +   "  不存在 " );

        List < GrantedAuthority >  authsList  =   new  ArrayList < GrantedAuthority > ();

         for  (Role role : user.getRoles()) {
             for  (Authority authority : role.getAuths()) {
                authsList.add( new  GrantedAuthorityImpl(authority.getName()));
            }
        }

         //  目前在MultiDatabaseExample的User類中沒有enabled, accountNonExpired,credentialsNonExpired, accountNonLocked等屬性
         //  暫時全部設為true,在需要時才新增這些屬性.
        org.springframework.security.userdetails.User userdetail  =   new  org.springframework.security.userdetails.User(
                user.getLoginName(), user.getPassword(),  true ,  true ,  true ,  true , authsList
                        .toArray( new  GrantedAuthority[authsList.size()]));

         return  userdetail;
    }

    @Required
     public   void  setUserManager(UserManager userManager) {
         this .userManager  =  userManager;
    }
}


最後再來說說這個命名的問題,我對Authentication和Authority這兩個單詞比較反感,兩個原因,一是因為它們太生僻了,二是因為它們長得太像了,明明一個是認證,一個是授權,意思相差很遠,外貌卻如此相似,確實很煩人。如果讓我來選擇,我喜歡Privilege這個單詞,在我剛使用MySQL的時候就跟它很熟了,所以在我的專案中,我可能會用Privilege來代替Authority。如果我們只使用User-Role兩級關係,使用RoleVoter預設的ROLE_字首當然沒有關係,如果是像白衣這樣是用三層關係,最好還是把這個字首改一改,以免混淆。

 

 

補充:

前文所講的是我對Acegi的一些理解,我認為只有把條理搞清楚了,才更容易深入。我想得比較簡單,當然會漏掉一些細節。這裡把它補充一下。

1、我上面講到的主要內容,包含了認證和授權,但是漏掉了資源,資源就是我們需要保護的URL,或者一些類中的方法。要保護URL,在xml檔案中按照前面的例子配置就可以了,要保護類中的方法,使用@secured就可以了。
但是它們是和Acegi中的哪個元件關聯起來的呢?是FilterSecurityInterceptor和MethodSecurityInterceptor,這兩個Interceptor都需要設定一個叫objectDefinitionSource的屬性。所以,有人要問,如何把對資源的保護設定全部轉移到資料庫中,避免寫在xml中,那就要從這個objectDefinitionSource著手了。

2、前面講到了UserDetailService,事實上在Acegi中還需要配置別的Service,如RememberMeService,當然,該Service也是現成的,不需要我們寫程式碼的,RememberMeProcessingFilter需要依賴這個Service。

3、Acegi支援OpenID和CAS 3,這兩個東西是幹什麼的呢?是可以實現單點登入功能的,也就是允許使用者只登入一次,就可以使用多個網站。這對於那些很龐大的網站非常有用,可以把使用者登入這樣的操作集中在一臺伺服器上。要使用CAS,只需要配置Filter時選擇CASProcessingFilte,配置Provider時選擇CasAuthenticationProvider,其餘的概念都是相通的。具體的實現細節,大家慢慢摸索吧。

 

相關文章