《Spring3實戰》摘要(9)保護Spring應用(Spring Security)

第九章 保護Spring應用

9.1 Spring Security 介紹

Spring Security 是爲基於 Spring 的應用程序提供聲明式安全保護的安全性框架。Spring Security 提供了完整的安全性解決方案,它能夠在 Web 請求級別和方法調用級別處理身份驗證和授權。因爲基於 Spring 框架,所以 Spring Security 充分利用了依賴注入和麪向切面的技術。

Spring Security 從兩個角度來解決安全性問題。
它使用 Servlet 過濾器保護 Web 請求並限制 URL 級別的訪問,也可以使用 Spring AOP 保護方法調用—-藉助於對象代理和使用通知,能夠確保只有具備適當權限的用戶才能訪問安全保護的方法。

9.1.1 Spring Security 起步

不管使用 Spring Security 保護哪種類型的應用程序,第一件需要做的事就是將 Spring Security 模塊添加到應用程序的類路徑下。(用到哪個模塊就導入對應的 jar 包)。

Spring Security 3.0 分爲了8個模塊:

模塊 描述
ACL 支持通過訪問控制列表爲域對象提供安全性
CAS客戶端 提供與 JA-SIG 的中心認證服務(CAS,Central Authentication Service)進行集成的功能
配置(config) 包含了對 Spring Security XML 的命名空間的支持
核心(core) 提供了 Spring Security 基本庫
LDAP 支持基於輕量目錄訪問協議(Lightweight Directory Access Protocol)進行認證
OpenID 支持分散式 OpenID 標準
Tag Library 包含了一組 JSP 標籤來實現視圖級別的安全性
Web 提供了 Spring Security 基於過濾器的 Web 安全性支持


應用程序類路徑下至少包含核心和配置這兩個模塊。
Spring Security 被用於保護 Web 應用,需要添加 Web 模塊。
如果需要使用到 Spring Security 的 JSP 標籤庫,還要已添加 Tag Library 模塊。

9.1.2 使用 Spring Security 配置命名空間

Spring Security 提供了安全性相關的命名空間,這極大簡化了 Spring的安全性配置。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-3.0.xsd" default-lazy-init="true">


</beans>

在 Web項目中,我們可以將安全性相關的配置拆分到一個單獨的 Spring 配置文件中。鑑於這個文件中的所有配置都來自於安全性命名空間,因此我們將安全性命名空間改爲這個文件的首要命名空間。

將安全性命名空間作爲首要命名空間之後,我們就可以避免爲所有元素添加那些令人討厭的 “security” 前綴了。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/security" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:beans="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-3.0.xsd" default-lazy-init="true">


</beans>

9.2 保護 Web 請求

我們使用 Java Web 應用所做的任何事情都是從 HttpServletRequest 開始的。如果說請求是 Web 應用入口的話,那這也是 Web 應用的安全性起始位置。

對於請求級別的安全性來說,最基本的形式涉及聲明一個或多個 URL 模式,並要求具備一定級別權限的用戶才能對其進行訪問,並阻止無這些權限的用戶訪問這些 URL 背後的內容。更進一步來講,你可能還會要求只能通過 HTTPS 訪問特定的 URL。

9.2.1 代理 Servlet 過濾器

Spring Security 藉助一系列 Servlet 過濾器來提供各種安全性功能。

在應用的 web.xml 中配置一個過濾器。

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <!-- DelegatingFilterProxy是一個特殊的 Servlet 過濾器,它本身所做的工作並不多,只是將工作委託給一個 javax.servlet.Filter 實現類,這個實現類作爲一個 bean 註冊在 Spring 應用上下文中 -->
    <!-- 我們無法對註冊在 web.xml 中的 Servlet 過濾器進行 Bean 注入,通過使用 DelegatingFilterProxy ,我們可以在 Spring 中配置實際的過濾器,從而能夠充分利用 Spring 對依賴注入的支持。 -->
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>

DelegatingFilterProxy 的 <filter-name> 值是有意義的。這個名字用於在 Spring 應用上下文中查找過濾器 Bean。Spring Security 將自動創建一個 ID 爲 springSecurityFilterChain 的過濾器 Bean,這就是我們在 web.xml 中爲 DelegatingFilterProxy 所設置的 name 值。

