Статьи

Начало работы с методом безопасности в Grails с использованием Spring Security

Этот пост будет посвящен реализации безопасности уровня метода с помощью выражений безопасности в Grails с использованием плагинов Spring Security. Я предполагаю, что у вас есть некоторое базовое понимание плагина Grails Spring Security Core.

Ролей недостаточно.

При использовании плагина Spring Security Core вы обычно начинаете настраивать, какие роли требуются для доступа к определенным URL-адресам. Эта конфигурация может быть выполнена с помощью карты конфигурации (см. Ниже), аннотирования действий контроллера с помощью аннотации @Secured или путем сохранения RequestMap в базе данных (см. Экземпляры карты запросов, хранящиеся в базе данных ).

Пример конфигурации роли:

1
2
3
4
5
grails.plugins.springsecurity.interceptUrlMap = [
  // /admin/** URLs can only be accessed by users with role ROLE_ADMIN
  '/admin/**' : ['ROLE_ADMIN'], 
  ...
]

Роли прекрасно работают, чтобы определить простые правила, например, только администраторам разрешен доступ к функциям администратора . К сожалению, этого часто недостаточно.

Рассмотрим приложение, в котором пользователи могут создавать какой-либо контент (например, комментарии, новости и т. Д.). Пользователь должен иметь возможность редактировать контент, который он создал позже. Однако пользователь не должен иметь возможности редактировать контент, созданный другими пользователями. Здесь роль можно использовать для проверки того, имеет ли пользователь общий доступ к функциям редактирования содержимого . К сожалению, роли не очень помогают проверить, разрешено ли определенному пользователю редактировать определенный фрагмент контента. Это где выражения безопасности и метод безопасности.

@ Защищенная аннотация

Если вы использовали Spring Security (без Grails) до того, как вспомните аннотацию @Secured от Spring Security. Плагин Grails Spring Security Core также содержит замену Grails для этой аннотации. Версия grails (@ grails.plugins.springsecurity.Secured) также работает с полями, в то время как оригинал (@ org.springframework.security.access.annotation.Secured) можно использовать только для методов. Это позволяет использовать аннотацию Grails с полями, которые содержат значение замыкания:

1
2
3
4
@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 для получения поддержки выражений безопасности в аннотациях. Никакой дополнительной настройки плагина для этого не требуется.

Хорошо. Как насчет кода?

Хорошо, давайте предположим, что мы хотим создать приложение, где пользователи могут управлять заметками. Классы домена выглядят так:

1
2
3
4
5
class Note {
  String title
  String text
  static belongsTo = [author: User]
}
01
02
03
04
05
06
07
08
09
10
11
12
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:

1
2
3
4
5
6
7
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 ():

1
2
3
4
@PreAuthorize('permitAll()')
public long getTotalNoteCount() {
  ...
}

@PreAuthorize и @PostAuthorize принимают выражение SpEL в качестве параметра, который оценивается, чтобы определить, предоставлен ли пользователю доступ. Каждый должен иметь возможность вызывать getTotalNoteCount (), поэтому мы просто вызываем предопределенную функцию allowAll ().

Выражение безопасности для createNote () выглядит примерно так:

1
2
3
4
@PreAuthorize('isFullyAuthenticated()')
public Note createNote(Note note) {
  ...
}

Поскольку только зарегистрированные пользователи должны иметь возможность создавать заметки, мы вызываем функцию isFullyAuthenticated () в выражении.

До сих пор можно было бы добиться того же эффекта, используя роли. Реальный бонус выражений безопасности мы увидим в следующем примере. Правило доступа для updateNote () немного сложнее:

1
2
3
4
@PreAuthorize('isAuthenticated() and principal?.username == #note.author.username')
public Note updateNote(Note note) {
  ...
}

При обновлении заметки мы должны быть уверены, что зарегистрированный пользователь является автором объекта заметки, который он хочет редактировать. Spring Security заполняет контекст SpEL предопределенной переменной с именем Principal, которую можно использовать для доступа к текущему вошедшему в систему пользователю (список всех предопределенных переменных можно найти здесь ). С префиксом # можно получить доступ к аргументам метода. Таким образом, #note в выражении SpEL ссылается на параметр метода note. В этом примере мы проверяем, совпадают ли имена вошедшего в систему пользователя (принципала) и автора переданного объекта Note (# note.author). Если оба они одинаковы, пользователю разрешено обновлять объект Note.

Метод getNote () немного отличается, потому что нет параметра объекта Note, который можно использовать для доступа к автору заметки. Однако возвращаемое значение является объектом Note, который можно проверить с помощью @PostAuthorize:

