这篇博客文章将介绍使用Spring Security插件在Grails中使用安全性表达式实现方法级别的安全性。 我假设您对Grails Spring Security Core插件有一些基本的了解。
角色还不够。
使用Spring Security Core插件时,通常会开始配置访问某些URL所需的角色。 可以通过使用配置映射(请参见下文),使用@Secured注释对控制器操作进行注释或通过在数据库中存储RequestMap(请参阅: 存储在数据库中的Requestmap实例 )来完成此配置。
角色配置示例:
grails.plugins.springsecurity.interceptUrlMap = [
// /admin/** URLs can only be accessed by users with role ROLE_ADMIN
'/admin/**' : ['ROLE_ADMIN'],
...
]
仅允许管理员访问管理员功能
编辑内容功能具有一般访问权限。 不幸的是,角色对于检查是否允许某个用户编辑特定内容没有太大帮助。 这是安全性表达式和方法安全性跃入的地方。
@Secured注释
如果您使用过Spring Security(没有Grails),则可能还记得Spring Security的@Secured批注。 Grails Spring Security Core插件还包含此注释的Grails替代品。 grails版本(@ grails.plugins.springsecurity.Secured)也可用于字段,而原始版本(@ org.springframework.security.access.annotation.Secured)仅可用于方法。 这允许Grails批注与包含闭包值的字段一起使用:
@Secured(['ROLE_ADMIN']) // only works with @grails.plugins.springsecurity.Secured
def adminAction = {
...
}
另外,Grails @Secured注释还支持SpEL表达式,而标准的Spring Security @Secured注释仅支持角色检查(有关更多详细信息,请参阅文档 )。
服务如何?
安全约束通常是您在服务层中想要的。 不幸的是,Grails Spring Security Core插件使@Secured注释仅在控制器中可用。 要使用服务级别的安全注释,我们必须添加Grails Spring Security ACL插件。 ACL插件还提供了更复杂的@PreAuthorize和@PostAuthorize批注。 这些注释可用于在方法执行之前和之后验证对方法的访问(我们将在后面看到)。 Grails Spring Security ACL插件的主要目的是提供对访问控制列表(ACL)的支持,该列表允许对访问权限进行非常精细的控制。 也许将来我会写一篇有关ACL用法的博客文章,但是绝对超出了本文的范围。 在这里,我们仅使用ACL插件在批注中获得安全性表达式支持。 不需要进一步的插件配置。
那代码呢?
好的,假设我们要构建一个用户可以管理笔记的应用程序。 域类如下所示:
class Note {
String title
String text
static belongsTo = [author: User]
}
class User {
String username
String password
static hasMany = [notes: Note]
static constraints = {
username blank: false, unique: true
...
}
// .. rest of generated user class from Spring Security Core plugin
}
用户可以有许多注释,而注释只有一个作者。 现在,我们要创建一个服务,该服务提供一些使用Note对象的常用方法:
class NoteService {
public long getTotalNoteCount() { .. }
public void createNote(Note note) { .. }
public void updateNote(Note note) { .. }
public Note getNote(long id) { .. }
public void removeNote(long id) { .. }
}
对于这些服务方法,我们要应用以下访问规则:
- 每个人都应该能够使用getTotalNoteCount()获得系统存储的笔记总数。
- 登录的用户可以使用createNote()创建新的笔记
- 作者只能阅读,更新或删除注释
我们首先将@PreAuthorize添加到getTotalNoteCount():
@PreAuthorize('permitAll()')
public long getTotalNoteCount() {
...
}
@PreAuthorize和@PostAuthorize将SpEL表达式作为参数,对其进行评估以确定是否授予用户访问权限。 每个人都应该能够调用getTotalNoteCount(),因此我们只需调用预定义的allowAll()函数。
createNote()的安全性表达式看起来类似:
@PreAuthorize('isFullyAuthenticated()')
public Note createNote(Note note) {
...
}
由于只有登录的用户才能创建注释,因此我们在表达式内调用isFullyAuthenticated()函数。
到目前为止,使用角色可以达到相同的效果。 在下一个示例中,我们将看到安全性表达式的真正好处。 updateNote()的访问规则稍微复杂一些:
@PreAuthorize('isAuthenticated() and principal?.username == #note.author.username')
public Note updateNote(Note note) {
...
}
更新便笺时,我们必须确保登录用户是他要编辑的便笺对象的作者。 Spring Security使用名为principal的预定义变量填充SpEL上下文,该预定义变量可用于访问当前登录的用户(所有预定义变量的列表均可在此处找到)。 使用#前缀可以访问方法参数。 因此,SpEL表达式中的#note引用了note方法参数。 在此示例中,我们检查登录用户的名称(主要)和传递的Note对象的作者(#note.author)的名称是否匹配。 如果两者相同,则允许用户更新Note对象。
getNote()方法有所不同,因为没有可用于访问注释作者的Note对象参数。 但是,返回值是一个Note对象,可以使用@PostAuthorize对其进行检查:
@PostAuthorize('isAuthenticated() and principal.username == returnObject.author.username')
public Note getNote(long id) {
...
}
如上所述,@ PostAuthorize批注可用于在调用方法后评估安全性表达式。 在@PostAuthorize表达式中,可以使用预定义的变量returnObject访问方法调用返回的对象。 如果登录的用户不是返回的Note对象的作者,则将抛出AccessDeniedException。
removeNote()有点复杂。 没有注释对象参数,因此没有简单的方法可以在@PreAuthorize中验证注释作者。 @PostAuthorize在这里也无济于事。 即使removeNote()会返回已删除的Note对象,@ PostAuthorize也会检查是否允许用户删除已删除的Note对象。 没那么有用…
在下面的内容中,我将展示将安全约束添加到removeNote()的两种不同方式。
1.使用bean引用
在SpEL表达式中,可以引用bean并将安全规则的评估委派给它们。 看起来像下面的代码:
@PreAuthorize("@securityService.canRemoveNote(#id)")
public Note removeNote(long id) {
...
}
@符号用于在SpEL表达式中引用bean。 在这里,调用了名为securityService的bean的canRemoveNote()方法。 注释ID作为参数传递给canRemoveNote()。 securityService bean是标准的Grails服务,用于实现安全约束:
class SecurityService {
def springSecurityService
public boolean canRemoveNote(long id) {
Note note = Note.get(id)
return note.author == springSecurityService.getCurrentUser()
}
}
不幸的是,这不会立即可用,并且需要对Spring Security配置进行一些小的调整。 前一段时间,我写了一篇简短的文章,所以我在这里不再赘述。 请查看此博客文章以获取更多详细信息: 在Spring Security表达式中调用bean方法 。
2.使用PermissionEvaluator
一种实现removeNote()访问规则的替代解决方案是使用Spring Security的内置hasPermission()方法将安全检查委托给PermissionEvaluator。 在安全性表达式上下文中,可以使用两种不同的hasPermission()方法:
hasPermission(Object targetObject, Object permission)
hasPermission(Serializable targetId, String targetType, Object permission)
如果必须检查的对象实例可用(例如在updateNote(Note note)中),则可以使用第一个。 可以使用第二种版本,没有可用的实例,并且需要通过ID和类型来标识对象。 后面的一个可以帮助我们使用removeNote()方法:
@PreAuthorize("hasPermission(#id, 'com.mscharhag.Note', 'remove')")
public Note removeNote(long id) {
..
}
请注意,id的基本类型long会转换为Long,从而实现所需的Serializable接口。 现在,我们必须创建自己的PermissionEvaluator实现来定义我们的安全约束。 PermissionEvaluator要求实现两个方法,这些方法与可在安全性表达式中使用的两个hasPermission()方法直接相关:
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission)
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission)
唯一的区别是Spring Security将当前身份验证状态作为附加参数传递给PermissionEvaluator。 PermissionEvaluator的可能实现如下所示:
class GrailsPermissionEvaluator implements PermissionEvaluator {
def grailsApplication
def springSecurityService
@Override
public boolean hasPermission(Authentication authentication, Object note, Object permission) {
def user = springSecurityService.getCurrentUser();
return permission == 'remove' && note.author == user
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// get domain class with name targetType
Class domainClass = grailsApplication.getDomainClass(targetType).clazz
// get domain object with id targetId
Note note = domainClass.get(targetId)
return hasPermission(authentication, note, permission)
}
}
如我们所见,第二种方法将targetType和targetId解析为Note对象,然后将其传递给第一种方法。 第一种方法检查是否允许当前登录的用户删除Note对象。
为了完成这项工作,我们需要重写由Spring Security ACL插件配置的默认的PermissionEvaluator bean。 这是在grails-app / conf / spring / resources.groovy中完成的:
permissionEvaluator(GrailsPermissionEvaluator) {
grailsApplication = ref('grailsApplication')
springSecurityService = ref('springSecurityService')
}
默认情况下,Grails Spring Security ACL插件将AclPermissionEvaluator实例配置为PermissionEvaluator,该实例可用于评估ACL规则。 但是,在此示例中,我们没有使用ACL,因此可以使用我们自己的实现覆盖它。
总之,bean引用和PermissionEvaluator方法都是实现您自己的安全性约束的好方法。 如果您的访问规则比本示例中的规则更复杂,这将特别有用。
最终的NoteService如下所示:
class NoteService {
@PreAuthorize('permitAll()')
public long getTotalNoteCount() { .. }
@PreAuthorize('isFullyAuthenticated()')
public Note createNote(Note note) { .. }
@PostAuthorize('isAuthenticated() and principal.username == returnObject.author.username')
public Note getNote(long id) { .. }
@PreAuthorize('isAuthenticated() and principal.username == #note.author.username')
public Note updateNote(Note note) { .. }
@PreAuthorize("@securityService.canRemoveNote(#id)")
public Note removeNoteUsingBeanResolver(long id) { .. }
@PreAuthorize("hasPermission(#id, 'com.mscharhag.Note', 'remove')")
public Note removeNoteUsingPermissionEvaluator(long id) { .. }
}
- 完整的源代码可以在GitHub上找到 。