springSecurityFilterChain 本身是另一個特殊的過濾器,它也被稱爲 FilterChainProxy。它可以鏈接任意一個或多個其他的過濾器。Spring Security 依賴一系列 Servlet 過濾器來提供不同的安全特性。但是,你幾乎不需要知道這些細節,因爲你不需要顯式聲明 springSecurityFilterChain 以及它所鏈接在一起的其他過濾器。當配置<http>元素時,Spring Security 將會爲我們自動創建這些Bean。

9.2.2 配置最小化的Web安全性

<!-- 在Spring Security的配置文件中配置,表示Spring Security攔截所有URL請求(使用Ant風格的路徑來聲明<intercept-url>的pattern屬性) -->
<!-- access屬性限制了只有擁有 ROLE_SPITTER(自定義的角色)角色的認證用戶才能訪問。 -->
<http auto-config="true">
    <intercept-url pattern="/**" access="ROLE_SPITTER" />
</http>

上示代碼中,<http>元素將會自動構建一個 FilterChainProxy (它會委託給配置在 web.xml 中的 DelegatingFilterProxy)以及鏈中的所有過濾器 Bean。

將 auto-config 屬性配置爲 true ,其會爲我們的應用提供一個額外的登錄頁、HTTP 基本認證和退出功能。實際上,將 auto-config 屬性配置爲 true 等價於下面這樣顯示配置的特性:

<http>
    <form-login />
    <http-basic />
    <logout />
    <intercept-url pattern="/**" access="ROLE_SPITTER" />
</http>

9.2.2.1 通過表單進行登錄

將 auto-config 屬性配置爲 true,Spring Security 將會自動爲你生成登錄頁面。你可以通過相對於應用上下文 URL 的 /spring_security_login 路徑來訪問這個自動生成的登錄表單。

<!-- Spring Security 自動生成的登錄頁面樣例 -->
<html>
    ...
    <form name="f" method="POST" action="/Spitter/j_spring_security_check">
        ...
        <input type="text" name="j_username" value="">
        ...
        <input type="password" name="j_password" />
        ...
    </form>
</html>

但是這個表單並不美觀,我們很可能會將其替換爲自己設計的登錄頁面。爲了設置自己的登錄頁,我們需要配置 <form-login> 元素來取代默認的行爲:

<!-- 
    login-page 屬性:爲登錄頁聲明一個新的且相對於上下文的URL。在示例中聲明登錄頁爲 /login, 它最終由一個 Spring MVC 控制器來進行處理。
    authentication-failure-url 屬性:如果認證失敗,將會把用戶重定向到相同的登錄頁。
    login-processing-url 屬性:這是登錄表單提交回來進行用戶認證的 URL。-->
<http auto-config="true" use-expressions="false">
    <form-login login-processing-url="/static/j_spring_security_check"
        login-page="/login"
        authentication-failure-url="/login?login_error=t"/>
        ...
</http>

通過上文所示的自動生成的登錄表單,我們可以知道 Spring Security 處理登錄請求時,用戶名和密碼需要在請求中使用名爲 j_username 和 j_password 的輸入域來進行提交。這樣,我們就可以創建自己的登錄頁了。

9.2.2.2 處理基本認證

對於應用程序是人類用戶,基於表單的認證是比較理想的。但是在第11章中,我們將會看到如何將Web應用的頁面轉化爲 RESTful API 。當應用程序的使用者是另外一個應用程序的話,使用表單來提示登錄的方式就不太合適了。

HTTP基本認證(HTTP Basic Authentication)是直接通過HTTP請求本省來對要訪問應用程序的用戶進行認證的一種方式。你可能在以前見過HTTP基本認證。當在 Web 瀏覽器中使用時,它將向用戶彈出一個簡單的模擬對話框。

但這只是 Web 瀏覽器的顯示方式。本質上,這是一個 HTTP 401 響應,表明必須在請求中包含一個用戶名和密碼。在 REST 客戶端向它使用的服務進行認證的場景中,這種方式比較適合。

<http-basic> 元素中,並沒有太多的可配置項。HTTP 基本認證要麼開啓要麼關閉。所以,與其進一步討論這個話題,還不如看看<logout>元素爲我們帶來了什麼。

9.2.2.3 退出

<logout>元素會構建一個 Spring Security 過濾器,這個過濾器用於使用戶的會話失效。在使用的時候,通過<logout>構建起來的過濾器將匹配“/j_spring_security_logout”地址。但這與我們已經構建的 DispatcherServlet 並不衝突,我們需要像登錄表單那樣重寫這個過濾器的 URL。爲了做到這一點,需要設置 logout-url 特性:

