程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 精通Grails: 身份驗證和授權

精通Grails: 身份驗證和授權

編輯:關於JAVA

在本文中,我將繼續構建一個“微型博客” 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 的樂趣吧!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved