使用 Grails 简化 Spring Security

工程 | Peter Ledbrook | 2010年8月11日 | ...

Spring Security 是一个功能强大的库,用于保护你的应用程序,提供了令人眼花缭乱的选项。基于 Spring,它可以轻松集成到 Grails 应用程序中。但为什么不省去麻烦,使用新的改进版 Grails 插件呢?

该插件经历了从 Acegi 插件开始的几个演进阶段。它最近一次的迭代是针对 Spring Security 3 和 Spring 3 的完全重写。其中一个结果是该插件只能与 Grails 1.2.2 及更高版本一起使用。另一个重大变化是 Spring Security 插件不再是单一的:一些功能已被拆分为可选插件。因此,现在你只需在应用程序中包含所需的功能即可。

那么这些插件为你提供了什么?核心插件提供基于用户和角色的、易于使用的访问控制基础功能。实际上,许多应用程序除了核心插件之外不需要任何其他插件。对于那些需要额外功能的开发者,下面列出了该家族的其他插件:

  • OpenID - 使用 OpenID 进行身份验证
  • LDAP - 针对 LDAP 服务器进行身份验证
  • CAS - 使用 CAS 进行单点登录
  • ACLs - 通过 Spring Security 的 ACLs 进行访问控制
  • UI - 用于用户和角色管理的界面,以及其他功能

在本文中,我将向你展示如何使用新的核心插件从零开始保护 Grails 应用程序。

更新 本文现在有两个配套截屏视频

[caption id="attachment_5509" align="center" width="250" caption="Spring Security 插件介绍"][/caption] [caption id="attachment_5510" align="center" width="250" caption="Spring Security 插件 - AJAX"][/caption]

设置

与大多数插件一样,你的第一步是安装 Spring Security 插件。当然,你需要一个项目来安装它,对于本文,我提供了一个一个简单的 Twitter 克隆应用 Hubbub(基于《Grails 实战》中的示例应用)。你也可以从这里获取已完成的项目。

因此,在你的项目内部,运行

    grails install-plugin spring-security-core

如果你查看插件安装生成的输出,你会发现它提供了几个命令。其中最重要的是s2-quickstart,它将帮助你快速轻松地开始。它生成了存储用户信息所需的基本领域类以及处理身份验证的控制器。

在运行命令之前,你可能需要做一个决定。如果你已经有一个“用户”领域类,你将不得不决定如何将其与插件生成的领域类集成。一种选择是替换现有的领域类,然后简单地将你的自定义应用于替换类。另一种方法是让你的领域类继承插件生成的领域类。

哪种方法更好?我更喜欢后者,因为它允许你在生成的用户领域类模板发生变化时轻松更新它。这也意味着你不会用 Spring Security 的特定内容过度污染你的领域模型。缺点是你必须处理领域类的继承,但这方面的代价非常小。

对于 Hubbub,我们将让用户领域类继承生成的领域类,这意味着我们应该使用与现有领域类不冲突的名称

    grails s2-quickstart org.example SecUser SecRole

这将为我们创建三个领域类

  • org.example.SecUser
  • org.example.SecRole
  • org.example.SecUserSecRole- 将用户链接到角色
以及两个控制器
  • LoginController
  • LogoutController
以及它们关联的视图。只需两个命令,我们就拥有了开始保护应用程序所需的一切!

示例应用程序还需要一项更改:其 URL 映射意味着无法访问登录和注销控制器。通过将以下两行添加到UrlMappings.groovy:

"/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")

可以轻松解决这个问题。如果你不进行此更改,登录页面将产生 404 错误!现在,让我们开始保护应用程序。

添加访问控制

这项工作的重点是限制对应用程序某些部分的访问。对于 Web 应用程序,这通常意味着保护特定页面,或者更具体地说,是 URL。对于 Hubbub,我们有以下要求:

  • 主页所有人都可以访问 -/
  • 只有已知用户才能看到特定用户的帖子 -/person/<username>
  • 只有具有“user”角色的用户才能访问他们的时间线 -/timeline
  • 关注另一个用户也是如此 -/post/followAjax
  • 只有完全认证且具有“user”角色的用户才能发布新消息 -/post/addPostAjax

