精通 Grails: 身份验证和授权

红薯 发布于 2009/06/20 10:19
阅读 1K+
收藏 7

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

现在可以确定身份验证能够正常工作,应当创建一个 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...
}
}

 

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

在 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 的乐趣吧!

加载中
0
毕志威

没仔细看 但感觉应该不错吧  今天刚发现这个网站 现在开始大搜索了

返回顶部
顶部