使用 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 in Action 中的示例应用程序)。您也可以从这里获取完成的项目。

因此,在您的项目内,运行:

    grails install-plugin spring-security-core

如果您查看插件安装生成的输出,您会发现它提供了一些命令。其中最重要的是:s2-quickstart该命令将帮助您尽可能轻松地启动和运行。它会生成存储用户信息所需的基本域类以及处理身份验证的控制器。

在运行该命令之前,您可能需要做一个决定。如果您已经有一个 'user' 域类,您将不得不决定如何将其与插件生成的类集成。一种选择是替换现有的域类,并简单地将您的自定义应用到这个替换的类上。另一种方法是让您自己的域类继承插件的类。

哪种方法更好?我更喜欢后一种,因为它允许您在生成的用户域类模板发生变化时轻松更新它。这也意味着您不会过度污染您的域模型,使其充斥着 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 插件,实现这一目标非常简单,尽管您必须决定使用三种机制中的哪一种。您可以采取以控制器为中心的方法,通过注解动作;在以下文件中使用静态 URL 规则处理:Config.groovy;或者使用请求映射在数据库中定义运行时规则。

注解

对于以控制器为中心的方法,您无法超越插件提供的@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
        }
        ...
    }
}

我们只需将安全服务注入BootStrap,然后使用其encodePassword()方法将明文密码转换为其哈希值。这种方法在您决定更改使用的摘要算法时特别有效,因为该服务将使用与比较密码进行身份验证时相同的算法来编码密码。换句话说,上面的代码无论使用哪种算法都保持不变。

更新:从 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() ]
    }
    ...
}

如您所见,我们只需再次注入安全服务并使用它来获取主体。除非您创建了UserDetailsService的自定义版本(如果您以前没有遇到过,请不用担心),否则主体将是org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser的实例,其id属性包含相应 'user' 域实例的 ID。

您需要注意的一点是:如果当前用户是以匿名方式认证的,即他/她未登录且未被记住,则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动作,而不是LoginControllerauth。但是,这仍然是重定向,对吗?是的,但您可以实现来发送错误状态或渲染 JSON - 基本上是客户端 Javascript 代码可以处理的任何内容。authAjax不幸的是,插件提供的

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

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>

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

动作的默认实现。authAjax您还可以自定义

ajaxSuccess还是ajaxDenied动作以返回您想要的任何响应。如您所见,服务器端 AJAX 处理简单易于定制。实际工作必须在客户端代码中完成。

自定义登录表单

将整个页面专门用于登录表单已不再流行。如今,应用程序更有可能拥有一个内容丰富的首页,其中包含一个不显眼的登录表单,可能只通过一些 Javascript 魔术才可见。提供您自己的专用登录页面(只需随意编辑。但是,这仍然是重定向,对吗?是的,但您可以实现动作,而不是LoginController及其关联的 GSP 视图),但这是否很难?那么登录面板呢?

这并不像您想象的那么难。首先,您需要决定在需要身份验证时用户应该重定向到哪里。您可能已经猜到,默认情况下是/login/auth。更改此默认值非常容易,只需在以下文件中添加一个设置:Config.groovy:

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

此行告诉插件在需要身份验证时将用户重定向到首页。然后,您只需将登录面板添加到首页即可。以下是一个 GSP 表单示例,可用于此类面板:

<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>

这里的关键点是:

  1. 表单必须使用 POST 方法;
  2. 表单必须提交到 <context>/j_spring_security_check;
  3. 用户名字段的名称必须是 'j_username';
  4. 密码字段的名称必须是 'j_password';以及
  5. "记住我"字段的名称必须是 '_spring_security_remember_me'。
只要满足这些要求,登录表单就能完美运行。嗯,不完全是。如果通过您的登录表单进行身份验证尝试失败,您将发现自己被重定向回旧的登录页面。幸运的是,可以通过添加另一个配置设置快速解决此问题:
grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/'

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

本文真的只是触及了 Spring Security 插件的表面。我没有提及 HTTP Basic 和 Digest 身份验证、事件、带盐密码等。这甚至还不包括提供额外功能的其他插件,例如替代的身份验证机制和访问控制列表(ACL)。但是,您到目前为止所读到的内容将使您能够立即建立一个功能齐全的访问控制系统。然后,您可以根据需要进行扩展和自定义,因为 Spring Security 提供的功能比您可能需要的还要多。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有