1
2
3
4
@PostAuthorize('isAuthenticated() and principal.username == returnObject.author.username')
public Note getNote(long id) {
  ...
}

Как упоминалось выше, аннотация @PostAuthorize может использоваться для оценки выражения безопасности после вызова метода. В выражении @PostAuthorize можно получить доступ к объекту, возвращенному вызовом метода, используя предопределенную переменную returnObject. Здесь будет создано исключение AccessDeniedException, если зарегистрированный пользователь не является автором возвращенного объекта Note.

removeNote () немного сложнее. Параметр объекта Note отсутствует, поэтому нет простого способа проверить автора заметки в @PreAuthorize. @PostAuthorize здесь тоже не помогает. Даже если removeNote () вернет удаленный объект Note, @PostAuthorize проверит, разрешено ли пользователю удалять объект Note после его удаления. Не так полезно …

Далее я покажу два разных способа добавления ограничения безопасности к removeNote ().

1. Использование ссылок на компоненты

В выражениях SpEL можно ссылаться на bean-компоненты и делегировать им оценку правил безопасности. Это может выглядеть как следующий фрагмент кода:

1
2
3
4
@PreAuthorize("@securityService.canRemoveNote(#id)")
public Note removeNote(long id) {
  ...
}

Знак @ используется для ссылки на bean-компоненты в выражениях SpEL. Здесь вызывается метод canRemoveNote () компонента с именем securityService. Идентификатор заметки передается в качестве параметра в canRemoveNote (). Bean-компонент securityService — это стандартная служба Grails, которая используется для реализации ограничений безопасности:

1
2
3
4
5
6
7
8
9
class SecurityService {
 
  def springSecurityService
 
  public boolean canRemoveNote(long id) {
    Note note = Note.get(id)
    return note.author == springSecurityService.getCurrentUser()
  }
}

К сожалению, это не сработает из коробки, и необходимо внести небольшие изменения в конфигурацию Spring Security. Некоторое время назад я написал небольшую статью именно об этом, поэтому я не буду объяснять это здесь снова. Пожалуйста, ознакомьтесь с этой записью в блоге для более подробной информации: Вызов методов bean в выражениях Spring Security

2. Использование PermissionEvaluator

Альтернативное решение для реализации правил доступа removeNote () заключается в использовании встроенных методов Spring Security в hasPermission () для делегирования проверок безопасности PermissionEvaluator. В контексте выражения безопасности доступны два различных метода hasPermission ():

1
2
hasPermission(Object targetObject, Object permission)
hasPermission(Serializable targetId, String targetType, Object permission)

Первый может быть использован, если доступен экземпляр объекта, который необходимо проверить (как в updateNote (Примечание). Вторая версия может использоваться, если экземпляр недоступен, и объект должен быть идентифицирован по идентификатору и типу. Последний — тот, который может помочь нам с методом removeNote ():

1
2
3
4
@PreAuthorize("hasPermission(#id, 'com.mscharhag.Note', 'remove')")
public Note removeNote(long id) {
  ..
}

Обратите внимание, что примитивный тип long из id преобразуется в Long, который реализует необходимый интерфейс Serializable. Теперь нам нужно создать собственную реализацию PermissionEvaluator, чтобы определить наши ограничения безопасности. PermissionEvaluator требует реализации двух методов, которые напрямую связаны с двумя методами hasPermission (), которые можно использовать в выражениях безопасности:

1
2
boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) 
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission)

Единственное отличие состоит в том, что Spring Security передает текущее состояние аутентификации в качестве дополнительного параметра PermissionEvaluator. Возможная реализация PermissionEvaluator может выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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.

Чтобы это работало, нам нужно переопределить bean-компонент accessEvaluator по умолчанию, настроенный плагином Spring Security ACL. Это делается в grails-app / conf / spring / resources.groovy:

1
2
3
4
permissionEvaluator(GrailsPermissionEvaluator) {
  grailsApplication     = ref('grailsApplication')
  springSecurityService = ref('springSecurityService')
}

По умолчанию подключаемый модуль Grails Spring Security ACL настраивает экземпляр AclPermissionEvaluator в качестве licenseEvaluator, который можно использовать для оценки правил ACL. Однако в этом примере мы не используем ACL, поэтому мы можем переопределить его собственной реализацией.

Вкратце, ссылки на компоненты и подход PermissionEvaluator обеспечивают хороший способ реализации собственных ограничений безопасности. Это особенно полезно, если ваши правила доступа более сложны, чем в этом примере.

Окончательный NoteService выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 .