订阅
领先一步
VMware 提供培训和认证,以加速您的进步。Spring Security 是一个功能强大的库,用于保护您的应用程序,它提供了令人眼花缭乱的选项。基于 Spring,它可以轻松集成到 Grails 应用程序中。但是,为什么不省去麻烦,使用新的改进的 Grails 插件呢?
该插件经历了几个演变阶段,最初是 Acegi 插件。它最新的版本是针对 Spring Security 3 和 Spring 3 的完全重写。其中一个结果是,该插件仅适用于 Grails 1.2.2 及更高版本。另一个重大变化是,不再只有一个 Spring Security 插件:某些功能已被分解为可选插件。因此,现在您只需在应用程序中包含所需的功能。
那么这些插件为您提供了什么?核心提供了基于用户和角色的易于使用的包中进行访问控制所需的必要基础。实际上,许多应用程序不需要除核心插件之外的任何其他插件。对于确实需要额外功能的用户,以下是该系列中其他插件的列表
在本文中,我将向您展示如何使用新的核心插件从头开始保护 Grails 应用程序。
更新 本文现在有两个配套的屏幕录像
与大多数插件一样,您的第一步是安装 Spring Security 插件。当然,您需要一个项目来安装它,对于本文,我提供了一个名为 Hubbub 的简单 Twitter 克隆(基于 Grails in Action 中的示例应用程序)。您还可以从此处获取完成的项目。
因此,在您的项目中,运行
grails install-plugin spring-security-core
如果您查看插件安装生成的输出,您会看到它提供了一些命令。其中最重要的是s2-quickstart,它将帮助您轻松上手。它同时生成存储用户信息所需的基本域类和处理身份验证的控制器。
在运行命令之前,您可能需要做出决定。如果您已经拥有“user”域类,则必须决定如何将其与插件生成的域类集成。一种选择是替换现有的域类,并简单地将您的自定义应用于替换。另一种方法是使您自己的域类扩展插件的域类。
哪个更好?我更喜欢后者,因为它允许您在生成的 user 域类的模板发生更改时轻松更新它。这也意味着您不会过度污染您的域模型,使其包含 Spring Security 的具体细节。缺点是,您必须处理域类继承,尽管成本非常低。
对于 Hubbub,我们将使 user 域类扩展生成的域类,这意味着我们应该使用不与现有域类名称冲突的域类名称
grails s2-quickstart org.example SecUser SecRole
这将为我们创建三个域类
示例应用程序确实需要更改一个方面:它的 URL 映射意味着无法访问登录和注销控制器。通过将以下两行添加到UrlMappings.groovy:
"/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")
可以轻松解决此问题。如果您不进行更改,登录页面将生成 404 错误!现在让我们开始保护应用程序。
此练习的重点是限制对应用程序某些部分的访问。对于 Web 应用程序,这通常意味着保护特定页面或更具体地说,URL。在 Hubbub 的情况下,我们有以下要求
使用 Spring Security 插件,这很容易实现,尽管您确实必须决定使用三种机制中的哪一种。您可以采用以控制器为中心的 подход,并注释操作;在Config.groovy中使用静态 URL 规则;或使用请求映射在数据库中定义运行时规则。
对于以控制器为中心的 подход,您无法击败插件提供的@Secured注释。在其最简单的形式中,您向其传递一个基本规则列表,这些规则定义了谁可以访问相应的操作。在这里,我通过帖子控制器的注释应用 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是一个特殊情况:如果指定,则必须满足除了列表中的其他规则之外。
内置规则如下
您还可以将注释应用于控制器类本身,这会导致所有操作继承由其定义的规则。如果操作有自己的注释,则会覆盖类级别的注释。注释不仅限于此类规则列表:查看用户指南,了解如何使用表达式来更好地控制规则。
如果您不喜欢注释,则可以通过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,我们像这样添加“用户”和“管理员”角色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().
添加用户几乎同样简单,但您需要牢记几个要求。首先,生成的“用户”域类有一个enabled属性,默认为false。如果您没有将其显式初始化为true,相应的用户将无法登录。其次,密码很少以纯文本形式存储在数据库中,因此您需要首先使用适当的摘要算法对其进行编码。
幸运的是,插件提供了一个有用的服务来帮助解决这个问题SpringSecurityService。假设我们想要在 Hubbub 的中创建一个“管理员”用户。BootStrap
代码如下所示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()
创建用户后,我们检查它是否具有“管理员”角色,如果没有,则将该角色分配给用户。我们通过生成的SecUserSecRole类及其create()方法执行此操作。
安全数据到位,并且了解如何在必要时按需创建它,现在是时候让用户界面了解身份验证、用户和角色了。
这里我想看 UI 的两个方面:显示特定于用户的信息以及确保用户只能看到他被允许查看的内容。第一个方面归结为一个问题:我们如何获取当前登录用户的“用户”域实例?考虑 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属性包含相应“用户”域实例的 ID。
您需要注意的一件事:如果当前用户匿名进行身份验证,即他未登录且未记住,则principal属性将返回一个字符串。因此,如果未经身份验证的用户可以访问某个操作,请确保在使用主体之前检查其类型!
如何确保用户只能看到他们应该看到的内容?为此,插件在sec命名空间中提供了一套丰富的 GSP 标签。假设我们要向 Hubbub 添加几个导航链接,但我们只想在用户未登录时显示其中一个,而另一个仅在用户具有ROLE_USERrole
<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 请求需要身份验证时,Spring Security 会重定向到authAjax操作LoginController而不是auth。但是等等,那仍然是重定向,对吧?是的,但是您可以实现authAjax以发送错误状态或呈现 JSON——基本上是客户端 Javascript 代码可以处理的任何内容。
不幸的是,LoginController插件提供的authAjax此时尚未实现,因此您需要自己添加它
import javax.servlet.http.HttpServletResponse
class LoginController {
...
def authAjax = {
response.sendError HttpServletResponse.SC_UNAUTHORIZED
}
...
}
这是一个非常简单的实现,它返回 401 HTTP 状态代码。我们如何处理这样的响应?这取决于您在浏览器中使用什么来实现 AJAX。示例 Hubbub 应用程序使用自适应 AJAX 标签,因此我将使用它来演示您可以执行的操作类型。这是用于发布新消息的 GSP 模板的一部分
<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属性,该属性指定当 AJAX 提交返回 401 状态代码时应执行的 Javascript 代码。例如,该 Javascript 代码可以为用户显示一个动态的客户端登录表单以进行身份验证。Hubbub 使用插件的用户指南中提供的客户端代码来做到这一点。
注意 插件的 1.1 版本将附带authAjax操作的默认实现。
您还可以自定义ajaxSuccess和ajaxDenied操作以发送您想要的任何响应。如您所见,服务器端 AJAX 处理非常简单且易于自定义。实际工作必须在客户端代码中完成。
将整个页面专门用于登录表单已不再流行。如今,应用程序更有可能具有内容丰富的首页,其中某个位置放置了一个独立的登录表单,也许只是通过一些 Javascript 魔法使其可见。提供您自己的专用登录页面非常容易(只需编辑auth操作LoginController及其关联的 GSP 视图即可),但登录面板呢?
这并不像您想象的那么难。首先,您需要确定在需要身份验证时应将用户重定向到哪里。您可能已经了解到,默认情况下,这是/login/auth。更改该默认值就像向Config.groovy:
grails.plugins.springsecurity.auth.loginFormUrl = '/'
中添加设置一样简单。
<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 表单
grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/'
只要满足这些要求,登录表单就能完美运行。好吧,不是完全完美。如果通过登录表单的身份验证尝试失败,您会发现自己被重定向回旧的登录页面。幸运的是,通过添加另一个配置设置可以快速解决此问题
就是这样,您就拥有了一个功能齐全的登录表单!还有许多其他选项可用于微调行为,但您现在拥有了构建的基础。