使用 Spring Security 插件实现这一点非常简单,尽管你必须决定使用三种机制中的哪一种。你可以采用以控制器为中心的方法并注解动作;在Config.groovy中使用静态 URL 规则;或者使用请求映射在数据库中定义运行时规则。

注解

对于以控制器为中心的方法,@Secured插件提供的注解是无与伦比的。在其最简单的形式中,你向它传递一个定义哪些用户可以访问相应动作的基本规则列表。在这里,我通过在 post 控制器上使用注解来应用 Hubbub 的访问控制规则

package org.example

import grails.plugins.springsecurity.Secured

class PostController {
    ...
    @Secured(['ROLE_USER'])
    def followAjax = { ... }

    @Secured(['ROLE_USER', 'IS_AUTHENTICATED_FULLY'])
    def addPostAjax = { ... }

    def global = { ... }

    @Secured(['ROLE_USER'])
    def timeline = { ... }

    @Secured(['IS_AUTHENTICATED_REMEMBERED'])
    def personal = { ... }
}

IS_AUTHENTICATED_*规则内置于 Spring Security 中,但ROLE_USER是一个必须存在于数据库中的角色——我们尚未完成这项工作。此外,如果在列表中指定多个规则,则当前用户通常只需满足其中一个即可——用户指南中对此有解释。IS_AUTHENTICATED_FULLY是一个特例:如果指定,则必须满足它以及列表中的其他规则。

内置规则如下:

  • IS_AUTHENTICATED_ANONYMOUSLY - 任何人都可以访问;用户无需登录
  • IS_AUTHENTICATED_REMEMBERED - 只有已登录或从先前会话中记住的已知用户才能访问
  • IS_AUTHENTICATED_FULLY - 用户必须登录才能访问,即使他们上次勾选了“记住我”
前两个规则区分已知用户和未知用户,已知用户是在“user”数据库表中有条目的用户。最后一个通常应用于用户访问特别敏感信息(例如银行账户或信用卡数据)的情况。毕竟,其他人可能正在使用前一个用户的“记住我” cookie 访问你的应用程序。

你也可以将注解应用于控制器类本身,这将导致所有动作继承其定义的规则。如果一个动作有自己的注解,则该注解会覆盖类级别的注解。注解也不仅限于像这样的规则列表:查阅用户指南,了解如何使用表达式提供对规则的更大控制。

静态 URL 规则

如果你不喜欢注解,你可以通过在Config.groovy中定义一个静态映射来定义访问控制规则。如果你喜欢将规则集中存放,这是一个理想选择。以下是如何使用此机制定义 Hubbub 的规则:

import grails.plugins.springsecurity.SecurityConfigType
...
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.InterceptUrlMap
grails.plugins.springsecurity.interceptUrlMap = [
    '/timeline':         ['ROLE_USER'],
    '/person/*':         ['IS_AUTHENTICATED_REMEMBERED'],
    '/post/followAjax':  ['ROLE_USER'],
    '/post/addPostAjax': ['ROLE_USER', 'IS_AUTHENTICATED_FULLY'],
    '/**':               ['IS_AUTHENTICATED_ANONYMOUSLY']
]

注意最通用的规则放在最后了吗?那是因为顺序很重要:Spring Security 会遍历规则并应用第一个与当前 URL 匹配的规则。因此,如果 '/**' 规则放在最前面,你的应用程序将实际上不受保护,因为所有 URL 都会匹配到它。另外请注意,你必须通过grails.plugins.springsecurity.securityConfigType设置明确告知插件使用该映射。

动态请求映射

你想在运行时更新 URL 规则而无需重启应用程序吗?如果是这样,你可能需要使用请求映射,它本质上是存储在数据库中的 URL 规则。要启用此机制,请将以下内容添加到Config.groovy:

import grails.plugins.springsecurity.SecurityConfigType
...
grails.plugins.springsecurity.securityConfigType = SecurityConfigType.Requestmap

然后你只需创建Requestmap领域类的实例,例如在BootStrap.groovy:

