基本授权
现在 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(有关 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 ] } } |
您可以(也应该)使用相同的四行代码锁定 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 | +----------------------+-------+-------+ |