使用 Spring Security 保護 Web 應用的安全

在 Web 應用開發中,安全一直是非常重要的一個方面。安全雖然屬於應用的非功能性需求,但是應該在應用開發的初期就考慮進來。如果在應用開發的後期才考慮安全的問 題,就可能陷入一個兩難的境地:一方面,應用存在嚴重的安全漏洞,無法滿足用戶的要求,並可能造成用戶的隱私數據被攻擊者竊取;另一方面,應用的基本架構 已經確定,要修復安全漏洞,可能需要對系統的架構做出比較重大的調整,因而需要更多的開發時間,影響應用的發佈進程。因此,從應用開發的第一天就應該把安 全相關的因素考慮進來,並在整個應用的開發過程中。

本文詳細介紹瞭如何使用 Spring Security 來保護 Web 應用的安全。Spring Security 本身以及 Spring 框架帶來的靈活性,能夠滿足一般 Web 應用開發的典型需求,並允許開發人員進行定製。下面首先簡單介紹 Spring Security。

Spring Security 簡介

Spring 是一個非常流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。一般來說,Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分。用戶認證指的是驗證某個用戶是否爲系統中 的合法主體,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼。系統通過校驗用戶名和密碼來完成認證過程。用戶授權指的是驗證某個用戶 是否有權限執行某個操作。在一個系統中,不同用戶所具有的權限是不同的。比如對一個文件來說,有的用戶只能進行讀取,而有的用戶可以進行修改。一般來說, 系統會爲不同的用戶分配不同的角色,而每個角色則對應一系列的權限。

對於上面提到的兩種應用情景,Spring Security 框架都有很好的支持。在用戶認證方面,Spring Security 框架支持主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。在用戶授權方面,Spring Security 提供了基於角色的訪問控制和訪問控制列表(Access Control List,ACL),可以對應用中的領域對象進行細粒度的控制。

本文將通過三個具體的示例來介紹 Spring Security 的使用。第一個示例是一個簡單的企業員工管理系統。該系統中存在三類用戶,分別是普通員工、經理和總裁。不同類別的用戶所能訪問的資源不同。對這些資源所 能執行的操作也不相同。Spring Security 能幫助開發人員以簡單的方式滿足這些安全性相關的需求。第二個示例展示瞭如何與 LDAP 服務器進行集成。第三個示例展示瞭如何與 OAuth 進行集成。下面首先介紹基本的用戶認證和授權的實現。

基本用戶認證和授權

本節從最基本的用戶認證和授權開始對 Spring Security 進行介紹。一般來說,Web 應用都需要保存自己系統中的用戶信息。這些信息一般保存在數據庫中。用戶可以註冊自己的賬號,或是由系統管理員統一進行分配。這些用戶一般都有自己的角 色,如普通用戶和管理員之類的。某些頁面只有特定角色的用戶可以訪問,比如只有管理員纔可以訪問/admin 這樣的網址。下面介紹如何使用 Spring Security 來滿足這樣基本的認證和授權的需求。

首先需要把 Spring Security 引入到 Web 應用中來,這是通過在 web.xml添加一個新的過濾器來實現的,如 代碼清單 1 所示。