new Requestmap(url: '/timeline', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/person/*', configAttribute: 'IS_AUTHENTICATED_REMEMBERED').save()
new Requestmap(url: '/post/followAjax', configAttribute: 'ROLE_USER').save()
new Requestmap(url: '/post/addPostAjax', configAttribute: 'ROLE_USER,IS_AUTHENTICATED_FULLY').save()
new Requestmap(url: '/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()

中。当然,这种方法会带来性能开销,因为它涉及到数据库,但通过使用缓存可以将其最小化。查阅用户指南,了解更多相关信息。此外,在这种情况下,你不必担心规则的顺序,因为插件会选择与当前 URL 匹配的最具体的 URL 模式。

你应该使用哪种方法?这取决于你的应用程序设置以及你如何看待访问控制。当规则按控制器应用且控制器具有不同的 URL 时,注解是合理的选择。如果你倾向于将控制器分组到单个 URL 下,例如/admin/,或者你只是喜欢将所有规则集中存放,那么最好使用在Config.groovy中定义的静态规则。第三种机制,请求映射,只有在你希望在运行时添加、更改或删除规则时才有意义。一个经典的例子是在 CMS 应用程序中,其中 URL 本身是动态定义的。

无论你采用哪种方法,一旦规则实施,你的应用程序受到保护。例如,如果你此时尝试访问 Hubbub 中的/timeline页面,你将被重定向到标准登录页面

太好了!但你要以谁的身份登录呢?用户如何注销?保护你的页面只是第一步。你还需要确保拥有相关的安全数据(用户和角色)以及一个具备安全意识的用户界面。

下一步

建立了访问控制后,你需要考虑用户体验。你真的想让用户点击他们没有访问权限的链接吗?你在访问控制中使用的那些角色呢?它们何时创建?现在让我们回答这些问题。

安全数据

有些应用程序只关心用户是否已知,在这种情况下,你无需担心角色,因为IS_AUTHENTICATED_*规则就足够了。但如果你的应用程序需要更精细地控制谁访问什么,你就需要角色。这些通常在应用程序生命周期早期定义,并对应于不变的参考数据。这使得BootStrap成为创建它们的理想位置。对于 Hubbub,我们像这样添加“user”和“admin”角色:

import org.example.SecRole

class BootStrap {
    def init = {
        ...
        def userRole = SecRole.findByAuthority('ROLE_USER') ?: new SecRole(authority: 'ROLE_USER').save(failOnError: true)
        def adminRole = SecRole.findByAuthority('ROLE_ADMIN') ?: new SecRole(authority: 'ROLE_ADMIN').save(failOnError: true)
        ...
    }
}

当然,如果数据已经存在,我们不想重新创建它,这就是我们使用findByAuthority().

的原因。添加用户也同样简单,但有几个注意事项需要记住。首先,生成的“user”领域类有一个enabled属性,默认情况下是false。如果你不明确将其初始化为true,相应的用户将无法登录。其次,密码很少以明文形式存储在数据库中,因此你需要先使用适当的摘要算法对其进行编码。

幸运的是,该插件提供了一个有用的服务来帮助解决此问题SpringSecurityService。假设我们想在 Hubbub 的BootStrap中创建一个“admin”用户。代码如下:

import org.example.*

class BootStrap {
    def springSecurityService

    def init = {
        ...
        def adminUser = SecUser.findByUsername('admin') ?: new SecUser(
                username: 'admin',
                password: springSecurityService.encodePassword('admin'),
                enabled: true).save(failOnError: true)

        if (!adminUser.authorities.contains(adminRole)) {
            SecUserSecRole.create adminUser, adminRole
        }
        ...
    }
}

我们只需将安全服务注入BootStrapBootStrap,然后使用其方法将明文密码转换为其哈希值。当你决定更改使用的摘要算法时,这种方法尤其有效,因为该服务将使用与身份验证比较时使用的算法相同的算法来编码密码。换句话说,无论使用何种算法,上述代码都保持不变。

更新 从 Spring Security Core 插件的 1.2 版本开始,生成的User类在实例保存时会自动编码密码。因此,你不再需要显式使用SpringSecurityService.encodePassword()

创建用户后,我们检查它是否具有“admin”角色,如果没有,则将该角色分配给用户。我们通过生成的SecUserSecRole类及其create()方法来实现。

安全数据到位,并且知道如何在需要时按需创建它,现在是时候让用户界面感知身份验证、用户和角色了。

用户界面

我在这里想看看 UI 的两个方面:显示特定于用户的信息,并确保用户只能看到他被允许看到的内容。第一个问题归结为一个问题:我们如何获取当前登录用户的“user”领域实例?考虑 Hubbub 的时间线页面,它显示当前用户正在关注的人的所有帖子

class PostController {
    def springSecurityService
    ...
    @Secured(['ROLE_USER'])
    def timeline = {
        def user = SecUser.get(springSecurityService.principal.id)

        def posts = []
        if (user.following) {
            posts = Post.withCriteria {
                'in'("user", user.following)
                order("createdOn", "desc")
            }
        }
        [ posts: posts, postCount: posts.size() ]
    }
    ...
}

如你所见,我们所需要做的就是再次注入安全服务并使用它来获取 principal。除非你创建了UserDetailsService的自定义版本(如果你以前没有遇到过,不必担心),principal 将是org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser的一个实例,其id属性包含相应“user”领域实例的 ID。

你需要注意一点:如果当前用户是匿名认证的,也就是说他没有登录且没有被记住,则principal属性将返回一个字符串。因此,如果某个动作可以由未认证用户访问,请确保在使用 principal 之前检查其类型!

如何确保用户只能看到他们应该看到的内容?为此,插件在sec命名空间中提供了一组丰富的 GSP 标签。假设我们想为 Hubbub 添加几个导航链接,但我们只希望在用户未登录时显示其中一个,而另一个只有在用户具有ROLE_USER角色时才显示。

<sec:ifNotLoggedIn>
  <g:link controller="login" action="auth">Login</g:link>
</sec:ifNotLoggedIn>
<sec:ifAllGranted roles="ROLE_USER">
  <g:link class="create" controller="post" action="timeline">My Timeline</g:link>
</sec:ifAllGranted>

<sec:if*>标签内的标记只有在条件满足时才会渲染到页面。插件提供了几个其他类似的标签,它们的行为都一致。查阅用户指南了解更多信息。

上例还向你展示了如何创建指向登录页面的链接。允许用户注销也同样简单。Hubbub 提供了一个侧边面板,其中显示了登录用户的姓名以及注销链接等信息

<sec:username /> (<g:link controller="logout">sign out</g:link>)

简单!这些标签和安全服务的结合足以将你的用户界面与 Spring Security 集成。只需记住保持你的用户界面元素与访问控制规则同步:你不想让某些 UI 部分可见,而这会导致“未经授权的用户”错误。

我现在已经介绍了 Spring Security 插件的所有基本元素,但仍然有两个将影响大量用户的特性:AJAX 请求和自定义登录表单。

最后的拼图块

现在有多少 Web 应用程序没有在一定程度上使用 AJAX?又有多少应用程序真正想使用默认的登录表单?对于内部使用来说没问题,但我不建议将其用于面向客户的应用。让我们从 AJAX 开始。

保护 AJAX 请求

基于 AJAX 的动态用户界面给访问控制带来了一系列新问题。处理需要身份验证的标准请求非常容易:只需将用户重定向到登录页面,然后在身份验证成功后将其重定向回目标页面。但这种重定向对于 AJAX 来说效果不佳。那么该怎么办呢?

插件提供了一种处理 AJAX 请求不同于普通请求的方式。当 AJAX 请求需要身份验证时,Spring Security 会重定向到authAjax动作,而不是LoginControllerLoginControllerauth。但是等等,那仍然是一个重定向对吧?是的,但你可以实现authAjaxauthAjax

动作在控制器中发送错误状态或渲染 JSON——基本上客户端 Javascript 代码可以处理的任何内容。LoginController不幸的是,authAjaxLoginController

import javax.servlet.http.HttpServletResponse

class LoginController {
    ...
    def authAjax = {
        response.sendError HttpServletResponse.SC_UNAUTHORIZED
    }
    ...
}

插件提供的

<g:form action="ajaxAdd">
    <g:textArea id='postContent' name="content" rows="3" cols="50" onkeydown="updateCounter()" /><br/>
    <g:submitToRemote value="Post"
                 url="[controller: 'post', action: 'addPostAjax']"
                 update="[success: 'firstPost']"
                 onSuccess="clearPost(e)"
                 onLoading="showSpinner(true)"
                 onComplete="showSpinner(false)"
                 on401="showLogin();"/>
</g:form>

authAjax动作目前尚未实现,因此你需要自己添加它这是一个非常简单的实现,返回 401 HTTP 状态码。我们如何处理这样的响应?这取决于你在浏览器中用于实现 AJAX 的方式。示例 Hubbub 应用程序使用自适应 AJAX 标签,因此我将以此为例来演示你可以做的事情。这是用于发布新消息的 GSP 模板的一部分

如你所见,它有一个authAjaxon401

属性,指定当 AJAX 提交返回 401 状态码时应执行的一段 Javascript 代码。这段 Javascript 代码可以例如显示一个动态的客户端登录表单供用户进行身份验证。Hubbub 使用插件用户指南中提供的客户端代码来实现这一点。注意 插件的 1.1 版本将自带一个默认实现authAjax动作。你也可以自定义

ajaxSuccess

auth动作,而不是LoginControllerajaxDenied

动作,以发送回你想要的任何响应。如你所见,服务器端的 AJAX 处理是简单且易于定制的。真正的工作必须在客户端代码中完成。自定义登录表单现在不再流行将整个页面专门用于登录表单。如今,应用程序更可能拥有一个内容丰富的主页,并在某个位置设置一个独立的登录表单,也许只有通过一些 Javascript 特效才能使其可见。提供你自己的专用登录页面足够容易(只需随心编辑Config.groovy:

grails.plugins.springsecurity.auth.loginFormUrl = '/'

LoginController

<form method="POST" action="${resource(file: 'j_spring_security_check')}">
  <table>
    <tr>
      <td>Username:</td><td><g:textField name="j_username"/></td>
    </tr>
    <tr>
      <td>Password:</td><td><input name="j_password" type="password"/></td>
    </tr>
    <tr>
      <td colspan="2"><g:submitButton name="login" value="Login"/></td>
    </tr>
    <tr>
      <td colspan="2">try "glen" or "peter" with "password"</td>
    </tr>
  </table>				
</form>

及其关联的 GSP 视图),但是登录面板呢?

  1. 这并没有你想的那么难。首先,你需要决定当需要身份验证时,用户应该被重定向到哪里。正如你可能已经知道的,默认情况下是
  2. /login/auth
  3. 。更改这个默认设置就像在
  4. Config.groovy
  5. 中添加一个设置一样简单。这一行告诉插件在需要身份验证时重定向到主页。然后你所需要做的就是在主页上添加一个登录面板。这是一个示例 GSP 表单,可以放在这样的面板中
这里的关键点是:
grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/'

表单必须使用 POST 方法;

表单必须提交到 <context>/j_spring_security_check;

用户名字段必须命名为 'j_username';

密码字段必须命名为 'j_password';以及

任何“记住我”字段必须命名为 '_spring_security_remember_me'。

只要满足这些要求,登录表单就能完美工作。嗯,也不完全完美。如果通过你的登录表单进行身份验证尝试失败,你会被重定向回旧的登录页面。

幸运的是,通过添加另一个配置设置可以快速纠正这个问题

Config.groovy

这就是实现一个功能齐全的登录表单所需的一切!还有许多其他选项可以用来微调行为,但现在你已经掌握了构建的基础。

本文实际上只触及了 Spring Security 插件的表面。我没有提及 HTTP Basic 和 Digest Authentication、事件、加盐密码等更多内容。这甚至还不包括提供额外功能(例如替代身份验证机制和访问控制列表 (ACL))的其他插件。但你目前所读到的内容将使你能够快速建立一个完全可用的访问控制系统。然后,你就可以根据需要进行扩展和自定义,并且知道 Spring Security 拥有比你可能需要的更多的功能。

Config.groovy

订阅 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