使用 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 in Action 中的示例應用程式)。您也可以從這裡獲取完成的專案。

因此,在您的專案內,執行:

    grails install-plugin spring-security-core

如果您檢視外掛安裝生成的輸出,您會發現它提供了一些命令。其中最重要的是:s2-quickstart該命令將幫助您儘可能輕鬆地啟動和執行。它會生成儲存使用者資訊所需的基本域類以及處理身份驗證的控制器。

在執行該命令之前,您可能需要做一個決定。如果您已經有一個 'user' 域類,您將不得不決定如何將其與外掛生成的類整合。一種選擇是替換現有的域類,並簡單地將您的自定義應用到這個替換的類上。另一種方法是讓您自己的域類繼承外掛的類。

哪種方法更好?我更喜歡後一種,因為它允許您在生成的使用者域類模板發生變化時輕鬆更新它。這也意味著您不會過度汙染您的域模型,使其充斥著 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 外掛,實現這一目標非常簡單,儘管您必須決定使用三種機制中的哪一種。您可以採取以控制器為中心的方法,透過註解動作;在以下檔案中使用靜態 URL 規則處理:Config.groovy;或者使用請求對映在資料庫中定義執行時規則。

註解

對於以控制器為中心的方法,您無法超越外掛提供的@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
        }
        ...
    }
}

我們只需將安全服務注入BootStrap,然後使用其encodePassword()方法將明文密碼轉換為其雜湊值。這種方法在您決定更改使用的摘要演算法時特別有效,因為該服務將使用與比較密碼進行身份驗證時相同的演算法來編碼密碼。換句話說,上面的程式碼無論使用哪種演算法都保持不變。

更新:從 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() ]
    }
    ...
}

如您所見,我們只需再次注入安全服務並使用它來獲取主體。除非您建立了UserDetailsService的自定義版本(如果您以前沒有遇到過,請不用擔心),否則主體將是org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser的例項,其id屬性包含相應 'user' 域例項的 ID。

您需要注意的一點是:如果當前使用者是以匿名方式認證的,即他/她未登入且未被記住,則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動作,而不是LoginControllerauth。但是,這仍然是重定向,對嗎?是的,但您可以實現來發送錯誤狀態或渲染 JSON - 基本上是客戶端 Javascript 程式碼可以處理的任何內容。authAjax不幸的是,外掛提供的

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

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>

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

動作的預設實現。authAjax您還可以自定義

ajaxSuccess還是ajaxDenied動作以返回您想要的任何響應。如您所見,伺服器端 AJAX 處理簡單易於定製。實際工作必須在客戶端程式碼中完成。

自定義登入表單

將整個頁面專門用於登入表單已不再流行。如今,應用程式更有可能擁有一個內容豐富的首頁,其中包含一個不顯眼的登入表單,可能只通過一些 Javascript 魔術才可見。提供您自己的專用登入頁面(只需隨意編輯。但是,這仍然是重定向,對嗎?是的,但您可以實現動作,而不是LoginController及其關聯的 GSP 檢視),但這是否很難?那麼登入面板呢?

這並不像您想象的那麼難。首先,您需要決定在需要身份驗證時使用者應該重定向到哪裡。您可能已經猜到,預設情況下是/login/auth。更改此預設值非常容易,只需在以下檔案中新增一個設定:Config.groovy:

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

此行告訴外掛在需要身份驗證時將使用者重定向到首頁。然後,您只需將登入面板新增到首頁即可。以下是一個 GSP 表單示例,可用於此類面板:

<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>

這裡的關鍵點是:

  1. 表單必須使用 POST 方法;
  2. 表單必須提交到 <context>/j_spring_security_check;
  3. 使用者名稱欄位的名稱必須是 'j_username';
  4. 密碼欄位的名稱必須是 'j_password';以及
  5. "記住我"欄位的名稱必須是 '_spring_security_remember_me'。
只要滿足這些要求,登入表單就能完美執行。嗯,不完全是。如果透過您的登入表單進行身份驗證嘗試失敗,您將發現自己被重定向回舊的登入頁面。幸運的是,可以透過新增另一個配置設定快速解決此問題:
grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/'

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

本文真的只是觸及了 Spring Security 外掛的表面。我沒有提及 HTTP Basic 和 Digest 身份驗證、事件、帶鹽密碼等。這甚至還不包括提供額外功能的其他外掛,例如替代的身份驗證機制和訪問控制列表(ACL)。但是,您到目前為止所讀到的內容將使您能夠立即建立一個功能齊全的訪問控制系統。然後,您可以根據需要進行擴充套件和自定義,因為 Spring Security 提供的功能比您可能需要的還要多。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有