使用 Grails 簡化 Spring Security

工程 | Peter Ledbrook | 2010年8月11日 | ...

Spring Security 是一個功能強大的庫,用於保護你的應用程式,提供了令人眼花繚亂的選項。基於 Spring,它可以輕鬆整合到 Grails 應用程式中。但為什麼不省去麻煩,使用新的改進版 Grails 外掛呢?

該外掛經歷了從 Acegi 外掛開始的幾個演進階段。它最近一次的迭代是針對 Spring Security 3 和 Spring 3 的完全重寫。其中一個結果是該外掛只能與 Grails 1.2.2 及更高版本一起使用。另一個重大變化是 Spring Security 外掛不再是單一的:一些功能已被拆分為可選外掛。因此,現在你只需在應用程式中包含所需的功能即可。

那麼這些外掛為你提供了什麼?核心外掛提供基於使用者和角色的、易於使用的訪問控制基礎功能。實際上,許多應用程式除了核心外掛之外不需要任何其他外掛。對於那些需要額外功能的開發者,下面列出了該家族的其他外掛:

  • OpenID - 使用 OpenID 進行身份驗證
  • LDAP - 針對 LDAP 伺服器進行身份驗證
  • CAS - 使用 CAS 進行單點登入
  • ACLs - 透過 Spring Security 的 ACLs 進行訪問控制
  • UI - 用於使用者和角色管理的介面,以及其他功能

在本文中,我將向你展示如何使用新的核心外掛從零開始保護 Grails 應用程式。

更新 本文現在有兩個配套截圖影片

[caption id="attachment_5509" align="center" width="250" caption="Spring Security 外掛介紹"][/caption] [caption id="attachment_5510" align="center" width="250" caption="Spring Security 外掛 - AJAX"][/caption]

設定

與大多數外掛一樣,你的第一步是安裝 Spring Security 外掛。當然,你需要一個專案來安裝它,對於本文,我提供了一個一個簡單的 Twitter 克隆應用 Hubbub(基於《Grails 實戰》中的示例應用)。你也可以從這裡獲取已完成的專案。

因此,在你的專案內部,執行

    grails install-plugin spring-security-core

如果你檢視外掛安裝生成的輸出,你會發現它提供了幾個命令。其中最重要的是s2-quickstart,它將幫助你快速輕鬆地開始。它生成了儲存使用者資訊所需的基本領域類以及處理身份驗證的控制器。

在執行命令之前,你可能需要做一個決定。如果你已經有一個“使用者”領域類,你將不得不決定如何將其與外掛生成的領域類整合。一種選擇是替換現有的領域類,然後簡單地將你的自定義應用於替換類。另一種方法是讓你的領域類繼承外掛生成的領域類。

哪種方法更好?我更喜歡後者,因為它允許你在生成的使用者領域類模板發生變化時輕鬆更新它。這也意味著你不會用 Spring Security 的特定內容過度汙染你的領域模型。缺點是你必須處理領域類的繼承,但這方面的代價非常小。

對於 Hubbub,我們將讓使用者領域類繼承生成的領域類,這意味著我們應該使用與現有領域類不衝突的名稱

    grails s2-quickstart org.example SecUser SecRole

這將為我們建立三個領域類

  • org.example.SecUser
  • org.example.SecRole
  • org.example.SecUserSecRole- 將使用者連結到角色
以及兩個控制器
  • LoginController
  • LogoutController
以及它們關聯的檢視。只需兩個命令,我們就擁有了開始保護應用程式所需的一切!

示例應用程式還需要一項更改:其 URL 對映意味著無法訪問登入和登出控制器。透過將以下兩行新增到UrlMappings.groovy:

"/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")

可以輕鬆解決這個問題。如果你不進行此更改,登入頁面將產生 404 錯誤!現在,讓我們開始保護應用程式。

新增訪問控制

這項工作的重點是限制對應用程式某些部分的訪問。對於 Web 應用程式,這通常意味著保護特定頁面,或者更具體地說,是 URL。對於 Hubbub,我們有以下要求:

  • 主頁所有人都可以訪問 -/
  • 只有已知使用者才能看到特定使用者的帖子 -/person/<username>
  • 只有具有“user”角色的使用者才能訪問他們的時間線 -/timeline
  • 關注另一個使用者也是如此 -/post/followAjax
  • 只有完全認證且具有“user”角色的使用者才能釋出新訊息 -/post/addPostAjax

