Spring Security的學習
xl_echo編輯整理,歡迎轉載,轉載請宣告文章來源。更多IT、程式設計案例、資料請聯絡QQ:1280023003 百戰不敗,依不自稱常勝,百敗不頹,依能奮力前行。——這才是真正的堪稱強大!! 本文轉載自:https://blog.csdn.net/u012517198/article/details/51648074
在 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_USER、ROLE_MANAGER 和 ROLE_PRESIDENT 來表示。程式碼清單 3 中定義了訪問不同的 URL 模式的使用者所需要的角色。這是通過 元素來實現的,其屬性 pattern 聲明瞭請求 URL 的模式,而屬性 access 則聲明瞭訪問此 URL 時所需要的許可權。需要按照 URL 模式從精確到模糊的順序來進行宣告。因為 Spring Security 是按照宣告的順序逐個進行比對的,只要使用者當前訪問的 URL 符合某個 URL 模式宣告的許可權要求,該請求就會被允許。如果把 程式碼清單 3 中本來在最後的 URL 模式 /** 宣告放在最前面,那麼當普通使用者訪問 /manager_portal.do 的時候,該請求也會被允許。這顯然是不對的。通過 元素聲明瞭使用 HTTP 表單驗證。也就是說,當未認證的使用者試圖訪問某個受限 URL 的時候,瀏覽器會跳轉到一個登入頁面,要求使用者輸入使用者名稱和密碼。 元素聲明瞭提供使用者登出登入的功能。預設的登出登入的 URL 是/j_spring_security_logout,可以通過屬性 logout-url 來修改。
當完成這些配置並執行應用之後,會發現 Spring Security 已經預設提供了一個登入頁面的實現,可以直接使用。開發人員也可以對登入頁面進行定製。通過 的屬性 login-page、login-processing-url 和 authentication-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 類的物件。再通過 SecurityContext的 setAuthentication() 方法來設定此認證物件。
程式碼清單 4 給出了使用 SecurityContext 和 Authentication的一個示例,用來獲取當前認證使用者的使用者名稱。
清單 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並通過 SecurityContextHolder的setStrategyName(String)方法讓 Spring Security 使用此實現即可。另外一種設定方式是使用系統屬性。除此之外,Spring Security 預設提供了另外兩種實現方式:MODE_GLOBAL表示當前應用共享唯一的 SecurityContextHolder;MODE_INHERITABLETHREADLOCAL表示子執行緒繼承父執行緒的 SecurityContextHolder。程式碼清單 5給出了使用全域性唯一的 SecurityContextHolder的示例。
清單 5. 使用全域性唯一的 SecurityContextHolder public void useGlobalSecurityContextHolder() { SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL); } 在介紹完 Spring Security 中的 SecurityContext和 Authentication之後,下面介紹如何保護服務層的方法。
回頁首
服務層方法保護 之前章節中介紹的是在 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_CLASS和 ACL_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的屬性classIdentityQuery和 sidIdentityQuery。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,分別通過屬性 access和 url來指定。如 限定內容只有具有經理角色的使用者才可見。限定內容只有能訪問 URL/manager_portal.do的使用者才可見。 authentication標籤:該標籤用來獲取當前認證物件(Authentication)中的內容。如 可以用來獲取當前認證使用者的使用者名稱。 accesscontrollist標籤:該標籤的作用與 authorize標籤類似,也是判斷其中包含的內容是否應該被顯示出來。所不同的是它是基於訪問控制列表來做判斷的。該標籤的屬性 domainObject表示的是領域物件,而屬性 hasPermission表示的是要檢查的許可權。如限定了其中包含的內容只在對領域物件myReport有讀許可權的時候才可見。 值得注意的是,在使用 authorize標籤的時候,需要通過 來啟用表示式的支援。檢視 許可權控制表示式一節瞭解關於表示式的更多內容。
在介紹完 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="(&(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。通過使用過濾條件 (&(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所示,只需要通過對 元素進行簡單的配置,就可以宣告使用 LinkedIn 的服務。每個 元素對應一個 OAuth 服務資源。該元素的屬性包含了與該服務資源相關的資訊。OAuth for Spring Security 在 Spring Security 提供的過濾器的基礎上,額外增加了處理 OAuth 認證的過濾器實現。通過 的子元素 可以定義過濾器起作用的 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()用來判斷當前使用者是否具備對某個領域物件的某些許可權等。這些基本表示式可以通過 and和 or等組合起來,表示複雜的語義。當通過 啟用了表示式支援之後,就可以在 元素的 access屬性上使用表示式。
表示式還可以用來對方法呼叫進行許可權控制,主要是用在方法註解中。要啟用 Spring Security 提供的方法註解,需要新增元素 。這幾個方法註解分別是:
@PreAuthorize:該註解用來確定一個方法是否應該被執行。該註解後面跟著的是一個表示式,如果表示式的值為真,則該方法會被執行。如 @PreAuthorize(“hasRole(‘ROLE_USER’)”)就說明只有當前使用者具有角色 ROLE_USER的時候才會執行。 @PostAuthorize:該註解用來在方法執行完之後進行訪問控制檢查。 @PostFilter:該註解用來對方法的返回結果進行過濾。從返回的集合中過濾掉表示式值為假的元素。如@PostFilter(“hasPermission(filterObject, ‘read’)”)說明返回的結果中只保留當前使用者有讀許可權的元素。 @PreFilter:該註解用來對方法呼叫時的引數進行過濾。 會話管理 Spring Security 提供了對 HTTP 會話的管理功能。這些功能包括對會話超時的管理、防範會話設定攻擊(Session fixation attack)和併發會話管理等。
如果當前使用者的會話因為超時而失效之後,如果使用者繼續使用此會話來訪問,Spring Security 可以檢測到這種情況,並跳轉到適當的頁面。只需要在 元素下新增 元素即可,屬性invalid-session-url指明瞭會話超時之後跳轉到的 URL 地址。
有些 Web 應用會把使用者的會話識別符號直接通過 URL 的引數來傳遞,並且在伺服器端不進行驗證,如使用者訪問的 URL 可能是/myurl;jsessionid=xxx。攻擊者可以用一個已知的會話識別符號來構建一個 URL,並把此 URL 發給要攻擊的物件。如果被攻擊者訪問這個 URL 並用自己的使用者名稱登入成功之後,攻擊者就可以利用這個已經通過認證的會話來訪問被攻擊者的資料。防範這種攻擊的辦法就是要求使用者在做任何重要操作之前都重新認證。Spring Security 允許開發人員定製使用者登入時對已有會話的處理,從而可以有效的防範這種攻擊。通過元素的屬性 session-fixation-protection可以修改此行為。該屬性的可選值有migrateSession、newSession和 none。migrateSession是預設值。在這種情況下,每次使用者登入都會建立一個新的會話,同時把之前會話的資料複製到新會話中。newSession表示的是隻建立新的會話,而不復制資料。none表示的是保持之前的會話。
在有些情況下,應用需要限定使用同一個使用者名稱同時進行登入所產生的會話數目。比如有些應用可能要求每個使用者在同一時間最多隻能有一個會話。可以通過 元素的子元素 來限制每個使用者的併發會話個數。如就限定了每個使用者在同一時間最多隻能有兩個會話。如果當前使用者的會話數目已經達到上限,而使用者又再次登入的話,預設的實現是使之前的會話失效。如果希望阻止後面的這次登入的話,可以設定屬性 error-if-maximum-exceeded的值為 true。這樣的話,後面的這次登入就會出錯。只有當之前的會話失效之後,使用者才能再次登入。
記住使用者 有些 Web 應用會在登入介面提供一個複選框,詢問使用者是否希望在當前計算機上記住自己的密碼。如果使用者勾選此選項的話,在一段時間內使用者訪問此應用時,不需要輸入使用者名稱和密碼進行登入。Spring Security 提供了對這種記住使用者的需求的支援。只需要在 中新增元素即可。
一般來說,有兩種方式可以實現記住使用者的能力。一種做法是利用瀏覽器端的 cookie。當用戶成功登入之後,特定內容的字串被儲存到 cookie 中。下次使用者再次訪問的時候,儲存在 cookie 中的內容被用來認證使用者。預設情況下使用的是這種方式。使用 cookie 的做法存在安全隱患,比如攻擊者可能竊取使用者的 cookie,並用此 cookie 來登入系統。另外一種更安全的做法是瀏覽器端的 cookie 只儲存一些隨機的數字,而且這些數字只能使用一次,在每次使用者登入之後都會重新生成。這些數字儲存在伺服器端的資料庫中。如果希望使用這種方式,需要建立一個數據庫表,並通過 data-source-ref屬性來指定包含此表的資料來源。
總結 對於使用 Spring 開發的 Web 應用來說,Spring Security 是增加安全性時的最好選擇。本文詳細介紹了 Spring Security 的各個方面,包括實現基本的使用者認證和授權、保護服務層方法、使用訪問控制列表保護具體的領域物件、JSP 標籤庫和與 LDAP 和 OAuth 的整合等。通過本文,開發人員可以瞭解如何使用 Spring Security 來實現不同的使用者認證和授權機制。