HDC調試需求開發(fā)(15萬預算),能者速來!>>>
安全,可是說是個老生常談的話題,也可以說是個永恒的話題,無論是在那個領域,那個行業(yè),安全都是非常的重要,寫本文時正好看到一些礦難和香港游客 在印尼發(fā)生災難的新聞,可見我們周圍到處都需要安全保駕護航,盡管安全是相對的,但不采取任何措施來應對安全問題帶來的挑戰(zhàn),其后果將是慘不忍睹的,因此 我們要重視安全性問題,無論是在生活中,還是在軟件領域,畢竟生活幸福,使用的軟件安全,都是我們希望的。閑話少說,下面我們就針對 Web Services 的安全問題逐一分析,講解;首先我們要介紹下 Acegi(Spring Security)。
Acegi(Spring Security) 介紹
Acegi 是一個用于 Spring Framework 的安全框架,能夠和目前流行的 Web 容器無縫集成。它使用了 Spring 的方式提供了安全和認證安全服務服務,包括使用 Bean Context,攔截器和面向接口的編程方式。因此,Acegi 安全系統(tǒng) 能夠輕松地適用于復雜的安全需求。
Acegi 的出現(xiàn)是令軟件開發(fā)人員興奮,激動,歡呼的,用福音來形容一點不為過呀。解決了 Java EE 安全性編程模型下的眾多弊端,比如:便攜性差,因 Java EE 應用必須使用容器提供的一些專有描述符 ; 開發(fā)人員要理解大量的專有描述符也不是一件容易的事情,畢竟不同的容器的描述符存在一定的差別的,有時候就是寫錯了一點,都需要好長時間的調試才能得以解 決 ; 還有就是使用 Java EE 安全性編程模型的應用測試工作很難進行,尤其是在 TDD 流行的當下,不能方便,順利的進行測試的開發(fā)可謂是一大弊端呀。Acegi 就是一名神醫(yī),將這些弊病統(tǒng)統(tǒng)解決,尤其是 Spring Security 的出現(xiàn)使應用 Acegi 更加方便,快捷,下面我們就進入 Spring Security 的旅程。
第一眼 Spring Security
準備環(huán)境 下載 spring-security-3.0.3.RELEASE.zip 包從 http://www.springsource.com/download/community 這個鏈接 ; 將 spring-security-samples-contacts-3.0.3.RELEASE.war 和 spring-security-samples-tutorial-3.0.3.RELEASE.war 復制到 apache-tomcat-6.0.18 的 webapps 下;
步驟 2 中的兩個 war 包是 spring-security-3.0.3.RELEASE.zip 解壓后包含的兩個 Demo,下面我們就是通過這兩個 Demo 來看看 Spring Security 給我們帶來的快感,每個的 Demo 我們都會做詳細的解釋,以使讀者能很清楚的了解 Spring Security 的工作過程。
用戶場景
下面我們模擬下經(jīng)常應用的場景,比如用戶登錄后可以操作業(yè)務方法,我們知道業(yè)務方法不是隨意讓用戶可以調用的,有些業(yè)務方法很敏感,比如電信的計費方法, 涉及分成,結算等關鍵業(yè)務,所以需要有足夠權限的用戶才可以調用,因此我們通常在業(yè)務方法前加入一層來攔截用戶請求,根據(jù)用戶權限來判斷是否可以訪問。
圖 1. 用戶場景實例圖
下面我們主要來研究下在 security 層是怎樣實現(xiàn)業(yè)務方法保護的。
初識 Acegi(contacts Demo)
找到 TOMCAT_HOME 下的 bin 目錄然后點擊 startup.bat 來完成啟動 tomcat 的操作,然后我們在瀏覽器中輸入 http://localhost:8080/spring-security-samples-contacts-3.0.3.RELEASE URL, 我們就可以看到應用的主頁了,從主頁內容中我們能夠知道這個 Demo 所應用的 Spring Security 的功能,比如:Remember-me 認證服務,form-based 認證,BASIC 認證和 Database-sourced security data 等。然后點擊 Manage 超鏈接將會轉到登錄界面,在登錄界面中列出了數(shù)據(jù)庫中的所有用戶信息,包括用戶名,密碼和是否可用字段。我們可以清楚的看到 Contacts Demo 使用的是 HTTP 表單認證,使用自定義 Web 表單頁面來收集用戶憑證信息的這一認證機制由于用戶交互性友好等方面受到廣大用戶的青睞。大家應該很熟悉 HTTP BASIC 認證吧,下面我們就通過修改 applicationContext-security.xml 文件來體驗一下這種認證方式,將 applicationContext-security.xml 文件中清單 3 列出的內容注釋掉。
清單 1. 注釋掉的內容
注:上面的內容是指定了登錄的頁面,用戶登錄時將顯示這個指定的頁面的。
然后我們重新啟動 tomcat 容器,輸入 http://localhost:8080/spring-security-samples-contacts-3.0.3.RELEASE/hello.htm URL. 然后點擊 Manage 超鏈接,顯示的這個界面是由瀏覽器生成的,和 XP 系統(tǒng)登錄界面很像吧。
另外一個 Demo spring-security-samples-tutorial-3.0.3.RELEASE.war 我們不做過多的解釋了,有興趣的讀者可以自行研究下,如有疑問可以和我聯(lián)系的。我們知道 Acegi 即可以保護 Web 資源,又可以保護服務層的業(yè)務方法,還可以保護領域對象。保護服務層的業(yè)務方法的這個功能和本文要講解的 Web Service 密切相關,所以下面我們將通過一個例子來詳細講解下 Acegi 是怎樣來保護服務層的業(yè)務方法的,以使大家有一個全面的理解。
Spring security(Acegi) 保護服務層的業(yè)務方法
首先新建一個 java project 命名為 acegi-method-project,然后新建一個接口命名為 EmployeeManager,接口內容如清單 2 所示:
清單 2. EmployeeManager.java 類代碼
package org.ibm.acegi.beans; import java.util.List; public interface EmployeeManager { /** * Delete employee by employee Id * @param id */ public void deleteEmployeeById(int id); /** * get all employees * @param id * @return List */ public List findAllEmployees(); } |
---|
可以看出我們需要兩個方法 deleteEmployeeById 和 findAllEmployees, 然后新建一個實現(xiàn)類 implement 接口 EmployeeManager,命名為 EmployeeManagerImpl,代碼內容如清單 4 所示:
清單 3. EmployeeManagerImpl.java 類代碼
package org.ibm.acegi.beans; import java.util.ArrayList;import java.util.List;import org.apache.log4j.Logger; public class EmployeeManagerImpl implements EmployeeManager { private static Logger logger = Logger.getLogger(EmployeeManagerImpl.class); @Override public void deleteEmployeeById(int id) { System.out.println("deleteEmployeeById()"); } @Override public List findAllEmployees() { List list = new ArrayList(); list.add("findEmployeeById"); System.out.println("findEmployeeById()"); r eturn list; }} |
---|
方法 deleteEmployeeById 通過員工 Id 來做刪除操作,這里我們只是打印一條信息而已,沒有實現(xiàn)具體過程 ; 同樣 findAllEmployees 方法也是打印一條信息。上面我們說過了,我們要做的是服務層的業(yè)務方法的保護,所以要添加清單 5 所示的內容到 applicationContext.xml,applicationContext.xml 中,大家都很熟悉吧? spring 的配置文件,這里不詳細介紹了。
清單 4. MethodSecurityInterceptor 業(yè)務方法保護
true org.ibm.acegi.beans.EmployeeManager.delete*=ROLE_SUPERVISOR,RUN_AS_SERVER org.ibm.acegi.beans.EmployeeManager.findAllEmployees = ROLE_TELLER,ROLE_SUPERVISOR,RUN_AS_SERVER |
---|
從上面我們可以看到 MethodSecurityInterceptor 攔截器暴露的 objectDefinitionSource 屬性類似與 FilterSecurityInterceptor 過濾器暴露的 objectDefinitionSource 屬性,不過前者是針對服務層的業(yè)務方法的,而后者針對的是 Web 資源,HTTP URL。我們來簡單介紹下 objectDefinitionSource 屬性的定義形式,“=”左邊的以全包路徑列出的為方法名,而“=”右邊的內容代表了此方法需要的角色集合,我們需要用逗號來間隔多個角色。
我們再來看下上面需要的 authenticationManager 屬性,內容見清單 6 所示:
清單 5. authenticationManager 內容
清單 6 的內容很關鍵,我們看到 ProviderManager 這個類,他是認證管理器的實現(xiàn),可以配置多種認證管理源來認證用戶的,通過 list 屬性我們可以很容易的明白這一點。我們再來看一下這里面提供的一種認證源 daoAuthenticationProvider,內容見清單 7 所示:
清單 6. daoAuthenticationProvider 內容
userDetailsService 屬性也是一個很關鍵的,詳細配置見清單 8 所示:
清單 7. userDetailsService 內容
它提供用戶的核心信息,包括用戶名,用戶密碼,賬戶狀態(tài)等信息,這是用戶信息的入口。
單元測試 - 保護業(yè)務方法
對于一個開發(fā)者來說,單元測試永遠是必不可少的,尤其是在 TDD 流行的今天來說,下面我們新建一個測試類命名為EmployeeManagerImplTest繼承自TestCase, 然后加入一個方法用來設置認證源命名為createSecureContext,源碼內容見清單 10 所示:
清單 8. createSecureContext 內容
private static void createSecureContext(final BeanFactory bf, final String username, final String password) { AuthenticationProvider provider = (AuthenticationProvider) bf.getBean("daoAuthenticationProvider"); Authentication auth = provider.authenticate( new UsernamePasswordAuthenticationToken(username, password)); SecurityContextHolder.getContext().setAuthentication(auth); } |
---|
這個方法提供了需要驗證的基礎信息,包括用戶名,密碼。
單元測試類的teardown方法內容如清單 11 所示:
清單 9. teardown 內容
public void teardown() { SecurityContextHolder.setContext(new SecurityContextImpl()); } |
---|
加入我們需要測試類EmployeeManagerImpl的本次需要測試的方法,代碼內容見清單 12 所示:
清單 10. EmployeeManagerImpl 的測試方法
/** * test deleteEmployeeById method. */ public void testDeleteEmployeeById() { EmployeeManager employeeManager = (EmployeeManager) factory.getBean("employeeManager"); createSecureContext(factory, "administrator", "administrator"); employeeManager.deleteEmployeeById(1011); } /** * test findAllEmployees method. */ public void testFindAllEmployees() { EmployeeManager employeeManager = (EmployeeManager) factory.getBean("employeeManager"); createSecureContext(factory, "sale_user", "sale_user"); employeeManager.findAllEmployees(); } |
---|
測試類EmployeeManagerImplTest依賴的淚如清單 13 所示:
清單 11. 測試類 EmployeeManagerImplTest 需要導入的依賴類
import junit.framework.TestCase; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.context.SecurityContextImpl; import org.acegisecurity.providers.AuthenticationProvider; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.ibm.acegi.beans.EmployeeManager; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; |
---|
至此我們的測試類全部完成了,我們可以運行該類,看一下效果,右鍵然后選擇Run as然后選擇Junit test,result見清單 14 所示:
清單 12. 測試結果
Test-begin test deleteEmployeeById() method! test findEmployeeById() method! ---sucess---- Test-end |
---|
重寫部分方法 - 了解整個工作過程
為了使大家更清楚的了解 Acegi 的工作過程,我們重寫了 AfterInvocationProvider 和 ApplicationEventPublisher 這兩個接口的方法,新建兩個類,AfterInvocationProviderImpl 實現(xiàn)了 AfterInvocationProvider 接口,ApplicationEventPublisherImpl 實現(xiàn)了 ApplicationEventPublisher 接口,代碼內容分別見清單 15 和清單 16 所示:
清單 13. AfterInvocationProviderImpl 類代碼
public class AfterInvocationProviderImpl implements AfterInvocationProvider { private static Logger logger = Logger .getLogger(AfterInvocationProviderImpl.class); @Override public boolean supports(ConfigAttribute attribute) { System.out.println("ConfigAttribute value is: " + attribute); if (attribute.getAttribute().equals("EMPLOYEESECURITY_CUSTOMER")) {return true;}return false;} @Override public boolean supports(Class clazz) { System.out.println("Security Type: " + clazz); if (clazz == MethodInvocation.class) {return true;} return false;} |
---|
可以看出我們重寫了 supports 方法,如果屬性值存在的話返回真,否則返回假。并打印一些信息。
清單 14. ApplicationEventPublisherImpl 類代碼
public class ApplicationEventPublisherImpl implements ApplicationEventPublisher { private static Logger logger = Logger.getLogger(ApplicationEventPublisherImpl.class); @Override public void publishEvent(ApplicationEvent event) { System.out.println("Security method: " + event); } } |
---|
publishEvent 方法我們加入了一些輸出信息,這樣有助于我們更好的了解 Spring Security 的工作過程,以使我們能更好的控制它。
然后我們修改下 applicationContext.xml 文件,在 bean employeeManagerSecurity 中添加屬性內容見清單 17 所示:
清單 15. applicationContext 文件修改的內容
然后再添加一個 bean 名稱為 afterInvocationManager,內容見清單 18 所示:
清單 16. afterInvocationManager bean 內容
接下來我們在運行下單元測試文件 EmployeeManagerImplTest,運行結果如清單 19 所示:
清單 17. 重構后的單元測試運行結果
Test-begin Security Type: interface org.aopalliance.intercept.MethodInvocation ConfigAttribute value is: EMPLOYEESECURITY_CUSTOMER Security method: org.acegisecurity.event.authorization .AuthorizedEvent[source=ReflectiveMethodInvocation: public abstract void org.ibm.acegi.beans.EmployeeManager.deleteEmployeeById(int); target is of class [org.ibm.acegi.beans.EmployeeManagerImpl]] test deleteEmployeeById() method! Security method: org.acegisecurity.event.authorization .AuthorizedEvent[source=ReflectiveMethodInvocation: public abstract java.util.List org.ibm.acegi.beans.EmployeeManager.findAllEmployees(); target is of class [org.ibm.acegi.beans.EmployeeManagerImpl]] test findEmployeeById() method! ---sucess---- Test-end |
---|
從結果中我們能很清晰的知道 Acegi 的加載過程,有助于我們更好的理解,使用好 Acegi。通過上面的學習我們對 Spring security(Acegi) 有了一個初步的了解,并對其是怎樣來對服務層的業(yè)務方法進行保護有了更清晰的認識,為我們接下來的知識講解鋪平了道路,下面我們就介紹下 Acegi 是怎樣為 Web Service 提供保護的,首先我們新建一個 Web Service,我們知道開發(fā) Web Service 的方法有很多種,我還是比較喜歡用 CXF 的。
開發(fā) Web Services
新建一個 Java project 工程,命名為 ws _example, 創(chuàng)建 service 類,CXFService.java 和 CXFServiceImpl.java, 代碼分別見清單 7 和清單 8 所示:
清單 18. CXFService.java
package org.ibm.cxf.service; public interface CXFService { public String sayHello(String name); } |
---|
清單 19. CXFServiceImpl.java
package org.ibm.cxf.service.impl; import org.ibm.cxf.service.CXFService; public class CXFServiceImpl implements CXFService{ public String sayHello(String name) {return "Hello "+name;}} |
---|
接下來,我們要新建一個 cxf-servlet.xml 和 web.xml 文件,文件內容分別如清單 9 和清單 10 所示:
清單 20. cxf-servlet.xml
注意一下,simple:server 后的 serviceClass 指定的是接口的名稱而 simple:serviceBean 后指定的是實現(xiàn)類的名稱,這個地方費了我好多時間呀,郁悶。
清單 21. web.xml
cxf cxf cxf cxf Apache CXF Endpoint org.apache.cxf.transport.servlet.CXFServlet 1 cxf /services/* 30 |
---|
上面代碼配置了一個 servlet,名稱為 cxf。
最后我們還是需要新建一個 ant 腳本來打包,build.xml,其腳本如清單 11 所示:
清單 22. build.xml for CXFService
運行 ant 腳本,將生成的 cxf.war 包放到 tomcat 的部署目錄下,啟動 tomcat,待啟動完成后,我們在瀏覽器中輸入 http://localhost:8080/cxf/services/CXFService?wsdl 將顯示如下信息:
出現(xiàn)如上信息表示 service 發(fā)布成功了,關于 service 的詳細信息在這個 WSDL 文件中都有定義,如出現(xiàn)問題請仔細檢查以上步驟。
下面我們測試下 service 是否可用。
單元測試
新建一個 JUnit 測試類 CXFServiceTest,代碼如清單 12 所示:
清單 23. CXFServiceTest.java
package org.ibm.axis.service.client; import junit.framework.Assert; import org.apache.cxf.frontend.ClientProxyFactoryBean; import org.ibm.cxf.service.CXFService; import org.junit.Test; public class CXFServiceTest { @Test public void testSayHello(){ ClientProxyFactoryBean factory = new ClientProxyFactoryBean(); factory.setServiceClass(CXFService.class); factory.setAddress("http://localhost:8080/cxf/services/CXFService"); CXFService client = (CXFService) factory.create(); String response = client.sayHello("jack"); Assert.assertEquals("Hello jack", response); } } |
---|
其中 Address 可以從上面的 WSDL 文件中得到。
運行 JUnit test, 結果如圖 6 所示:
圖 2. Junit service 測試結果
可見這個 Web Service 方法是可用的,對于這個 Web Service 的保護我們將采取攔截器的方式。
對于 Interceptor 更多知識請大家自行查看相關資料,下面我們新建一個攔截器 SecurityInInterceptor 繼承自 AbstractPhaseInterceptor
類,代碼見清單 26 所示:
清單 24. SecurityInInterceptor 攔截器代碼 public class SecurityInInterceptor extends AbstractPhaseInterceptor { private static Log logger = LogFactory.getLog(SecurityInInterceptor.class); private AuthenticationManager authenticationManager; public void setAuthenticationManager( AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } public SecurityInInterceptor() { super(Phase.INVOKE);} public void handleMessage(Message message) throws Fault { String baseAuth = null; Map> reqHeaders = CastUtils .cast((Map, ?>)message.get(Message.PROTOCOL_HEADERS)); if (reqHeaders != null) { for (Map.Entry> e : reqHeaders.entrySet()) { if ("Authorization".equalsIgnoreCase(e.getKey())) baseAuth = e.getValue().get(0);} } if ((baseAuth != null) && baseAuth.startsWith("Basic ")) { byte[] base64Token;String username = "";String password = ""; try { base64Token = baseAuth.substring(6).getBytes("UTF-8"); String token=new String(Base64.decodeBase64(base64Token),"UTF-8"); int delim = token.indexOf(":"); if (delim != -1) {username = token.substring(0, delim); password = token.substring(delim + 1);} Authentication authResult = authenticationManager.authenticate (new UsernamePasswordAuthenticationToken(username, password)); if(logger.isDebugEnabled()){logger. debug("Authentication success:"+ authResult.toString());}SecurityContextHolder.getContext() .setAuthentication(authResult);} catch (AuthenticationException failed) {if (logger.isDebugEnabled()) {logger.debug("Authentication request for user '" + username+ "' failed: " + failed.toString());} SecurityContextHolder.clearContext();throw new Fault(failed); } catch (Exception e){SecurityContextHolder.getContext().setAuthentication(null); throw new Fault(e);}}}} |
---|
再建一個 SecurityOutInterceptor 攔截器,代碼見清單 26 所示:
清單 25. SecurityOutInterceptor 攔截器內容 public class SecurityOutInterceptor extends AbstractPhaseInterceptor { public SecurityOutInterceptor() { super(Phase.SEND);} public void handleMessage(Message message) throws Fault { SecurityContextHolder.clearContext();}} |
---|
然后我們修改 cxf-servlet.xml 文件,修改內容如清單 27 所示:
清單 26. 修改 cxf-servlet.xml 的內容
這樣當我們訪問Web Service時候將會被這個攔截器攔截,我們看下authenticationManager這個屬性很熟悉吧?對,我們上面講解服務層的業(yè)務方法保護那部分提到了,最后根據(jù)applicationContext.xml文件做相應的修改后,我們就可以使用spring security(Acegi)為我們開發(fā)的Web Service方法提供保護了,以上對業(yè)務方法保護的過程我們可以用圖 7 來簡單概括下。
圖 3. 業(yè)務保護方法時序圖 mywang2011-03-22T11:17:00 圖片順序不對
總結
本文首先通過 Spring Security(Acegi) 自帶的兩個例子帶領大家認識一下 Spring Security(Acegi) 的面貌,然后通過一個具體實例來講解 Spring Security(Acegi) 是怎樣來保護服務層的業(yè)務方法的,然后用 CXF 開發(fā)了一個 Web Service 并實現(xiàn)了 Spring Security(Acegi) 對它的保護。每一個實例都是通過從新建工程開始一步一步的帶領大家來繼續(xù)的,我們知道僅僅通過一片文章來很詳細的將 Spring Security(Acegi) 保護 Web Services 的方方面面都闡述的很清楚,那是不可能的。
本文提供了最基本,最基礎的開發(fā)過程,任何復雜的事務歸根結底還是源于基礎,有句話是這樣說的,“授之以魚,不如授之以漁”,我想只要方向對了,知道如何下手了,就不會有大的失誤,
文章出處: IBM developerWorks