Оказалось, что все намного проще, чем я первоначально думал, и я завершил его менее чем за день, используя опцию AOP.
Это код аннотации AccessControl
, комментарии удалены:
@Documented
@Inherited
@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface AccessControl {
public String[] value() default {};
}
Его можно разместить либо на контроллере (см. Мой оригинальный пост / вопрос), либо на методе контроллера:
@RestController
@RequestMapping("/admin")
@AccessControl({ Roles.ADMIN })
public class AdminController {
// This endpoint has open access: no authorization check will happen.
@AccessControl
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public DummyDto getNoCheck(@PathVariable Integer id) {
return service.get(id);
}
// This endpoint specifically allows access to the "USER" role, which is lower
// than ADMIN in my hierarchy of roles.
@AccessControl(Roles.USER)
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public DummyDto getCheckUser(@PathVariable Integer id) {
return service.get(id);
}
// The authorization check defaults to checking the "ADMIN" role, because there's
// no @AccessControl annotation here.
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public DummyDto getCheckRoleAdmin(@PathVariable Integer id) {
return service.get(id);
}
}
Чтобы выполнить фактическую проверку, необходимо два вопросаответить:
- во-первых, какие методы должны быть обработаны?
- во-вторых, что проверяется?
Вопрос 1: какие методыдолжны быть обработаны?
Для меня ответ был что-то вроде "все конечные точки REST в моем коде".Поскольку мой код находится в определенном корневом пакете, и поскольку я использую аннотацию RequestMapping в Spring, конкретный ответ приходит в форме спецификации Pointcut:
@Pointcut("execution(@org.springframework.web.bind.annotation.RequestMapping * *(..)) && within(my.package..*)")
Вопрос 2: чтоточно проверяется во время выполнения?
Я не буду помещать весь код здесь, но в основном ответ заключается в сравнении ролей пользователя с ролями, требуемыми для метода (или его контроллера, если сам метод несетнет спецификации контроля доступа).
@Around("accessControlled()")
public Object process(ProceedingJoinPoint pjp) throws Throwable {
...
// Get the roles specified in the access control rule that applies (from the method annotation, or from the controller annotation).
// Get the user roles from the UserDetails previously saved when the user went through the authentication process.
// Check authorizations: does the user have one role that is required? If no, throw an exception. If yes, don't do anything.
// No exception has been thrown: let the method proceed and return its results.
}
То, что беспокоило меня в моем первоначальном мышлении, было исключением.Поскольку у меня уже был класс сопоставления исключений, который содержит аннотацию @ControllerAdvice
, я просто повторно использовал этот класс для сопоставления моего конкретного AccessControlException
с кодом статуса 403 Запрещенным.
Для получения ролей пользователя я использовал SecurityContextHolder.getContext().getAuthentication()
для восстановления токена аутентификации, затем authentication.getPrincipal()
для извлечения пользовательского объекта сведений о пользователе, который имеет поле roles
, которое я обычно настраиваю в процессе аутентификации.
Вышеприведенный код не должен использоватьсякак есть (например, будут возникать коллизии пути), но это просто для того, чтобы передать общую идею.