在本文中,我將繼續構建一個“微型博客” Blogito。我刪除了此前文章(“用定制 URI 和 codec 優化 Grails 中的 URI”)中的 User,因為 name 字段是 URI 的重要組成部分。這一次我們將實現完整 的 User 子系統。您將理解到如何根據 User 是否登錄啟用登錄、限制用戶行為,甚至根據 User 的角色 添加一些授權。
首先,User 需要一種登錄方式,從而能夠發布新的條目。
身份驗證
對於支持多個用戶的博客服務器來說,進行身份驗證是個好主意。您肯定不希望 John Doe 以 Jane Smith 的身份發布博客條目,不管是有意還是無意。設置身份驗證基礎設施將回答這個問題:“您是誰? ”,稍後,您還將添加一些授權機制。授權將回答關於 “允許您做什麼” 的問題。
清單 1 展示了您在 在上一篇文章 中創建的 grails-app/domain/User.groovy 文件:
清單 1. User 類
class User {
static constraints = {
login(unique:true)
password(password:true)
name()
}
static hasMany = [entries:Entry]
String login
String password
String name
String toString(){
name
}
}
login 和 password 字段已經就緒。您現在只需要提供一個控制器和一個表單。創建 grails- app/controllers/UserController.groovy 並添加如清單 2 所示的代碼:
清單 2. 將 login、authenticate 和 logout 閉包添加到 UserController
class UserController {
def scaffold = User
def login = {}
def authenticate = {
def user = User.findByLoginAndPassword(params.login, params.password)
if(user){
session.user = user
flash.message = "Hello ${user.name}!"
redirect(controller:"entry", action:"list")
}else{
flash.message = "Sorry, ${params.login}. Please try again."
redirect(action:"login")
}
}
def logout = {
flash.message = "Goodbye ${session.user.name}"
session.user = null
redirect(controller:"entry", action:"list")
}
}
空的 login 閉包僅僅表示在您的浏覽器中訪問 http://localhost:9090/blogito/user/login 將呈現 grails-app/views/user/login.gsp 文件(您稍後即將創建該文件)。
authenticate 閉包使用了一個方便的 GORM 方法(findByLoginAndPassword() )執行需要的操作: 在數據庫中查找 User,該 User 的 login 和 password 匹配表單字段中輸入的值,並通過 params hashmap 使用戶可用。如果 User 存在的話,將它添加到會話中。如果不存在的話,重定向回登錄表單以 允許 User 再一次提供正確的憑證。logout 閉包將執行 User 退出,將他或她從會話中刪除,然後重定 向回 EntryController 中的 list 操作。
現在讓我們開始創建 login.gsp。可以手動輸入清單 3 中所示的代碼,或者可以執行下面的操作:
在命令行輸入 grails generate-views User。
將 create.gsp 復制到 login.gsp。
簡化生成的代碼。
清單 3. login.gsp
<html>
<head>
<meta name="layout" content="main" />
<title>Login</title>
</head>
<body>
<div class="body">
<h1>Login</h1>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<g:form action="authenticate" method="post" >
<div class="dialog">
<table>
<tbody>
<tr class="prop">
<td class="name">
<label for="login">Login:</label>
</td>
<td>
<input type="text" id="login" name="login"/>
</td>
</tr>
<tr class="prop">
<td class="name">
<label for="password">Password:</label>
</td>
<td>
<input type="password" id="password" name="password"/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="buttons">
<span class="button">
<input class="save" type="submit" value="Login" />
</span>
</div>
</g:form>
</div>
</body>
</html>
注意,表單的 action 是 authenticate,它匹配 UserController.groovy 中的閉包的名稱。輸入元 素( login 和 password )中的名稱對應於 authenticate 閉包中的 params.login 和 params.password。
輸入 grails run-app 並運行您的身份驗證基礎設施。嘗試使用密碼 foo 以 jsmith 的身份登錄(記 住在 “用定制 URI 和 codec 優化 Grails 中的 URI” 中,您在 grails-app/conf/BootStrap.groovy 中為 Blogito 提供了一些用戶)。您的登錄將失敗,如圖 1 所示:
圖 1. 失敗的登錄嘗試,顯示錯誤消息
再次以 jsmith 的身份和密碼 wordpass 嘗試登錄。這一次應當成功。
如果歡迎消息沒有出現在 grails-app/views/entry/list.gsp 中 — 並且它不應該出現 — 那麼只需 將 <g:if test="${flash.message}"> 塊從 login.gsp 復制到 list.gsp 文件的頂部。再次以 jsmith 身份登錄,檢驗現在是否顯示了如圖 2 所示的消息:
圖 2. 確認成功登錄的 Flash 消息
現在可以確定身份驗證能夠正常工作,應當創建一個 TagLib 來簡化登錄和退出。
創建一個身份驗證 TagLib
像 Google 和 Amazon 這樣的 Web 站點在標題處提供了一個不太顯眼的文本鏈接,允許您登錄和退出 。您只需要幾行代碼就可以在 Grails 中實現這一點。
首先,在命令提示下輸入 grails create-tag-lib Login。將清單 4 中的代碼添加到新創建的 grails-app/taglib/LoginTagLib.groovy 中:
清單 4. LoginTagLib.groovy
class LoginTagLib {
def loginControl = {
if(session.user){
out << "Hello ${session.user.name} "
out << """[${link(action:"logout", controller:"user"){"Logout"}}]"""
} else {
out << """[${link(action:"login", controller:"user"){"Login"}}]"""
}
}
}
現在,將新的 <g:loginControl> 標記添加到 grails-app/views/layouts/_header.gsp,如清 單 5 所示:
清單 5. 將 <loginControl> 標記添加到標題
<div id="header">
<p><g:link class="header-main" controller="entry">Blogito</g:link></p>
<p class="header-sub">A tiny little blog</p>
<div id="loginHeader">
<g:loginControl />
</div>
</div>
最後,將針對 loginHeader <div> 的一些 CSS 格式添加到 web-app/css/main.css,如清單 6 所示:
清單 6. loginHeader <div> 的 CSS 格式
#loginHeader {
float: right;
color: #fff;
}
重啟 Grails 並以 jsmith 身份登錄後,屏幕應該如圖 3 所示:
圖 3. 實際使用 Login TagLib
基本授權
現在 Blogito 已經實現了身份驗證,接下來是限制您所能執行的操作。例如,任何人都應當能夠讀取 Entry,但是只有登錄用戶能夠創建、更新和刪除 Entry。要達到這個目的,Grails 提供了一個 beforeInterceptor,顧名思義,它為您提供一個鉤子,可以在調用目標閉包之前對行為進行授權。
將清單 7 中的代碼添加到 EntryController:
清單 7. 向 EntryController 添加授權
class EntryController {
def beforeInterceptor = [action:this.&auth, except:["index", "list", "show"]]
def auth() {
if(!session.user) {
redirect(controller:"user", action:"login")
return false
}
}
def list = {
//snip...
}
}
auth 和 list 之間微妙但重要的一點區別是 list 是一個閉包,而 auth 是一個私有方法(閉包在定 義中使用等號;方法使用圓括號)。閉包以 URI 的形式被公開給最終用戶;方法則無法從浏覽器中進行 訪問。
auth 方法將執行檢查,查看某個 User 是否在會話中。如果不在的話,它將重定向到登錄屏幕並返回 false,阻塞初始的閉包調用。
在 beforeInterceptor 調用每個閉包之前,auth 方法將得到調用。該操作使用 Groovy 標記來指向 this 類的 auth 方法,該方法使用了 ampersand(&)字符。except 列表包含了應當從 auth 調用 中移除的閉包。如果希望攔截一些閉包調用,可以使用 only 替換 except。
重新啟動 Grails 並測試 beforeInterceptor。嘗試在未登錄的情況下訪問 http://localhost:9090/blogito/entry/create。您應當被重定向到登錄屏幕。以 jsmith 身份登錄並重 新嘗試。這一次您應當能夠成功創建新的 Entry。
細粒度授權
beforeInterceptor 提供的粗粒度授權僅僅是個開始,但是也可以向單獨的閉包添加授權鉤子。例如 ,任何已登錄的 User(不僅僅是初始創建者)都可以編輯任何 Entry。可以關閉安全漏洞:將 4 行良好 布置的代碼添加到 EntryController.groovy 中的 edit 閉包中,如清單 8 所示:
清單 8. 向 edit 閉包添加授權
def edit = {
def entryInstance = Entry.get( params.id )
//limit editing to the original author
if( !(session.user.login == entryInstance.author.login) ){
flash.message = "Sorry, you can only edit your own entries."
redirect(action:list)
}
if(!entryInstance) {
flash.message = "Entry not found with id ${params.id}"
redirect(action:list)
}
else {
return [ entryInstance : entryInstance ]
}
}
您可以(也應該)使用相同的四行代碼鎖定 delete 和 update 閉包。如果來回復制和粘帖相似代碼 的工作非常繁瑣(並且應當會如此),那麼可以創建一個單一的私有方法並在所有三個閉包中調用它。如 果發現在許多控制器內使用的是相同的 beforeInterceptor 和私有方法,那麼可以將常見的行為解析為 單個主控制器,並使用其他控制器擴展它,就像在任何 Java 類中所做的那樣。
可以向授權基礎設施添加另外一項內容以使它變得更加健壯:角色
添加角色
為 User 分配角色是一種方便的分組方法。隨後可以向組分配權限,而不是向個人分配權限。例如, 現在任何人都可以創建一個新的 User。僅僅檢查某個用戶是否登錄還遠遠不夠。我希望限制管理員管理 User 帳戶的權限。
清單 9 向 User 添加了一個角色字段以及一條限制,限制 author 或 admin 的值:
清單 9. 向 User 添加一個角色字段
class User {
static constraints = {
login(unique:true)
password(password:true)
name()
role(inList:["author", "admin"])
}
static hasMany = [entries:Entry]
String login
String password
String name
String role = "author"
String toString(){
name
}
}
注意,role 默認值為 author。inList 限制給出了一個復選框,只顯示了兩個有效選項。圖 4 展示 了它的實際使用:
圖 4. 將新用戶角色限制為 author 或 admin
在 grails-app/conf/BootStrap.groovy 中創建一個 admin User,如清單 10 所示。不要忘記將 author role 添加到兩個現有的 User 中。
清單 10. 添加一個 admin User
import grails.util.GrailsUtil
class BootStrap {
def init = { servletContext ->
switch(GrailsUtil.environment){
case "development":
def admin = new User(login:"admin",
password:"password",
name:"Administrator",
role:"admin")
admin.save()
def jdoe = new User(login:"jdoe",
password:"password",
name:"John Doe",
role:"author")
//snip...
def jsmith = new User(login:"jsmith",
password:"wordpass",
name:"Jane Smith",
role:"author")
//snip...
break
case "production":
break
}
}
def destroy = {
}
}
最後,添加清單 11 中的代碼,將所有 User 帳戶活動限制為只有擁有 admin 角色的人員才能執行:
清單 11. 將 User 帳戶管理限制為只有擁有 admin 角色的人員才能執行
class UserController {
def beforeInterceptor = [action:this.&auth,
except:["login", "authenticate", "logout"]]
def auth() {
if( !(session?.user?.role == "admin") ){
flash.message = "You must be an administrator to perform that task."
redirect(action:"login")
return false
}
}
//snip...
}
要測試基於角色的授權,以 jsmith 身份登錄並隨後嘗試訪問 http://localhost:9090/blogito/user/create。應當被重定向到登錄屏幕,如圖 5 所示:
圖 5. 阻塞非管理員訪問
現在以 admin 用戶的身份登錄。應當能夠訪問所有的閉包。
使用插件實現更高級功能
這個 “微型” 博客應用程序的 “微型” 身份驗證和授權系統現在已經初具雛形。您可以輕松地對 它進行擴展。也許您希望 User 能夠管理他們各自的帳戶,而不是其他人的。也許 admin 應當具備編輯 所有 Entries 的能力,而不僅僅是編輯他們自己的。在這些情況下,只需要策略性地放置幾行代碼就可 以添加新的功能。
人們常常將簡潔性誤解為缺乏功能。Blogito 仍然不足 200 行代碼 — 並且這還包含了單元和集成測 試。在命令行輸入 grails stats 以確認這點。結果如清單 12 所示。但是 Blogito 不復雜並不表示它 的功能不完備。
清單 12. “微型” 應用程序的大小
$ grails stats
+----------------------+-------+-------+
| Name | Files | LOC |
+----------------------+-------+-------+
| Controllers | 2 | 95 |
| Domain Classes | 2 | 32 |
| Tag Libraries | 2 | 21 |
| Unit Tests | 5 | 20 |
| Integration Tests | 1 | 10 |
+----------------------+-------+-------+
| Totals | 12 | 178 |
+----------------------+-------+-------+
從本系列的第一篇文章開始,我的目標就是向您展示核心 Grails 與生俱來的強大功能,以及 Groovy 語言的簡潔的表達能力。例如,一旦理解了 Grails 的編解碼器,就可能打亂數據庫中存儲的密碼,而不 是以簡潔的形式顯示出來。創建 grails-app/utils/HashCodec.groovy 並添加清單 13 中的代碼:
清單 13. 創建一個簡單的 HashCodec
import java.security.MessageDigest
import sun.misc.BASE64Encoder
import sun.misc.CharacterEncoder
class HashCodec {
static encode = { str ->
MessageDigest md = MessageDigest.getInstance('SHA')
md.update(str.getBytes('UTF-8'))
return (new BASE64Encoder()).encode(md.digest())
}
}
有了 HashCodec 之後,只需要在 UserController 的 login、save 和 update 閉包中將對 User.password 的引用修改為 User.password.encodeAsHash()。令人驚訝的是,只需要 10 行代碼,您 讓應用程序變得更高級。
但是,有時並不是增加代碼就能獲得回報。對於 Grails 中,典型的 “構建還是購買” 問題變成了 “構建還是下載插件”。http://grails.org/plugin/list#security+tags 中的一些插件試圖解決身份驗 證和授權挑戰,使用了與 grails install-plugin 不同的方法。
比如,Authentication 插件提供了一些非常不錯的特性,例如允許 User 注冊一個帳戶,而不是要求 admin 為他們創建帳戶。隨後可以配置此插件,向 User 發送一條確認消息,表示 “使用這個電子郵件 地址創建了一個新的用戶帳戶。單擊此鏈接將驗證您的新帳戶”。
OpenID 插件則采取不同的方法。您的最終用戶不需要創建另一個用戶名和密碼組合(他們肯定會遺忘 ),身份驗證被委托給他們選擇的 OpenID 提供商。Lightweight Directory Access Protocol (LDAP) 插件采用了類似地方法,允許您的 Grails 應用程序利用現有的 LDAP 基礎設施。
Authentication 和 OpenID 插件只提供身份驗證功能。其他插件還提供了授權解決方案。JSecurity 插件提供了一個完整的安全框架,為 User、Role 和 Permission 提供了模板(boilerplate)域類。 Spring Security 插件利用了 Spring Security (formerly Acegi Security) 庫,允許您重用現有的 Spring Security 知識和源代碼。
可以看到,Grails 中可以應用多種身份驗證和授權策略,因為應用程序之間的需求是不一樣的。通過 在功能中設置這些策略,應用程序不可避免地將增加相應的復雜性。在生產應用程序中,我曾使用了這裡 列出的一些插件,但前提是,必須確保使用插件帶來的優點超過了我最早給出的簡單的 hand-rolled 策 略的好處。
結束語
您現在擁有了一個安全的 Blogito。User 擁有了一種登錄和退出方法,以及一個可用於執行這些操作 的方便的鏈接集合,這全部歸功於所創建的 LoginTagLib。在某些情況下,只需要登錄到應用程序就足夠 保證安全性了,正如檢驗身份驗證的 EntryController 中的 beforeInterceptor 所展示的那樣。對於其 他情況,角色讓授權更加高級。向 User 添加簡單的角色允許將用戶管理訪問限制為只能由管理員執行。
現在 Blogito 已經具備了安全性,在下一期精通 Grails 文章中,我們將關注目前最主要的任務 — 為通過身份驗證的用戶提供一種方法來上傳文件,以及為最終用戶提供一種方法來訂閱 Atom 提要。具備 了這些功能後,Blogito 將真正成為一個博客應用程序。到那時,請盡情享受精通 Grails 的樂趣吧!