<http auto-config="true" use-expressions="false">
    <form-login login-processing-url="/static/j_spring_security_check"
        login-page="/login"
        authentication-failure-url="/login?login_error=t"/>
        ...
    <logout logout-url="/static/j_spring_security_logout" />        
    ...
</http>

以上就是自動配置爲我們帶來的功能。

9.2.3 攔截請求

<intercept-url> 元素是實現請求級別安全的第一道防線。它的 pattern 屬性定義了對傳入請求要進行匹配的 URL 模式。如果請求匹配這個模式的話,<intercept-url>的安全規則就會啓動。

<http auto-config="true">
    <!-- 
        pattern="/**",表明所有的請求都需要具備 ROLE_SPITTER(自定義) 角色才能進行訪問。
     -->
    <intercept-url pattern="/**" access="ROLE_SPITTER" />
</http>

pattern 屬性默認爲使用 Ant 風格的路徑。可以通過將<http>元素的 path-type 屬性設置爲 regex,pattern 屬性就可以使用正則表達式了。

假設 Spitter 應用程序中的一些特定區域,只有管理用戶才能訪問。爲了實現這一點,我們可以在已有的那條記錄前插入以下的<intercept-url>:

<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />

這條<intercept-url>限制這個站點的/admin分支只能由具備 ROLE_ADMIN 權限的用戶才能訪問。你可以使用任意數量的<intercept-url>條目來保護 Web 應用程序中的各種路徑。但是比較重要的一點是<intercept-url>規則是從上往下使用的。所以。這個新的<intercept-url>應該放在原有記錄之前,否則它會因爲前面更寬泛的 /** 路徑範圍而失去作用。

9.2.3.1 使用 Spring 表達式進行安全保護

列出所需的權限很簡單,但這卻顯得有些功能單一。Spring security 3.0 版本也支持 SpEL 作爲聲明訪問限制的一種方式。爲了啓用它,必須將 <http> 的 use-espressions 屬性設置爲 true:

<http auto-config="true" use-espressions="true">
...
</http>

現在我們可以在 access 屬性中使用 SpEL 表達式了。

<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />

如果當前用戶被授予了給定的權限,則 hasRole() 表達式將會得到 true 值。hasRole() 只是 Spring 支持的安全相關表達式中的一種。

下表列出了 Spring Security 3.0 支持的所有 SpEL 表達式:

安全表達式 計算結果
authentication 用戶認證對象
denyAll 結果始終爲false
hasAnyRole(list of roles) 如果用戶被授予了指定的任意權限,結果爲true
hasRole(role) 如果用戶被授予了指定的權限,結果爲true
hasIpAddress(IP Address) 用戶的IP地址(只能用在 Web 安全性中)
isAnonymous() 如果當前用戶爲匿名用戶,結果爲true
isAuthenticated() 如果當前用戶不是匿名用戶,結果爲true
isFullyAuthenticated() 如果當前用戶不是匿名用戶也不是remember-me認證的,結果爲true
isRememberMe() 如果當前用戶是通過remember-me自動認證的,結果爲true
permitAll 結果始終爲true
principal 用戶的主要信息對象
<!-- 示例:限制 /admin/** 這些URL,不僅需要ROLE_ADMIN,還需要指定的IP地址,才能訪問 -->
<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN') and hasIpAddress('192.168.1.2')" />

9.2.3.2 強制請求使用 HTTPS

使用 HTTP 提交數據是一件具有風險的事情。如果你通過 HTTP 發送諸如密碼和信用卡賬號這樣的敏感信息,那你就是在找麻煩了。這就是爲什麼敏感信息要通過 HTTPS 來加密發送的原因。

<intercept-url>元素的 requires-channel 屬性將通道增強的任務轉移到了 Spring Security 配置中。

<!-- 不管何時,只要是對 /spitter/form 的請求,Spring Security 都視爲需要HTTPS通道並自動將請求重定向到HTTPS上。 -->
...
<intercept-url pattern="/spitter/form" requires-channel="https" />
...
<!-- 類似地,首頁不需要 HTTPS,所以我們可以聲明其使用 HTTP 進行發送 -->
...
<intercept-url pattern="/home" requires-channel="http" />
...

9.3 保護視圖級別的元素

爲了支持視圖級別的安全性,Spring Security 提供了一個 JSP 標籤庫。這個標籤庫很小且只包含 3 個標籤:

JSP 標籤 作用
<security:accesscontrollist> 如果當前認證用戶對特定的域對象具備某一指定的權限,則渲染標籤主體中的內容
<security:authentication> 訪問當前用戶認證對象的屬性
<security:authorize> 如果特定的安全性限制滿足的話,則渲染標籤主體中的內容

爲了使用 JSP 標籤庫,需要在對應的 JSP 中聲明它:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

9.3.1 訪問認證信息的細節

<!-- 在頁面頂部顯示登錄用戶的用戶名 -->
Hello <security:authentication property="principal.username" />

property 屬性用來標示用戶認證對象的一個屬性。可用的屬性取決於用戶認證的方式。但是,你可以依賴幾個通用的屬性,在不同的認證方式下,它們都是可用的。

認證屬性 描述
authorities 一組用於表示用戶所授權權限的 GranteAuthority 對象
credentials 用於覈實用戶的憑據(通常是用戶的密碼)
details 認證的附加信息(IP地址、證件序列號、會話ID等)
principal 用戶的主要信息對象
<!--
    var 屬性可爲指定的變量名(示例爲loginId)賦值
    scope 屬性設置變量的作用域,默認是當前頁面。
-->
<security:authentication property="principal.username"  var="loginId" scope="request" />

9.3.2 根據權限渲染

Spring Security 的 <security:authorize> 標籤能夠根據用戶被授予的權限有條件地渲染頁面的部分內容.

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>

<!-- 只有在具有 ROLE_SPITTER 權限時,才被渲染 -->
<security:authorize access="hasRole('ROLE_SPITTER')">
    <s:url value="/spittles" var="spitter_url" />
        <sf:form modelAttribute="spittle" action="${spittle_url}" >
            <sf:label path="text">
                <s:message code="label.spittle" text="Enter spittle:" />
            </sf:label>
            <sf:textarea path="text" rows="2" cols="40" />
            <sf:errors path="text" />
        </br>
        <div class="spitItSubmitIt">
            <input type="submit" value="Spit it" class="status-btn round-btn disabled" />
        </div>
    </sf:form>
</security:authorize>

access 屬性被賦值爲一個 SpEL 表達式,這個表達式的值將確定<security:authorize標籤主體內的內容是否被渲染。

<!-- 如果當前用戶不是匿名用戶並且用戶名爲'habuma'則爲用戶渲染此鏈接 -->
<security:authorize access="isAuthenticated() and principal.username=='habuma'">
    <a href="/admin">Administration</a>
</security:authorize>

<!-- 
    上例只能在視圖上阻止鏈接的渲染,並不能阻止在瀏覽器中手動輸入 /admin URL
    可如9.2.3所示,在安全性配置文件中添加一個新的<intercept-url>
 -->
 <intercept-url pattern="/admin/**"
     access="hasRole('ROLE_ADMIN') and hasIpAddress('192.168.1.2')" />
<!-- 
    url 屬性對一個給定的 URL 模式間接引用其安全性限制。無需如 access 屬性那樣明確聲明安全性限制。
    結合第二條配置,表示只有具備 ROLE_ADMIN 權限的認證用戶,而且來自於特定IP地址的請求才能訪問 /admin URL.
-->
<security:authorize url="/admin/**">
    <s:url value="/admin" var="admin_url" />
    <br /> <a href="${admin_url}">Admin</a>
</security:authorize>

9.4 認證用戶

Spring Security 非常靈活,基本上能夠處理任意我們所需的認證策略。
Spring Security 涵蓋了許多常用的認證場景,包含如下的用戶認證策略:

  • 內存(基於 Spring 配置)用戶存儲庫
  • 基於 JDBC 的用戶存儲庫
  • 基於 LDAP 的用戶存儲庫
  • OpenID 分散式的用戶身份識別系統
  • 中心認證服務(CAS)
  • X.509證書
  • 基於 JAAS 的提供者

如果沒有合適的內置用戶認證策略,你還可以很容易地實現自己的認證策略,並將其裝配進來。

9.4.1 配置內存用戶存儲庫

在可用的認證策略中,最簡單的一種就是直接在 Spring 配置中聲明用戶的詳細信息。這可以通過使用 Spring Security XML 命名空間中的 <user-service> 元素來創建一個用戶服務來實現。

<user-service id="userService">
    <user name="habuma" password="letmein"
        authorities="ROLE_SPITTER,ROLE_ADMIN" />
    <user name="twoqubed" password="longhorns" 
        authorities="ROLE_SPITTER"/>
    <user name="admin" password="admin"
        authorities="ROLE_ADMIN" />
</user-service>

用戶服務實際上是一個數據訪問對象,它在給定用戶登錄ID時查找用戶詳細信息。用戶服務現在已經準備就緒,並等待爲認證功能查找用戶詳細信息。剩下的事情就是將其裝配到 Spring Security 的認證管理器中:

<authentication-manager>
    <authentication-provider user-service-ref="userService" />
</authentication-manager>

<authentication-manager>元素會註冊一個認證管理器。更確切的將,它將註冊一個 ProviderManager 實例,認證管理器將把認證的任務委託給一個或多個認證提供者。在本示例中,是一個依賴於用戶服務的認證提供者來獲取用戶詳細信息。

我們還可以直接將用戶服務嵌入到認證提供者中:

<authentication-provider>
    <user-service id="userService">
        <user name="xxx" password="xxx" 
            authorities="aaa,bbb" />
        ...
    </user-service>
</authentication-provider>

9.4.2 基於數據庫進行認證

使用 Spring Security 提供的<jdbc-user-service>元素設置用戶服務。

<jdbc-user-service> 的使用方式與 <user-service> 相同。這包括將其裝配到 <authentication-provider> 的 user-service-ref 屬性中或者將其嵌入到 <authentication-provider>中。

<jdbc-user-service id="userService" 
    data-source-ref="dataSource" />

如果沒有其他的配置,用戶服務將會使用如下的 SQL 語句

-- 查詢用戶信息
select username, password, enabled from users where username = ?

--查詢用戶權限
select username, authority from authorities where username = ?

大多數情況下,默認的 SQL 語句並不能正常工作。幸好,<jdbc-user-service> 能夠方便地配置成最適合你應用程序的查詢。<jdbc-user-service> 的屬性能夠改變查詢用戶詳細信息的 SQL 語句。

屬性 作用
users-by-username-query 根據用戶名查詢用戶的用戶名、,密碼以及是否可用的狀態
authorities-by-username-query 根據用戶名查詢用戶被授予的權限
group-authorities-by-username-query 根據用戶名查詢用戶的組權限
<!-- 修改用戶認證默認SQL 的簡單示例 -->
<jdbc-user-service id="userService" data-source-ref="dataSource"
    users-by-username-query="select username, password from spitter where username=?"
    authorities-by-username-query="select username,role  from spitter where username=?" />

9.4.3 基於 LDAP 進行認證

大多數組織機構都是等級結構化的。關係型數據庫是非常有用的,但是它們不能很好地表示層級的數據。而另一方面,LDAP目錄恰好擅長存儲層級數據。基於以上原因,公司的組織機構在 LDAP 目錄中進行展現是很常見的。另外,你會發現公司的安全性限制往往對應於目錄中的一個條目。

爲了使用基於 LDAP 的認證,我們首先需要使用到 Spring Security 的 LDAP 模塊,並在 Spring 應用上下文中配置 LDAP 認證。當配置 LDAP 認證時,有兩種選擇:

  • 使用面向 LDAP 的認證提供者 ;
  • 使用面向 LDAP 的用戶服務。

9.4.3.1 聲明 LDAP 驗證提供者

對於面向 LDAP 的用戶服務,同樣可以聲明 <authentication-provider>並裝配用戶服務。但是一種更直接的方式是使用一個特殊的面向 LDAP 的認證提供者,可以通過在<authentication-manager>中聲明<ldap-authentication-provider>來實現:

<authentication-manager alias="authenticationManager">
    <ldap-authentication-provider user-search-filter="(uid={0})"
    group-search-filter="member={0}" />
</authentication-manager>

屬性 user-search-filter 和 group-search-filter 用於爲基礎 LDAP 查詢提供過濾條件,它們分別用於搜索用戶和組。默認情況下,對於用戶和組的基礎查詢都是空的,也就是表明搜索會在 LDAP 層級結構的根開始。但是我們可以通過制定查詢基礎來改變這個默認行爲:

<ldap-user-service id="userService"
    user-search-base="ou=people"
    user-search-filter="({uid={0})"
    group-search-base="ou=groups"
    group-search-filter="member={0}" />

user-search-base 屬性爲查找用戶提供了基礎查詢。同樣,group-search-base 爲查找組指定了基礎查詢。上例中,我們聲明用戶應該在名爲 people 的組織單元下搜索而不是從跟開始。而組應該在名爲 groups 的組織單元下搜索。

9.4.3.2 配置密碼比對

基於 LDAP 進行認證的默認策略是進行綁定操作,直接通過 LDAP 服務器認證用戶。另一種可選的方式是進行比對操作。這涉及將輸入的密碼發送到 LDAP 目錄上,並要求服務器將這個密碼和用戶的密碼進行比對。

如果你希望通過密碼比對進行認證,則可以通過聲明<password-compare>元素實現:

<ldap-authentication-provider 
    user-search-filter="(uid={0})"
    group-search-filter="member={0}">
    <password-compare />
</ldap-authentication-provider>

正如上面所聲明的,在登錄表單中提供的密碼將會與用戶的 LDAP 條目中的 userPassword 屬性進行比對。如果密碼被保護在不同的屬性中,可以通過 password-attribute 來聲明:

<password-compare hash="md5"
    password-attribute="passcode" />

hash 屬性可以設置爲如下某個值來聲明加密策略:

  • {sha}
  • {ssha}
  • md4
  • md5
  • plaintext
  • sha
  • sha-256

9.4.3.3 引用遠程的 LDAP 服務器

默認情況下,Spring Security 的 LDAP 認證假設 LDAP 服務器監聽本機的 33389 端口。但是,如果你的 LDAP 服務器在另一臺機器上,那麼可以使用 <ldap-authentication-provider>元素來配置這個地址:

<ldap-server url="ldap://habuma.com:389/dc=habuma,dc=com">

9.4.3.4 配置嵌入式的 LDAP 服務器

如果你沒有現成的 LDAP 服務器進行認真,那 <ldap-server>還可以依賴嵌入式的 LDAP 服務器。只需去掉 url 參數。

<ldap-server root="dc=habuma,dc=com" />

root 屬性是可選的。它的默認值是”dc=springframework,dc=org”。

當 LDAP 服務器啓動時,它會嘗試在類路徑下查找 LDIF 文件來加載數據。LDIF(LDAP 數據交換格式)是以文本文件顯示 LDAP 數據的標準方式。每條記錄可以有一行或多行,每項包含一個名值對。記錄之間通過空行進行分隔。

如果你想更明確指定加載哪個 LDIF 文件,可以使用 ldif 屬性:

<ldap-server root="dc=habuma,dc=com" 
    ldif="classpath:users.ldif"/>

ldif 文件示例:

dn: ou=groups,dc=habuma,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: out=people,dc=habuma,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=habuma,ou=people,dc=habuma,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Craig Walls
sn: Walls
uid: habuma
userPassword: password

dn: uid=jsmith,ou=people,dc=habuma,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: John Smith
sn: Smth
uid: jsmith
userPassword: password

dn: cn=spitter,ou=groups,dc=habuma,dc=com
objectclass: top
objectclass: groupOfNames
cn: spitter
member: uid=habuma,ou=people,dc=habuma,dc=com

9.4.4 啓用 remember-me 功能

remember-me 功能,只需要你登錄過一次,應用就會記住你,當再次回到應用的時候你就不需要再次登錄了。

Spring Security 爲應用添加 remember-me 功能非常容易。爲了啓用這項功能,我們只需要在 <http> 元素中添加一個 <remember-me> 元素:

<http auto-config="true" use-expressions="true">
    ...
    <remember-me key="spitterKey" 
        token-validity-seconds="2419200" />
</http>

如果你在使用 <remember-me> 元素時沒有配置任何屬性,那麼這個功能是通過在 cookie 中存儲一個令牌(token)完成的,這個額令牌最多2周內有效。但是,在上例中,我們制定這個令牌最多4周內有效(2419200秒)。

存儲在 cookie 中的令牌包含用戶名、密碼、過期時間和一個私鑰—-在寫入 cookie 前都進行了 MD5 哈希。默認情況下,私鑰名爲 SpringSecured,上例中將它設置爲 spitterKey 來使其專門用於 Spitter 應用中。

既然 remember-me 功能已經啓動,我們需要有一種方式來讓用戶表明他們希望應用程序記住他們。爲了實現這一點,登錄請求必須包含一個名爲 _spring_security_remember_me 的參數。登錄表單中的簡單複選框可以完成這件事情:

<input id="remember_me" name="_spring_security_remember_me" 
    type="checkbox" />
<label for="remember_me" class="inline">Remember me</label>

9.5 保護方法調用

Spring AOP 是 Spring Security 中方法級安全性的基礎。保護方法調用中所有涉及的 AOP 都打包進了一個元素中:<global-method-security>

<global-method-security secured-annotations="enabled" />

這將會啓用 Spring Security 保護那些使用 Spring Security 自定義註解 @Secured 的方法。Spring Security 支持4種方法級安全性的方式:

  • 使用 @Secured 註解的方法;
  • 使用 JSR-250 @RolesAllowed 註解的方法;
  • 使用 Spring 方法調用前和調用後註解的方法;
  • 匹配一個或多個明確聲明的切點的方法。

9.5.1 使用 @Secured 註解保護方法調用

<!-- secured-annotations="enabled" 
    表明將創建一個切點來包裝使用了 @Secured 註解的 Bean 方法。 -->
<global-method-security secured-annotations="enabled" />

//例子1:
@Secured("ROLE_SPITTER")
public void addSpittle(Spittle spittle){
    //...
}

註解 @Secured 使用一個 String 數組作爲參數。每個 String 值是一個權限,調用這個方法至少需要具備其中的一個權限。

如果方法被沒有認證的用戶或沒有所需權限的用戶調用,保護這個方法的切面將拋出一個 Spring Security 異常(可能是 AuthenticationException 或 AccessDeniedException 的子類)。最終,這個異常必須要被捕獲。如果被保護的方法是在 Web 請求中調用的,這個異常會被 Spring security 的過過濾器自動處理。否則,你需要編寫代碼來處理這個異常。

@Secured 註解的不足之處在於它是 Spring 的註解。如果更傾向於使用標準註解,那麼你應該考慮使用 @RolesAllowed 註解。

9.5.2 使用 JSR-250 的 @RolesAllowed 註解

@RolesAllowed 註解和 @Secured 註解在各個方便基本上都是一致的。

如果選擇使用 @RolesAllowed,則需要將global-method-security的 jsr250-annotations 屬性設置爲 true 以啓用此功能:

<global-method-security jsr250-annotations="enabled" />

jsr250-annotations 與 secured-annotations 並不衝突,這兩種註解風格可以同時開啓。它們甚至可以與 Spring 的方法調用前後安全性註解共同使用,這也是我們接下來講解的內容。

9.5.3 使用 SpEL 實現調用前後的安全性

儘管 @Secured 和 @RolesAllowed 註解在拒絕爲認證用戶方面表現不錯,但這也是他們所能做到的所有事情了。有時候,安全性限制不僅僅涉及用戶是否擁有權限。

Spring Security 3.0 引入了幾個新註解,它們使用 SpEL 能夠在方法調用上實現更有意思的安全性限制。

Spring security 3.0 提供了4個新的註解,可以使用 SpEL 表達式來保護方法調用:

註解 描述
@PreAuthorize 在方法調用之前,基於表達式的計算結果來限制對方法的訪問
@PostAuthorize 允許方法調用,但是如果表達式計算結果爲false,則會拋出一個安全性異常
@PostFilter 允許方法調用,但必須按照表達式來過濾方法的結果
@PreFilter 允許方法調用,但必須在進入方法之前過濾輸入值



如果你希望使用這些註解,則需要將 <global-method-security> 的 pre-post-annotations 屬性設置爲 enabled 來啓用它們:

<global-method-security pre-post-annotations="enabled" /> 

9.5.3.1 在方法調用前驗證權限

// 示例1:只允許擁有 ROLE_SPITTER 角色的用戶訪問方法
@PreAuthorize("hasRole('ROLE_SPITTER')")
public  void addSpittle(Spittle spittle){
    //...
}

// 示例2:ROLE_SPITTER角色的用戶傳入的 spittle.text 的長度需小於等於140方可訪問方法,對 ROLE_PREMIUM 角色用戶無限制
//表達式中的 #spittle 部分直接引用了方法中的同名參數。這使得Spring Security 能夠檢查傳入方法的參數。
@PreAuthorize("hasRole('ROLE_SPITTER') 
    and #spittle.text.length() <= 140 
    or hasRole('ROLE_PREMIUM')")
public  void addSpittle(Spittle spittle){
    //...
}

9.5.3.2 在方法調用後驗證權限

在方法調用之後驗證權限並不是比較常見的方式。事後驗證一般用於基於安全保護方法的返回值來進行安全性決策的場景中。這種情況意味着方法必須被調用執行並且得到了返回值。

除了驗證的時機之外,@PostAuthorize 與 @PreAuthorize 的工作方式差不多。

示例:
@PostAuthorize("returnObject.spitter.username == principal.username")
public Spittle getSpittleById(Long id){
    //...
}

爲了便利地訪問受保護方法的返回對象,Spring Security 在 SpEL 提供了 returnObject 變量名。在上述示例中,表達式通過returnObject.spitter.username直接訪問返回值中spittle屬性中的 username 屬性。

上述示例中,表達式到內置的 principal 對象中取出其 username 屬性。principal 是另一個 Spring Security 內置的特殊名字,它代表了當前認證用戶的主要信息。

需要注意的是,@PostAuthorize 註解的方法會首先執行然後被攔截。這意味着,你需要確保一旦驗證失敗不會出現一些負面的結果。

9.5.3.3 事後對方法返回值進行過濾

// 示例1:
@PreAuthorize("hasRole('ROLE_SPITTER')")
@PostFilter("filterObject.spitter.username == principal.name")
public List<Spittle> getABunchOfSpittles() {
    //...
}

上述示例1中,@PostAuthorize 註解只允許具有 ROLE_SPITTER 權限的用戶執行這個方法。如果用戶通過了這個檢查點,方法將會被調用並返回一個 Spittle 的 List。但是 @PostFilter 註解將過濾這個列表,確保用戶只能看到屬於自己的 Spittle 對象。

//示例2:過濾掉用戶對當前 filterObject(示例中爲Spittle對象) 沒有刪除權限的對象
@PreAuthorize("hasRole('ROLE_SPITTER')")
@PostFilter("hasPermission(filterObject,'delete')")
public List<Spittle> getABunchOfSpittles() {
    //...
}

實際上 hasPermission() 默認一直返回 false。需要重寫 hasPermission() 的默認行爲,這涉及到創建和註冊一個許可計算器。示例代碼如下:

import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import com.spring.springdemo.spittermvc.entity.Spittle;

public class SpittlePermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object target, Object permission) {
        if(target instanceof Spittle){
            Spittle spittle = (Spittle) target;
            if("delete".equals(permission)){
                return spittle.getSpitter().getUsername()
                        .equals(authentication.getName()) 
                        // hasProfanity()方法表示是否有侮辱性的詞彙
                        || hasProfanity(spittle);
            }

        }
        throw new UnsupportedOperationException("hasPermission not supported for object <" 
                + target 
                + "> and permission <" + permission +">");
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        throw new UnsupportedOperationException();
    }

    private boolean hasProfanity(Spittle spittle){
        // ...
        return false;
    }
}

SpittlePermissionEvaluator 實現了 Spring Security 的 PermissionEvaluator 接口,它需要實現兩個不同的 hasPermission() 方法。其中的一個 hasPermission() 方法把要評估的對象作爲第二個參數。第二個 hasPermission() 方法在只有目標對象的 ID 可以得到的時候纔有用,並將 ID 作爲 Serializable 傳入第二個參數。(這裏只是簡單地拋出異常)。

許可計算器已經準備就緒,你需要將其註冊到 Spring Security 中,以便在使用 @PostFilter 時支持 haspermission() 操作。要做到這一點需要創建一個表達式處理器並註冊到 <global-method-security> 中。

對於表達式處理器,你需要創建 DefaultmethodSecurityExpressionHandler 類型的 Bean,並將 SpittlePermissionEvaluator 的實例作爲它的 permissionEvalutor 屬性注入進去。

<beans:bean id="expressionHandler" 
    class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
    <beans:property name="permissionEvalutor">
        <beans:bean class="com.xxx.xxx.SpittlePermissionEvaluator" />
    </beans:property>
</beans:bean>

接下來,我們就可以在 <global-method-security> 中配置 expressionHandler,如下所示:

<global-method-security pre-post-annotations="enabled">
    <expression-handler ref="expressionHandler" />
</global-method-security>

以前,在配置<global-method-security>時,我們沒有指定表達式處理器。但是在這裏,配置了幫我們計算的表達式處理器,用於替換默認的表達式處理器。

9.5.4 聲明方法級別的安全性切點

方法級別的安全性限制在不同的方法見往往有所差別。爲每個方法添加最合適的約束註解有很大的意義。但是,有時候爲幾個方法設置相同的授權檢查也是很有意義的,也稱爲橫切的授權。

爲了限制對多個方法進行訪問,可以使用 <protect-pointcut> 作爲 <global-method-security> 元素的子元素。例如:

<global-method-security>
    <protect-pointcut access="ROLE_SPITTER" 
        expression="execution(@com.xx.xxx.Sensitive * *.*(String))" />
</global-method-security>

expression 屬性被設置成了一個 AspectJ 切面表達式。在本示例中,它標示了所有使用 @Sensitive 自定義註解的方法。同時,access 屬性標示認證用戶需要什麼樣的權限才能訪問 expression 屬性所指定的方法。

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