清單 1. 在 web.xml 中添加 Spring Security 的過濾器

				
 <filter> 
    <filter-name>springSecurityFilterChain</filter-name> 
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
 </filter> 

 <filter-mapping> 
    <filter-name>springSecurityFilterChain</filter-name> 
    <url-pattern>/*</url-pattern> 
 </filter-mapping>

 

Spring Security 使用的是 Servlet 規範中標準的過濾器機制。對於特定的請求,Spring Security 的過濾器會檢查該請求是否通過認證,以及當前用戶是否有足夠的權限來訪問此資源。對於非法的請求,過濾器會跳轉到指定頁面讓用戶進行認證,或是返回出錯信 息。需要注意的是,代碼清單 1 中雖然只定義了一個過濾器,Spring Security 實際上是使用多個過濾器形成的鏈條來工作的。

下一步是配置 Spring Security 來聲明系統中的合法用戶及其對應的權限。用戶相關的信息是通過 org.springframework.security.core.userdetails.UserDetailsService 接口來加載的。該接口的唯一方法是loadUserByUsername(String username),用來根據用戶名加載相關的信息。這個方法的返回值是 org.springframework.security.core.userdetails.UserDetails 接口,其中包含了用戶的信息,包括用戶名、密碼、權限、是否啓用、是否被鎖定、是否過期等。其中最重要的是用戶權限,由org.springframework.security.core.GrantedAuthority 接口來表示。雖然 Spring Security 內部的設計和實現比較複雜,但是一般情況下,開發人員只需要使用它默認提供的實現就可以滿足絕大多數情況下的需求,而且只需要簡單的配置聲明即可。

在第一個示例應用中,使用的是數據庫的方式來存儲用戶的信息。Spring Security 提供了 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 類來支持從數據庫中加載用戶信息。開發人員只需要使用與該類兼容的數據庫表結構,就可以不需要任何改動,而直接使用該類。代碼清單 2 中給出了相關的配置。


清單 2. 聲明使用數據庫來保存用戶信息

				
 <bean id="dataSource" 
    class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 
    <property name="driverClassName" value="org.apache.derby.jdbc.ClientDriver" /> 
    <property name="url" value="jdbc:derby://localhost:1527/mycompany" /> 
    <property name="username" value="app" /> 
    <property name="password" value="admin" /> 
 </bean> 

 <bean id="userDetailsService" 
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> 
    <property name="dataSource" ref="dataSource" /> 
 </bean> 

 <sec:authentication-manager> 
    <sec:authentication-provider user-service-ref="userDetailsService" /> 
 </sec:authentication-manager>

 

代碼清單 2 所示,首先定義了一個使用 Apache Derby 數據庫的數據源,Spring Security 的 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 類使用該數據源來加載用戶信息。最後需要配置認證管理器使用該UserDetailsService

接着就可以配置用戶對不同資源的訪問權限了。這裏的資源指的是 URL 地址。配置的內容如 代碼清單 3 所示。sec 是 Spring Security 的配置元素所在的名稱空間的前綴。


清單 3. 配置對不同 URL 模式的訪問權限

 <sec:http> 
    <sec:intercept-url pattern="/president_portal.do**" access="ROLE_PRESIDENT" /> 
    <sec:intercept-url pattern="/manager_portal.do**" access="ROLE_MANAGER" /> 
    <sec:intercept-url pattern="/**" access="ROLE_USER" /> 
    <sec:form-login /> 
    <sec:logout /> 
 </sec:http>

 

第一個示例應用中一共定義了三種角色:普通用戶、經理和總裁,分別用 ROLE_USERROLE_MANAGERROLE_PRESIDENT 來表示。代碼清單 3 中定義了訪問不同的 URL 模式的用戶所需要的角色。這是通過<sec:intercept-url> 元素來實現的,其屬性 pattern 聲明瞭請求 URL 的模式,而屬性access 則聲明瞭訪問此 URL 時所需要的權限。需要按照 URL 模式從精確到模糊的順序來進行聲明。因爲 Spring Security 是按照聲明的順序逐個進行比對的,只要用戶當前訪問的 URL 符合某個 URL 模式聲明的權限要求,該請求就會被允許。如果把代碼清單 3 中本來在最後的 URL 模式 /** 聲明放在最前面,那麼當普通用戶訪問 /manager_portal.do 的時候,該請求也會被允許。這顯然是不對的。通過<sec:form-login> 元素聲明瞭使用 HTTP 表單驗證。也就是說,當未認證的用戶試圖訪問某個受限 URL 的時候,瀏覽器會跳轉到一個登錄頁面,要求用戶輸入用戶名和密碼。<sec:logout> 元素聲明瞭提供用戶註銷登錄的功能。默認的註銷登錄的 URL 是/j_spring_security_logout,可以通過屬性 logout-url 來修改。

當完成這些配置並運行應用之後,會發現 Spring Security 已經默認提供了一個登錄頁面的實現,可以直接使用。開發人員也可以對登錄頁面進行定製。通過<form-login> 的屬性 login-pagelogin-processing-urlauthentication-failure-url就可以定製登錄頁面的 URL、登錄請求的處理 URL 和登錄出現錯誤時的 URL 等。從這裏可以看出,一方面 Spring Security 對開發中經常會用到的功能提供了很好的默認實現,另外一方面也提供了非常靈活的定製能力,允許開發人員提供自己的實現。

在介紹如何用 Spring Security 實現基本的用戶認證和授權之後,下面介紹其中的核心對象。

SecurityContext 和 Authentication 對象

下面開始討論幾個 Spring Security 裏面的核心對象。org.springframework.security.core.context.SecurityContext接口表示的是當前應用的安全上下文。通過此接口可以獲取和設置當前的認證對象。org.springframework.security.core.Authentication接口用來表示此認證對象。通過認證對象的方法可以判斷當前用戶是否已經通過認證,以及獲取當前認證用戶的相關信息,包括用戶名、密碼和權限等。要使用此認證對象,首先需要獲取到SecurityContext 對象。通過 org.springframework.security.core.context.SecurityContextHolder 類提供的靜態方法getContext() 就可以獲取。再通過 SecurityContext對象的 getAuthentication()就可以得到認證對象。通過認證對象的getPrincipal() 方法就可以獲得當前的認證主體,通常是 UserDetails 接口的實現。聯繫到上一節介紹的UserDetailsService,典型的認證過程就是當用戶輸入了用戶名和密碼之後,UserDetailsService通過用戶名找到對應的UserDetails 對象,接着比較密碼是否匹配。如果不匹配,則返回出錯信息;如果匹配的話,說明用戶認證成功,就創建一個實現了 Authentication接口的對象,如 org.springframework.security. authentication.UsernamePasswordAuthenticationToken 類的對象。再通過SecurityContextsetAuthentication() 方法來設置此認證對象。

代碼清單 4 給出了使用SecurityContextAuthentication的一個示例,用來獲取當前認證用戶的用戶名。


清單 4. 獲取當前認證用戶的用戶名

				
 public static String getAuthenticatedUsername() { 
    String username = null; 
    Object principal = SecurityContextHolder.getContext() 
        .getAuthentication().getPrincipal(); 
    if (principal instanceof UserDetails) { 
        username = ((UserDetails) principal).getUsername(); 
    } else { 
        username = principal.toString(); 
    } 
    return username; 
 }

 

默認情況下,SecurityContextHolder使用 ThreadLocal來保存 SecurityContext對象。因此,SecurityContext對象對於當前線程上所有方法都是可見的。這種實現對於 Web 應用來說是合適的。不過在有些情況下,如桌面應用,這種實現方式就不適用了。Spring Security 允許開發人員對此進行定製。開發人員只需要實現接口org.springframework.security.core.context.SecurityContextHolderStrategy並通過SecurityContextHoldersetStrategyName(String)方法讓 Spring Security 使用此實現即可。另外一種設置方式是使用系統屬性。除此之外,Spring Security 默認提供了另外兩種實現方式:MODE_GLOBAL表示當前應用共享唯一的SecurityContextHolderMODE_INHERITABLETHREADLOCAL表示子線程繼承父線程的SecurityContextHolder代碼清單 5給出了使用全局唯一的SecurityContextHolder的示例。


清單 5. 使用全局唯一的 SecurityContextHolder

				
 public void useGlobalSecurityContextHolder() { 
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL); 
 } 

 

在介紹完 Spring Security 中的 SecurityContextAuthentication之後,下面介紹如何保護服務層的方法。

服務層方法保護

之前章節中介紹的是在 URL 這個粒度上的安全保護。這種粒度的保護在很多情況下是不夠的。比如相同的 URL 對應的頁面上,不同角色的用戶所能看到的內容和執行的操作是有可能不同的。在第一個示例應用中,系統中記錄了每個員工的工資收入。所有員工都可以查看自己 的工資,但是隻有員工的直接經理纔可以修改員工的工資。這就涉及到對應用中服務層的方法進行相應的權限控制,從而避免安全漏洞。

保護服務層方法涉及到對應用中的方法調用進行攔截。通過 Spring 框架提供的良好面向方面編程(AOP)的支持,可以很容易的對方法調用進行攔截。Spring Security 利用了 AOP 的能力,允許以聲明的方式來定義調用方式時所需的權限。代碼清單 6中給出了對方法調用進行保護的配置文件示例。


清單 6. 對方法調用進行保護

				
 <bean id="userSalarySecurity" 
    class="org.springframework.security.access.intercept.aspectj. 
        AspectJMethodSecurityInterceptor"> 
    <property name="authenticationManager" ref="authenticationManager" /> 
    <property name="accessDecisionManager" ref="accessDecisionManager" /> 
    <property name="securityMetadataSource"> 
        <value> 
            mycompany.service.UserService.raiseSalary=ROLE_MANAGER 
        </value> 
    </property> 
 </bean>

 

代碼清單 6所示,通過 mycompany.service.UserService.raiseSalary=ROLE_MANAGER聲明瞭mycompany.service.UserService類的 raiseSalary方法只有具有角色 ROLE_MANAGER的用戶才能執行。這就使得只具有角色 ROLE_USER的用戶無法調用此方法。

不過僅對方法名稱進行權限控制並不能解決另外的一些問題。比如在第一個示例應用中的增加工資的實現是通過發送 HTTP POST 請求到 salary.do這個 URL 來完成的。salary.do對應的控制器mycompany.controller.SalaryController會調用 mycompany.service.UserService類的raiseSalary方法來完成增加工資的操作。存在的一種安全漏洞是具有 ROLE_MANAGER角色的用戶可以通過其它工具(如 cURL 或 Firefox 擴展 Poster 等)來創建 HTTP POST 請求來更改其它員工的工資。爲了解決這個問題,需要對raiseSalary的調用進行更加細粒度的控制。通過 Spring Security 提供的 AspectJ 支持就可以編寫相關的控制邏輯,如代碼清單 7所示。


清單 7. 使用 AspectJ 進行細粒度的控制

				
 public aspect SalaryManagementAspect { 
    private AspectJMethodSecurityInterceptor securityInterceptor; 

    private UserDao userDao; 

    pointcut salaryChange(): target(UserService) 
        && execution(public void raiseSalary(..)) &&!within(SalaryManagementAspect); 

    Object around(): salaryChange() { 
        if (this.securityInterceptor == null) { 
            return proceed(); 
        } 
        AspectJCallback callback = new AspectJCallback() { 
            public Object proceedWithObject() { 
                return proceed(); 
            } 
        }; 
        Object[] args = thisJoinPoint.getArgs(); 
        String employee = (String) args[0]; // 要修改的員工的用戶名
        User user = userDao.getByUsername(employee); 
        String currentUser = UsernameHolder.getAuthenticatedUsername(); // 當前登錄用戶
        if (!currentUser.equals(user.getManagerId())) { 
            throw new AccessDeniedException 
                ("Only the direct manager can change the salary."); 
        } 

        return this.securityInterceptor.invoke(thisJoinPoint, callback); 
    } 
 }

 

代碼清單 7所示,定義了一個切入點(pointcut)salaryChange和對應的環繞增強。當方法 raiseSalary被調用的時候,會比較要修改的員工的經理的用戶名和當前登錄用戶的用戶名是否一致。當不一致的時候就會拋出AccessDeniedException異常。

在介紹瞭如何保護方法調用之後,下面介紹如何通過訪問控制列表來保護領域對象。

訪問控制列表

之前提到的安全保護和權限控制都是隻針對 URL 或是方法調用,只對一類對象起作用。而在有些情況下,不同領域對象實體所要求的權限控制是不同的。以第一類示例應用來說,系統中有報表這一類實體。由於報表的特殊性,只有具有角色ROLE_PRESIDENT的 用戶纔可以創建報表。對於每份報表,創建者可以設定其對於不同用戶的權限。比如有的報表只允許特定的幾個用戶可以查看。對於這樣的需求,就需要對每個領域 對象的實例設置對應的訪問控制權限。Spring Security 提供了對訪問控制列表(Access Control List,ACL)的支持,可以很方便的對不同的領域對象設置針對不同用戶的權限。

Spring Security 中的訪問控制列表的實現中有 3 個重要的概念,對應於 4 張數據庫表。

  • 授權的主體:一般是系統中的用戶。由 ACL_SID表來表示。
  • 領域對象:表示系統中需要進行訪問控制的實體。由 ACL_CLASSACL_OBJECT_IDENTITY表來表示,前者保存的是實體所對應的 Java 類的名稱,而後者保存的是實體本身。
  • 訪問權限:表示一個用戶對一個領域對象所具有的權限。由表 ACL_ENTRY來表示。

Spring Security 已經提供了參考的數據庫表模式和相應的基於 JDBC 的實現。在大多數情況下,使用參考實現就可以滿足需求了。類 org.springframework.security.acls.jdbc.JdbcMutableAclService可以對訪問控制列表進行查詢、添加、更新和刪除的操作,是開發人員最常直接使用的類。該類的構造方法需要 3 個參數,分別是javax.sql.DataSource表示的數據源、org.springframework.security.acls.jdbc.LookupStrategy表示的數據庫的查詢策略和org.springframework.security.acls.model.AclCache表示的訪問控制列表緩存。數據源可以使用第一個示例應用中已有的數據源。查詢策略可以使用默認的實現org.springframework.security.acls.jdbc.BasicLookupStrategy。緩存可以使用基於 EhCache 的緩存實現org.springframework.security.acls.domain.EhCacheBasedAclCache代碼清單 8中給出了相關代碼。


清單 8. 使用 JDBC 的訪問控制列表服務基本配置

				
 <bean id="aclService"
    class="org.springframework.security.acls.jdbc.JdbcMutableAclService"> 
    <constructor-arg ref="dataSource" /> 
    <constructor-arg ref="lookupStrategy" /> 
    <constructor-arg ref="aclCache" /> 
    <property name="classIdentityQuery" value="values IDENTITY_VAL_LOCAL()"/> 
    <property name="sidIdentityQuery" value="values IDENTITY_VAL_LOCAL()"/> 	
 </bean>

 

代碼清單 8所示,需要注意的是 org.springframework.security.acls.jdbc.JdbcMutableAclService的屬性classIdentityQuerysidIdentityQuery。Spring Security 的默認數據庫模式使用了自動增長的列作爲主鍵。而在實現中,需要能夠獲取到新插入的列的 ID。因此需要與數據庫實現相關的 SQL 查詢語言來獲取到這個 ID。Spring Security 默認使用的 HSQLDB,因此這兩個屬性的默認值是 HSQLDB 支持的call identity()。如果使用的數據庫不是 HSQLDB 的話,則需要根據數據庫實現來設置這兩個屬性的值。第一個示例應用使用的是 Apache Derby 數據庫,因此這兩個屬性的值是values IDENTITY_VAL_LOCAL()。對於 MySQL 來說,這個值是 select @@identity代碼清單 9給出了使用org.springframework.security.acls.jdbc.JdbcMutableAclService來管理訪問控制列表的 Java 代碼。


清單 9. 使用訪問控制列表服務

				
 public void createNewReport(String title, String content) throws ServiceException { 
    final Report report = new Report(); 
    report.setTitle(title); 
    report.setContent(content); 
		
    transactionTemplate.execute(new TransactionCallback<Object>() { 
        public Object doInTransaction(TransactionStatus status) { 
            reportDao.create(report); 
            addPermission(report.getId(), new PrincipalSid(getUsername()), 
                BasePermission.ADMINISTRATION); 
            return null; 
        } 
    }); 
 } 
	
 public void grantRead(final String username, final Long reportId) { 
    transactionTemplate.execute(new TransactionCallback<Object>() { 
        public Object doInTransaction(TransactionStatus status) { 
            addPermission(reportId, new PrincipalSid(username), BasePermission.READ); 
            return null; 
        } 
    }); 
 } 

 private void addPermission(Long reportId, Sid recipient, Permission permission) { 
    MutableAcl acl; 
    ObjectIdentity oid = new ObjectIdentityImpl(Report.class, reportId); 

    try { 
        acl = (MutableAcl) mutableAclService.readAclById(oid); 
    } catch (NotFoundException nfe) { 
        acl = mutableAclService.createAcl(oid); 
    } 

    acl.insertAce(acl.getEntries().size(), permission, recipient, true); 
    mutableAclService.updateAcl(acl); 
 }

 

代碼清單 9中的addPermission(Long reportId, Sid recipient, Permission permission)方法用來爲某個報表添加訪問控制權限,參數reportId表示的是報表的 ID,用來標識一個報表;recipient表示的是需要授權的用戶;permission表示的是授予的權限。createNewReport()方法用來創建一個報表,同時給創建報表的用戶授予管理權限(BasePermission.ADMINISTRATION)。grantRead()方法用來給某個用戶對某個報表授予讀權限(BasePermission.READ)。這裏需要注意的是,對訪問控制列表的操作都需要在一個事務中進行處理。利用 Spring 提供的事務模板(org.springframework.transaction.support.TransactionTemplate)就可以很好的處理事務。對於權限,Spring Security 提供了 4 種基本的權限:讀、寫、刪除和管理。開發人員可以在這基礎上定義自己的權限。

在介紹完訪問控制列表之後,下面介紹 Spring Security 提供的 JSP 標籤庫。

JSP 標籤庫

之前的章節中介紹了在 Java 代碼中如何使用 Spring Security 提供的能力。很多情況下,用戶可能有權限訪問某個頁面,但是頁面上的某些功能對他來說是不可用的。比如對於同樣的員工列表,普通用戶只能查看數據,而具有 經理角色的用戶則可以看到對列表進行修改的鏈接或是按鈕等。Spring Security 提供了一個 JSP 標籤庫用來方便在 JSP 頁面中根據用戶的權限來控制頁面某些部分的顯示和隱藏。使用這個 JSP 標籤庫很簡單,只需要在 JSP 頁面上添加聲明即可:<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>。這個標籤庫包含如下 3 個標籤:

  • authorize標籤:該標籤用來判斷其中包含的內容是否應該被顯示出來。判斷的條件可以是某個表達式的求值結果,或是是否能訪問某個 URL,分別通過屬性accessurl來指定。如 <sec:authorize access="hasRole('ROLE_MANAGER')">限定內容只有具有經理角色的用戶纔可見。<sec:authorize url="/manager_portal.do">限定內容只有能訪問 URL/manager_portal.do的用戶纔可見。
  • authentication標籤:該標籤用來獲取當前認證對象(Authentication)中的內容。如<sec:authentication property="principal.username" />可以用來獲取當前認證用戶的用戶名。
  • accesscontrollist標籤:該標籤的作用與 authorize標籤類似,也是判斷其中包含的內容是否應該被顯示出來。所不同的是它是基於訪問控制列表來做判斷的。該標籤的屬性domainObject表示的是領域對象,而屬性 hasPermission表示的是要檢查的權限。如 <sec:accesscontrollist hasPermission="READ" domainObject="myReport">限定了其中包含的內容只在對領域對象myReport有讀權限的時候纔可見。

值得注意的是,在使用 authorize標籤的時候,需要通過 <sec:http use-expressions="true">來啓用表達式的支持。查看權限控制表達式一節瞭解關於表達式的更多內容。

在介紹完 JSP 標籤庫之後,下面介紹如何與 LDAP 進行集成。

使用 LDAP

很多公司都使用 LDAP 服務器來保存員工的相關信息。內部的 IT 系統都需要與 LDAP 服務器做集成來進行用戶認證與訪問授權。Spring Security 提供了對 LDAP 協議的支持,只需要簡單的配置就可以讓 Web 應用使用 LDAP 來進行認證。第二個示例應用使用 OpenDS LDAP 服務器並添加了一些測試用戶。代碼清單 10中給出了配置文件的示例,完整的代碼見 參考資料


清單 10. 集成 LDAP 服務器的配置文件

				
 <bean id="contextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource"> 
    <constructor-arg value="ldap://localhost:389" /> 
 </bean> 
   
 <bean id="ldapAuthProvider"
    class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider"> 
    <constructor-arg> 
        <bean class="org.springframework.security.ldap.authentication.BindAuthenticator"> 
            <constructor-arg ref="contextSource" /> 
            <property name="userSearch"> 
                <bean id="userSearch" 
        class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch"> 
                    <constructor-arg index="0" value="ou=People,dc=mycompany,dc=com" /> 
                    <constructor-arg index="1" 
                value="(&amp;(uid={0})(objectclass=person))" /> 
                    <constructor-arg index="2" ref="contextSource" /> 
                </bean> 
            </property> 
        </bean> 
    </constructor-arg> 
    <constructor-arg> 
        <bean class="mycompany.CompanyAuthoritiesPopulator"></bean> 
    </constructor-arg> 
 </bean> 

 <sec:authentication-manager> 
    <sec:authentication-provider ref="ldapAuthProvider" /> 
 </sec:authentication-manager>

 

代碼清單 10所示,配置中的核心部分是類 org.springframework.security.ldap.authentication.LdapAuthenticationProvider,它用來與 LDAP 服務器進行認證以及獲取用戶的權限信息。一般來說,與 LDAP 服務器進行認證的方式有兩種。一種是使用用戶提供的用戶名和密碼直接綁定到 LDAP 服務器;另外一種是比較用戶提供的密碼與 LDAP 服務器上保存的密碼是否一致。前者通過類org.springframework.security.ldap.authentication.BindAuthenticator來實現,而後者通過類org.springframework.security. ldap.authentication.PasswordComparisonAuthenticator來實現。第二個示例應用中使用的是綁定的方式來進行認證。在進行綁定的時候,需要在 LDAP 服務器上搜索當前的用戶。搜索的時候需要指定基本的識別名(Distinguished Name)和過濾條件。在該應用中,用戶登錄時使用的是其唯一識別符(uid),如user.0,而在 LDAP 服務器上對應的識別名是 uid=user.0,ou=People,dc=mycompany,dc=com。通過使用過濾條件(&amp;(uid={0})(objectclass=person))就可以根據 uid來搜索到用戶並進行綁定。當認證成功之後,就需要獲取到該用戶對應的權限。一般是通過該用戶在 LDAP 服務器上所在的分組來確定的。不過在示例應用中展示瞭如何提供自己的實現來爲用戶分配權限。類mycompany.CompanyAuthoritiesPopulator實現了 org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator接口,併爲所有的用戶分配了單一的角色ROLE_USER

在介紹完與 LDAP 進行集成之後,下面介紹如何與 OAuth 進行集成。

OAuth 集成

現在的很多 Web 服務都提供 API 接口,允許第三方應用使用其數據。當第三方應用需要訪問用戶私有數據的時候,需要進行認證。OAuth 是目前流行的一種認證方式,被很多 Web 服務採用,包括 Twitter、LinkedIn、Google Buzz 和新浪微博等。OAuth 的特點是第三方應用不能直接獲取到用戶的密碼,而只是使用一個經過用戶授權之後的令牌(token)來進行訪問。用戶可以對能夠訪問其數據的第三方應用進 行管理,通過回收令牌的方式來終止第三方應用對其數據的訪問。OAuth 的工作方式涉及到服務提供者、第三方應用和用戶等 3 個主體。其基本的工作流程是:第三方應用向服務提供者發出訪問用戶數據的請求。服務提供者會詢問用戶是否同意此請求。如果用戶同意的話,服務提供者會返回 給第三方應用一個令牌。第三方應用只需要在請求數據的時候帶上此令牌就可以成功獲取。

第三方應用在使用 OAuth 認證方式的時候,其中所涉及的交互比較複雜。Spring Security 本身並沒有提供 OAuth 的支持,通過另外一個開源庫 OAuth for Spring Security 可以實現。OAuth for Spring Security 與 Spring Security 有着很好的集成,可以很容易在已有的使用 Spring Security 的應用中添加 OAuth 的支持。不過目前 OAuth for Spring Security 只對 Spring Security 2.0.x 版本提供比較好的支持。對 OAuth 的支持包括服務提供者和服務消費者兩個部分:服務提供者是數據的提供者,服務消費者是使用這些數據的第三方應用。一般的應用都是服務消費者。OAuth for Spring Security 對服務提供者和消費者都提供了支持。下面通過獲取 LinkedIn 上的狀態更新的示例來說明其用法。

作爲 OAuth 的服務消費者,需要向服務提供者申請表示其應用的密鑰。服務提供者會提供 3 個 URL 來與服務消費者進行交互。代碼清單 11中給出了使用 OAuth for Spring Security 的配置文件。


清單 11. 使用 OAuth for Spring Security 的配置文件

				
 <oauth:consumer resource-details-service-ref="linkedInResourceDetails"
    oauth-failure-page="/oauth_error.jsp"> 
    <oauth:url pattern="/linkedin.do**" resources="linkedIn" /> 
 </oauth:consumer> 

 <bean id="oauthConsumerSupport"
    class="org.springframework.security.oauth.consumer.CoreOAuthConsumerSupport"> 
    <property name="protectedResourceDetailsService" ref="linkedInResourceDetails" /> 
 </bean> 

 <oauth:resource-details-service id="linkedInResourceDetails"> 
    <oauth:resource id="linkedIn"
        key="***" secret="***"
        request-token-url="https://api.linkedin.com/uas/oauth/requestToken"
        user-authorization-url="https://www.linkedin.com/uas/oauth/authorize"
        access-token-url="https://api.linkedin.com/uas/oauth/accessToken" /> 
 </oauth:resource-details-service>

 

代碼清單 11所示,只需要通過對 <oauth:resource>元素進行簡單的配置,就可以聲明使用 LinkedIn 的服務。每個<oauth:resource>元素對應一個 OAuth 服務資源。該元素的屬性包含了與該服務資源相關的信息。OAuth for Spring Security 在 Spring Security 提供的過濾器的基礎上,額外增加了處理 OAuth 認證的過濾器實現。通過<oauth:consumer>的子元素 <oauth:url>可以定義過濾器起作用的 URL 模式和對應的 OAuth 服務資源。當用戶訪問指定的 URL 的時候,應用會轉到服務提供者的頁面,要求用戶進行授權。當用戶授權之後,應用就可以訪問其數據。訪問數據的時候,需要在 HTTP 請求中添加額外的Authorization頭。代碼清單 12給出了訪問數據時使用的代碼。


清單 12. 獲取訪問令牌和構建 HTTP 請求

				
 public OAuthConsumerToken getAccessTokenFromRequest(HttpServletRequest request) { 
    OAuthConsumerToken token = null; 

    List<OAuthConsumerToken> tokens = (List<OAuthConsumerToken>) request 
        .getAttribute(OAuthConsumerProcessingFilter.ACCESS_TOKENS_DEFAULT_ATTRIBUTE); 
    if (tokens != null) { 
        for (OAuthConsumerToken consumerToken : tokens) { 
            if (consumerToken.getResourceId().equals(resourceId)) { 
                token = consumerToken; 
                break; 
            } 
        } 
    } 
    return token; 
 } 

 public GetMethod getGetMethod(OAuthConsumerToken accessToken, URL url) { 
    GetMethod method = new GetMethod(url.toString()); 
    method.setRequestHeader("Authorization", 
				 getHeader(accessToken, url, "GET")); 
    return method; 
 } 

 public String getHeader(OAuthConsumerToken accessToken, URL url, 
			 String method) { 
    ProtectedResourceDetails details = support 
        .getProtectedResourceDetailsService() 
        .loadProtectedResourceDetailsById(accessToken.getResourceId()); 
    return support.getAuthorizationHeader(details, accessToken, url, method, null); 
 }

 

代碼清單 12所示,OAuth for Spring Security 的過濾器會把 OAuth 認證成功之後的令牌保存在當前的請求中。通過 getAccessTokenFromRequest()方法就可以從請求中獲取到此令牌。有了這個令牌之後,就可以通過 getHeader()方法構建出 HTTP 請求所需的Authorization頭。只需要在請求中添加此 HTTP 頭,就可以正常訪問到所需的數據。默認情況下,應用的 OAuth 令牌是保存在 HTTP 會話中的,開發人員可以提供其它的令牌保存方式,如保存在數據庫中。只需要提供org.springframework.security.oauth.consumer.token.OAuthConsumerTokenServices接口的實現就可以了。

在介紹完與 OAuth 的集成方式之後,下面介紹一些高級話題。

高級話題

這些與 Spring Security 相關的高級話題包括權限控制表達式、會話管理和記住用戶等。

權限控制表達式

有些情況下,對於某種資源的訪問條件可能比較複雜,並不只是簡單的要求當前用戶具有某一個角色即可,而是由多種條件進行組合。權限控制表達式允許使用一種簡單的語法來描述比較複雜的授權條件。Spring Security 內置了一些常用的表達式,包括hasRole()用來判斷當前用戶是否具有某個角色,hasAnyRole()用來判斷當前用戶是否具備列表中的某個角色,以及hasPermission()用來判斷當前用戶是否具備對某個領域對象的某些權限等。這些基本表達式可以通過 andor等組合起來,表示複雜的語義。當通過 <sec:http use-expressions="true">啓用了表達式支持之後,就可以在<sec:intercept-url>元素的 access屬性上使用表達式。

表達式還可以用來對方法調用進行權限控制,主要是用在方法註解中。要啓用 Spring Security 提供的方法註解,需要添加元素 <global-method-security pre-post-annotations="enabled"/>。這幾個方法註解分別是:

  • @PreAuthorize:該註解用來確定一個方法是否應該被執行。該註解後面跟着的是一個表達式,如果表達式的值爲真,則該方法會被執行。如@PreAuthorize("hasRole('ROLE_USER')")就說明只有當前用戶具有角色 ROLE_USER的時候纔會執行。
  • @PostAuthorize:該註解用來在方法執行完之後進行訪問控制檢查。
  • @PostFilter:該註解用來對方法的返回結果進行過濾。從返回的集合中過濾掉表達式值爲假的元素。如 @PostFilter("hasPermission(filterObject, 'read')")說明返回的結果中只保留當前用戶有讀權限的元素。
  • @PreFilter:該註解用來對方法調用時的參數進行過濾。

會話管理

Spring Security 提供了對 HTTP 會話的管理功能。這些功能包括對會話超時的管理、防範會話設置攻擊(Session fixation attack)和併發會話管理等。

如果當前用戶的會話因爲超時而失效之後,如果用戶繼續使用此會話來訪問,Spring Security 可以檢測到這種情況,並跳轉到適當的頁面。只需要在 <sec:http>元素下添加 <sec:session-management invalid-session-url="/sessionTimeout.jsp" />元素即可,屬性invalid-session-url指明瞭會話超時之後跳轉到的 URL 地址。

有些 Web 應用會把用戶的會話標識符直接通過 URL 的參數來傳遞,並且在服務器端不進行驗證,如用戶訪問的 URL 可能是 /myurl;jsessionid=xxx。 攻擊者可以用一個已知的會話標識符來構建一個 URL,並把此 URL 發給要攻擊的對象。如果被攻擊者訪問這個 URL 並用自己的用戶名登錄成功之後,攻擊者就可以利用這個已經通過認證的會話來訪問被攻擊者的數據。防範這種攻擊的辦法就是要求用戶在做任何重要操作之前都重 新認證。Spring Security 允許開發人員定製用戶登錄時對已有會話的處理,從而可以有效的防範這種攻擊。通過<sec:session-management>元素的屬性 session-fixation-protection可以修改此行爲。該屬性的可選值有migrateSessionnewSessionnonemigrateSession是默認值。在這種情況下,每次用戶登錄都會創建一個新的會話,同時把之前會話的數據複製到新會話中。newSession表示的是隻創建新的會話,而不復制數據。none表示的是保持之前的會話。

在有些情況下,應用需要限定使用同一個用戶名同時進行登錄所產生的會話數目。比如有些應用可能要求每個用戶在同一時間最多只能有一個會話。可以通過 <sec:session-management>元素的子元素<sec:concurrency-control>來限制每個用戶的併發會話個數。如 <sec:concurrency-control max-sessions="2" />就限定了每個用戶在同一時間最多只能有兩個會話。如果當前用戶的會話數目已經達到上限,而用戶又再次登錄的話,默認的實現是使之前的會話失效。如果希望阻止後面的這次登錄的話,可以設置屬性error-if-maximum-exceeded的值爲 true。這樣的話,後面的這次登錄就會出錯。只有當之前的會話失效之後,用戶才能再次登錄。

記住用戶

有些 Web 應用會在登錄界面提供一個複選框,詢問用戶是否希望在當前計算機上記住自己的密碼。如果用戶勾選此選項的話,在一段時間內用戶訪問此應用時,不需要輸入用戶名和密碼進行登錄。Spring Security 提供了對這種記住用戶的需求的支持。只需要在<sec:http>中添加 <sec:remember-me>元素即可。

一般來說,有兩種方式可以實現記住用戶的能力。一種做法是利用瀏覽器端的 cookie。當用戶成功登錄之後,特定內容的字符串被保存到 cookie 中。下次用戶再次訪問的時候,保存在 cookie 中的內容被用來認證用戶。默認情況下使用的是這種方式。使用 cookie 的做法存在安全隱患,比如攻擊者可能竊取用戶的 cookie,並用此 cookie 來登錄系統。另外一種更安全的做法是瀏覽器端的 cookie 只保存一些隨機的數字,而且這些數字只能使用一次,在每次用戶登錄之後都會重新生成。這些數字保存在服務器端的數據庫中。如果希望使用這種方式,需要創建 一個數據庫表,並通過 data-source-ref屬性來指定包含此表的數據源。

總結

對於使用 Spring 開發的 Web 應用來說,Spring Security 是增加安全性時的最好選擇。本文詳細介紹了 Spring Security 的各個方面,包括實現基本的用戶認證和授權、保護服務層方法、使用訪問控制列表保護具體的領域對象、JSP 標籤庫和與 LDAP 和 OAuth 的集成等。通過本文,開發人員可以瞭解如何使用 Spring Security 來實現不同的用戶認證和授權機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章