使用 Spring Security 外掛實現這一點非常簡單,儘管你必須決定使用三種機制中的哪一種。你可以採用以控制器為中心的方法並註解動作;在Config.groovy中使用靜態 URL 規則;或者使用請求對映在資料庫中定義執行時規則。

註解

對於以控制器為中心的方法,@Secured外掛提供的註解是無與倫比的。在其最簡單的形式中,你向它傳遞一個定義哪些使用者可以訪問相應動作的基本規則列表。在這裡,我透過在 post 控制器上使用註解來應用 Hubbub 的訪問控制規則

package org.example

import grails.plugins.springsecurity.Secured

class PostController {
    ...
    @Secured(['ROLE_USER'])
    def followAjax = { ... }

    @Secured(['ROLE_USER', 'IS_AUTHENTICATED_FULLY'])
    def addPostAjax = { ... }

    def global = { ... }

    @Secured(['ROLE_USER'])
    def timeline = { ... }

    @Secured(['IS_AUTHENTICATED_REMEMBERED'])
    def personal = { ... }
}

IS_AUTHENTICATED_*規則內置於 Spring Security 中,但ROLE_USER是一個必須存在於資料庫中的角色——我們尚未完成這項工作。此外,如果在列表中指定多個規則,則當前使用者通常只需滿足其中一個即可——使用者指南中對此有解釋。IS_AUTHENTICATED_FULLY是一個特例:如果指定,則必須滿足它以及列表中的其他規則。

內建規則如下:

  • IS_AUTHENTICATED_ANONYMOUSLY - 任何人都可以訪問;使用者無需登入
  • IS_AUTHENTICATED_REMEMBERED - 只有已登入或從先前會話中記住的已知使用者才能訪問
  • IS_AUTHENTICATED_FULLY - 使用者必須登入才能訪問,即使他們上次勾選了“記住我”
前兩個規則區分已知使用者和未知使用者,已知使用者是在“user”資料庫表中有條目的使用者。最後一個通常應用於使用者訪問特別敏感資訊(例如銀行賬戶或信用卡資料)的情況。畢竟,其他人可能正在使用前一個使用者的“記住我” cookie 訪問你的應用程式。

你也可以將註解應用於控制器類本身,這將導致所有動作繼承其定義的規則。如果一個動作有自己的註解,則該註解會覆蓋類級別的註解。註解也不僅限於像這樣的規則列表:查閱使用者指南,瞭解如何使用表示式提供對規則的更大控制。

靜態 URL 規則

如果你不喜歡註解,你可以透過在Config.groovy中定義一個靜態對映來定義訪問控制規則。如果你喜歡將規則集中存放,這是一個理想選擇。以下是如何使用此機制定義 Hubbub 的規則:

import grails.plugins.springsecurity.SecurityConfigType
...
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.InterceptUrlMap
grails.plugins.springsecurity.interceptUrlMap = [
    '/timeline':         ['ROLE_USER'],
    '/person/*':         ['IS_AUTHENTICATED_REMEMBERED'],
    '/post/followAjax':  ['ROLE_USER'],
    '/post/addPostAjax': ['ROLE_USER', 'IS_AUTHENTICATED_FULLY'],
    '/**':               ['IS_AUTHENTICATED_ANONYMOUSLY']
]

注意最通用的規則放在最後了嗎?那是因為順序很重要:Spring Security 會遍歷規則並應用第一個與當前 URL 匹配的規則。因此,如果 '/**' 規則放在最前面,你的應用程式將實際上不受保護,因為所有 URL 都會匹配到它。另外請注意,你必須透過grails.plugins.springsecurity.securityConfigType設定明確告知外掛使用該對映。

動態請求對映

你想在執行時更新 URL 規則而無需重啟應用程式嗎?如果是這樣,你可能需要使用請求對映,它本質上是儲存在資料庫中的 URL 規則。要啟用此機制,請將以下內容新增到Config.groovy:

import grails.plugins.springsecurity.SecurityConfigType
...
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.Requestmap

然後你只需建立Requestmap領域類的例項,例如在BootStrap.groovy:

