精通 Grails: 身份验证和授权

保护您的 Grails 应用程序

Grails 提供了组成安全 Web 应用程序所需的所有基本构建模块,包括从简单的登录基础设施到基于角色的授权等各种组件,在本期的 精通 Grails 中,Scott Davis 帮助您通过动手操作保护 Grails 应用程序。您还将了解一些插件,可以帮助您以不同的方式扩展应用程序的安全功能。

Scott Davis, 创始人, ThirstyHead.com

Scott Davis 是国际知名作家、演讲家、软件开发人员。他是 ThirstyHead.com 的创始人,这是一家 Groovy 和 Grails 培训公司。他出版的书籍有 Groovy Recipes: Greasing the Wheels of JavaGIS for Web Developers: Adding Where to Your ApplicationThe Google Maps APIJBoss At Work。他为 IBM developerWorks 撰写两部正在刊发的系列文章:精通 Grails实战 Groovy



2009 年 6 月 18 日

在本文中,我将继续构建一个“微型博客” 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
  }
}

loginpassword 字段已经就绪。您现在只需要提供一个控制器和一个表单。创建 grails-app/controllers/UserController.groovy 并添加如清单 2 所示的代码:

清单 2. 将 loginauthenticatelogout 闭包添加到 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,该 Userloginpassword 匹配表单字段中输入的值,并通过 params hashmap 使用户可用。如果 User 存在的话,将它添加到会话中。如果不存在的话,重定向回登录表单以允许 User 再一次提供正确的凭证。logout 闭包将执行 User 退出,将他或她从会话中删除,然后重定向回 EntryController 中的 list 操作。

现在让我们开始创建 login.gsp。可以手动输入清单 3 中所示的代码,或者可以执行下面的操作:

  1. 在命令行输入 grails generate-views User
  2. 将 create.gsp 复制到 login.gsp。
  3. 简化生成的代码。
清单 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>

注意,表单的 actionauthenticate,它匹配 UserController.groovy 中的闭包的名称。输入元素( loginpassword )中的名称对应于 authenticate 闭包中的 params.loginparams.password

输入 grails run-app 并运行您的身份验证基础设施。尝试使用密码 foojsmith 的身份登录(记住在 “用定制 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 消息
确认成功登录的 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
实际使用 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...
  }
}

authlist 之间微妙但重要的一点区别是 list 是一个闭包,而 auth 是一个私有方法(闭包在定义中使用等号;方法使用圆括号)。闭包以 URI 的形式被公开给最终用户;方法则无法从浏览器中进行访问。

auth 方法将执行检查,查看某个 User 是否在会话中。如果不在的话,它将重定向到登录屏幕并返回 false,阻塞初始的闭包调用。

beforeInterceptor 调用每个闭包之前,auth 方法将得到调用。该操作使用 Groovy 标记来指向 this 类的 auth 方法,该方法使用了 ampersand(&)字符。except 列表包含了应当从 auth 调用中移除的闭包。如果希望拦截一些闭包调用,可以使用 only 替换 except(有关 beforeInterceptor 的更多信息,参见 参考资料)。

重新启动 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 ]
    }
}

您可以(也应该)使用相同的四行代码锁定 deleteupdate 闭包。如果来回复制和粘帖相似代码的工作非常繁琐(并且应当会如此),那么可以创建一个单一的私有方法并在所有三个闭包中调用它。如果发现在许多控制器内使用的是相同的 beforeInterceptor 和私有方法,那么可以将常见的行为解析为单个主控制器,并使用其他控制器扩展它,就像在任何 Java 类中所做的那样。

可以向授权基础设施添加另外一项内容以使它变得更加健壮:角色


添加角色

User 分配角色是一种方便的分组方法。随后可以向组分配权限,而不是向个人分配权限。例如,现在任何人都可以创建一个新的 User。仅仅检查某个用户是否登录还远远不够。我希望限制管理员管理 User 帐户的权限。

清单 9 向 User 添加了一个角色字段以及一条限制,限制 authoradmin 的值:

清单 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 默认值为 authorinList 限制给出了一个复选框,只显示了两个有效选项。图 4 展示了它的实际使用:

图 4. 将新用户角色限制为 authoradmin
将新用户角色限制为 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 的编解码器,就可能打乱数据库中存储的密码,而不是以简洁的形式显示出来(有关 HashCodec 的更多信息,参见 参考资料)。创建 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 之后,只需要在 UserControllerloginsaveupdate 闭包中将对 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 插件提供了一个完整的安全框架,为 UserRolePermission 提供了模板(boilerplate)域类。Spring Security 插件利用了 Spring Security (formerly Acegi Security) 库,允许您重用现有的 Spring Security 知识和源代码。

可以看到,Grails 中可以应用多种身份验证和授权策略,因为应用程序之间的需求是不一样的。通过在功能中设置这些策略,应用程序不可避免地将增加相应的复杂性。在生产应用程序中,我曾使用了这里列出的一些插件,但前提是,必须确保使用插件带来的优点超过了我最早给出的简单的 hand-rolled 策略的好处。


结束语

您现在拥有了一个安全的 Blogito。User 拥有了一种登录和退出方法,以及一个可用于执行这些操作的方便的链接集合,这全部归功于所创建的 LoginTagLib。在某些情况下,只需要登录到应用程序就足够保证安全性了,正如检验身份验证的 EntryController 中的 beforeInterceptor 所展示的那样。对于其他情况,角色让授权更加高级。向 User 添加简单的角色允许将用户管理访问限制为只能由管理员执行。

现在 Blogito 已经具备了安全性,在下一期精通 Grails 文章中,我们将关注目前最主要的任务 — 为通过身份验证的用户提供一种方法来上传文件,以及为最终用户提供一种方法来订阅 Atom 提要。具备了这些功能后,Blogito 将真正成为一个博客应用程序。到那时,请尽情享受精通 Grails 的乐趣吧!

参考资料

学习

  • 精通 Grails:阅读本系列的更多文章,进一步了解 Grails 以及使用它可以实现的所有功能。
  • Grails:访问 Grails Web 站点。
  • Grails Framework Reference Documentation:Grails 宝典。
  • Action Interceptors:进一步了解基于请求、会话或应用程序状态的拦截处理。
  • Simple Dynamic Password Codec:使用这个 Grails 编解码器可以创建单向的散列密码字符串。
  • Groovy Recipes(Scott Davis,Pragmatic Programmers,2008 年):在 Scott Davis 的最新著作中了解有关 Groovy and Grails 的更多信息。
  • 实战 Groovy:这个 developerWorks 系列文章专门探讨了 Groovy 的实际应用,并教您何时以及如何成功地应用它。
  • Groovy:在 Groovy 项目 Web 站点了解有关 Groovy 的更多信息。
  • AboutGroovy.com:获得最新的 Groovy 新闻和文章链接。
  • 技术书店:浏览有关这些主题和其他技术主题的图书。
  • developerWorks Java 技术专区:可以找到几百篇关于 Java 编程的各个方面的文章。

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development, Open source
ArticleID=398218
ArticleTitle=精通 Grails: 身份验证和授权
publish-date=06182009