new Requestmap(url: '/timeline', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/person/*', configAttribute: 'IS_AUTHENTICATED_REMEMBERED').save()
new Requestmap(url: '/post/followAjax', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/post/addPostAjax', configAttribute: 'ROLE_USER,IS_AUTHENTICATED_FULLY').save()
new Requestmap(url: '/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()

中。當然,這種方法會帶來效能開銷,因為它涉及到資料庫,但透過使用快取可以將其最小化。查閱使用者指南,瞭解更多相關資訊。此外,在這種情況下,你不必擔心規則的順序,因為外掛會選擇與當前 URL 匹配的最具體的 URL 模式。

你應該使用哪種方法?這取決於你的應用程式設定以及你如何看待訪問控制。當規則按控制器應用且控制器具有不同的 URL 時,註解是合理的選擇。如果你傾向於將控制器分組到單個 URL 下,例如/admin/,或者你只是喜歡將所有規則集中存放,那麼最好使用在Config.groovy中定義的靜態規則。第三種機制,請求對映,只有在你希望在執行時新增、更改或刪除規則時才有意義。一個經典的例子是在 CMS 應用程式中,其中 URL 本身是動態定義的。

無論你採用哪種方法,一旦規則實施,你的應用程式受到保護。例如,如果你此時嘗試訪問 Hubbub 中的/timeline頁面,你將被重定向到標準登入頁面

太好了!但你要以誰的身份登入呢?使用者如何登出?保護你的頁面只是第一步。你還需要確保擁有相關的安全資料(使用者和角色)以及一個具備安全意識的使用者介面。

下一步

建立了訪問控制後,你需要考慮使用者體驗。你真的想讓使用者點選他們沒有訪問許可權的連結嗎?你在訪問控制中使用的那些角色呢?它們何時建立?現在讓我們回答這些問題。

安全資料

有些應用程式只關心使用者是否已知,在這種情況下,你無需擔心角色,因為IS_AUTHENTICATED_*規則就足夠了。但如果你的應用程式需要更精細地控制誰訪問什麼,你就需要角色。這些通常在應用程式生命週期早期定義,並對應於不變的參考資料。這使得BootStrap成為建立它們的理想位置。對於 Hubbub,我們像這樣新增“user”和“admin”角色:

import org.example.SecRole

class BootStrap {
    def init = {
        ...
        def userRole = SecRole.findByAuthority('ROLE_USER') ?: new SecRole(authority: 'ROLE_USER').save(failOnError: true)
        def adminRole = SecRole.findByAuthority('ROLE_ADMIN') ?: new SecRole(authority: 'ROLE_ADMIN').save(failOnError: true)
        ...
    }
}

當然,如果資料已經存在,我們不想重新建立它,這就是我們使用findByAuthority().

的原因。新增使用者也同樣簡單,但有幾個注意事項需要記住。首先,生成的“user”領域類有一個enabled屬性,預設情況下是false。如果你不明確將其初始化為true,相應的使用者將無法登入。其次,密碼很少以明文形式儲存在資料庫中,因此你需要先使用適當的摘要演算法對其進行編碼。

幸運的是,該外掛提供了一個有用的服務來幫助解決此問題SpringSecurityService。假設我們想在 Hubbub 的BootStrap中建立一個“admin”使用者。程式碼如下:

import org.example.*

class BootStrap {
    def springSecurityService

    def init = {
        ...
        def adminUser = SecUser.findByUsername('admin') ?: new SecUser(
                username: 'admin',
                password: springSecurityService.encodePassword('admin'),
                enabled: true).save(failOnError: true)

        if (!adminUser.authorities.contains(adminRole)) {
            SecUserSecRole.create adminUser, adminRole
        }
        ...
    }
}

我們只需將安全服務注入BootStrapBootStrap,然後使用其方法將明文密碼轉換為其雜湊值。當你決定更改使用的摘要演算法時,這種方法尤其有效,因為該服務將使用與身份驗證比較時使用的演算法相同的演算法來編碼密碼。換句話說,無論使用何種演算法,上述程式碼都保持不變。

更新 從 Spring Security Core 外掛的 1.2 版本開始,生成的User類在例項儲存時會自動編碼密碼。因此,你不再需要顯式使用SpringSecurityService.encodePassword()

建立使用者後,我們檢查它是否具有“admin”角色,如果沒有,則將該角色分配給使用者。我們透過生成的SecUserSecRole類及其create()方法來實現。

安全資料到位,並且知道如何在需要時按需建立它,現在是時候讓使用者介面感知身份驗證、使用者和角色了。

使用者介面

我在這裡想看看 UI 的兩個方面:顯示特定於使用者的資訊,並確保使用者只能看到他被允許看到的內容。第一個問題歸結為一個問題:我們如何獲取當前登入使用者的“user”領域例項?考慮 Hubbub 的時間線頁面,它顯示當前使用者正在關注的人的所有帖子

class PostController {
    def springSecurityService
    ...
    @Secured(['ROLE_USER'])
    def timeline = {
        def user = SecUser.get(springSecurityService.principal.id)

        def posts = []
        if (user.following) {
            posts = Post.withCriteria {
                'in'("user", user.following)
                order("createdOn", "desc")
            }
        }
        [ posts: posts, postCount: posts.size() ]
    }
    ...
}

如你所見,我們所需要做的就是再次注入安全服務並使用它來獲取 principal。除非你建立了UserDetailsService的自定義版本(如果你以前沒有遇到過,不必擔心),principal 將是org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser的一個例項,其id屬性包含相應“user”領域例項的 ID。

你需要注意一點:如果當前使用者是匿名認證的,也就是說他沒有登入且沒有被記住,則principal屬性將返回一個字串。因此,如果某個動作可以由未認證使用者訪問,請確保在使用 principal 之前檢查其型別!

如何確保使用者只能看到他們應該看到的內容?為此,外掛在sec名稱空間中提供了一組豐富的 GSP 標籤。假設我們想為 Hubbub 新增幾個導航連結,但我們只希望在使用者未登入時顯示其中一個,而另一個只有在使用者具有ROLE_USER角色時才顯示。

<sec:ifNotLoggedIn>
  <g:link controller="login" action="auth">Login</g:link>
</sec:ifNotLoggedIn>
<sec:ifAllGranted roles="ROLE_USER">
  <g:link class="create" controller="post" action="timeline">My Timeline</g:link>
</sec:ifAllGranted>

<sec:if*>標籤內的標記只有在條件滿足時才會渲染到頁面。外掛提供了幾個其他類似的標籤,它們的行為都一致。查閱使用者指南瞭解更多資訊。

上例還向你展示瞭如何建立指向登入頁面的連結。允許使用者登出也同樣簡單。Hubbub 提供了一個側邊面板,其中顯示了登入使用者的姓名以及登出連結等資訊

<sec:username /> (<g:link controller="logout">sign out</g:link>)

簡單!這些標籤和安全服務的結合足以將你的使用者介面與 Spring Security 整合。只需記住保持你的使用者介面元素與訪問控制規則同步:你不想讓某些 UI 部分可見,而這會導致“未經授權的使用者”錯誤。

我現在已經介紹了 Spring Security 外掛的所有基本元素,但仍然有兩個將影響大量使用者的特性:AJAX 請求和自定義登入表單。

最後的拼圖塊

現在有多少 Web 應用程式沒有在一定程度上使用 AJAX?又有多少應用程式真正想使用預設的登入表單?對於內部使用來說沒問題,但我不建議將其用於面向客戶的應用。讓我們從 AJAX 開始。

保護 AJAX 請求

基於 AJAX 的動態使用者介面給訪問控制帶來了一系列新問題。處理需要身份驗證的標準請求非常容易:只需將使用者重定向到登入頁面,然後在身份驗證成功後將其重定向回目標頁面。但這種重定向對於 AJAX 來說效果不佳。那麼該怎麼辦呢?

外掛提供了一種處理 AJAX 請求不同於普通請求的方式。當 AJAX 請求需要身份驗證時,Spring Security 會重定向到authAjax動作,而不是LoginControllerLoginControllerauth。但是等等,那仍然是一個重定向對吧?是的,但你可以實現authAjaxauthAjax

動作在控制器中傳送錯誤狀態或渲染 JSON——基本上客戶端 Javascript 程式碼可以處理的任何內容。LoginController不幸的是,authAjaxLoginController

import javax.servlet.http.HttpServletResponse

class LoginController {
    ...
    def authAjax = {
        response.sendError HttpServletResponse.SC_UNAUTHORIZED
    }
    ...
}

外掛提供的

<g:form action="ajaxAdd">
    <g:textArea id='postContent' name="content" rows="3" cols="50" onkeydown="updateCounter()" /><br/>
    <g:submitToRemote value="Post"
                 url="[controller: 'post', action: 'addPostAjax']"
                 update="[success: 'firstPost']"
                 onSuccess="clearPost(e)"
                 onLoading="showSpinner(true)"
                 onComplete="showSpinner(false)"
                 on401="showLogin();"/>
</g:form>

authAjax動作目前尚未實現,因此你需要自己新增它這是一個非常簡單的實現,返回 401 HTTP 狀態碼。我們如何處理這樣的響應?這取決於你在瀏覽器中用於實現 AJAX 的方式。示例 Hubbub 應用程式使用自適應 AJAX 標籤,因此我將以此為例來演示你可以做的事情。這是用於釋出新訊息的 GSP 模板的一部分

如你所見,它有一個authAjaxon401

屬性,指定當 AJAX 提交返回 401 狀態碼時應執行的一段 Javascript 程式碼。這段 Javascript 程式碼可以例如顯示一個動態的客戶端登入表單供使用者進行身份驗證。Hubbub 使用外掛使用者指南中提供的客戶端程式碼來實現這一點。注意 外掛的 1.1 版本將自帶一個預設實現authAjax動作。你也可以自定義

ajaxSuccess

auth動作,而不是LoginControllerajaxDenied

動作,以傳送回你想要的任何響應。如你所見,伺服器端的 AJAX 處理是簡單且易於定製的。真正的工作必須在客戶端程式碼中完成。自定義登入表單現在不再流行將整個頁面專門用於登入表單。如今,應用程式更可能擁有一個內容豐富的主頁,並在某個位置設定一個獨立的登入表單,也許只有透過一些 Javascript 特效才能使其可見。提供你自己的專用登入頁面足夠容易(只需隨心編輯Config.groovy:

grails.plugins.springsecurity.auth.loginFormUrl = '/'

LoginController

<form method="POST" action="${resource(file: 'j_spring_security_check')}">
  <table>
    <tr>
      <td>Username:</td><td><g:textField name="j_username"/></td>
    </tr>
    <tr>
      <td>Password:</td><td><input name="j_password" type="password"/></td>
    </tr>
    <tr>
      <td colspan="2"><g:submitButton name="login" value="Login"/></td>
    </tr>
    <tr>
      <td colspan="2">try "glen" or "peter" with "password"</td>
    </tr>
  </table>				
</form>

及其關聯的 GSP 檢視),但是登入面板呢?

  1. 這並沒有你想的那麼難。首先,你需要決定當需要身份驗證時,使用者應該被重定向到哪裡。正如你可能已經知道的,預設情況下是
  2. /login/auth
  3. 。更改這個預設設定就像在
  4. Config.groovy
  5. 中新增一個設定一樣簡單。這一行告訴外掛在需要身份驗證時重定向到主頁。然後你所需要做的就是在主頁上新增一個登入面板。這是一個示例 GSP 表單,可以放在這樣的面板中
這裡的關鍵點是:
grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/'

表單必須使用 POST 方法;

表單必須提交到 <context>/j_spring_security_check;

使用者名稱欄位必須命名為 'j_username';

密碼欄位必須命名為 'j_password';以及

任何“記住我”欄位必須命名為 '_spring_security_remember_me'。

只要滿足這些要求,登入表單就能完美工作。嗯,也不完全完美。如果透過你的登入表單進行身份驗證嘗試失敗,你會被重定向回舊的登入頁面。

幸運的是,透過新增另一個配置設定可以快速糾正這個問題

Config.groovy

這就是實現一個功能齊全的登入表單所需的一切!還有許多其他選項可以用來微調行為,但現在你已經掌握了構建的基礎。

本文實際上只觸及了 Spring Security 外掛的表面。我沒有提及 HTTP Basic 和 Digest Authentication、事件、加鹽密碼等更多內容。這甚至還不包括提供額外功能(例如替代身份驗證機制和訪問控制列表 (ACL))的其他外掛。但你目前所讀到的內容將使你能夠快速建立一個完全可用的訪問控制系統。然後,你就可以根據需要進行擴充套件和自定義,並且知道 Spring Security 擁有比你可能需要的更多的功能。

Config.groovy

訂閱 Spring 新聞通訊

訂閱 Spring 新聞通訊,保持聯絡

訂閱