Статьи

Автоматизируйте контроль доступа для пользовательских объектов

Практически каждое веб-приложение должно иметь несколько пользователей, и у каждого пользователя есть некоторые данные — посты, документы, сообщения, что угодно. И самое очевидное, что нужно сделать, это защитить эти объекты от получения пользователями, которые не являются законными владельцами этих ресурсов.

К сожалению, это не самая простая вещь. Я не имею в виду, что это сложно, просто это не так интуитивно понятно, как простой возврат ресурсов. Когда вы являетесь конечной точкой /record/{recordId} , запрос к базе данных для recordId — это то, что вы делаете немедленно. Только тогда возникает проблема проверки, принадлежит ли эта запись текущему пользователю, прошедшему проверку подлинности.

Фреймворки здесь не помогают, потому что логика управления доступом и владения зависит от конкретного домена. Там нет очевидного общего способа определения владельца. Это зависит от модели объекта и отношений между объектами. В некоторых случаях это может быть довольно сложно, включая поиск в таблице соединений (для отношений «многие ко многим»).

Но вы должны автоматизировать это по двум причинам. Во-первых, ручное выполнение этих проверок для каждого метода конечной точки / контроллера утомительно и делает код ужасным. Во-вторых, легче забыть добавить эти проверки, особенно если есть новые разработчики.

Вы можете выполнять эти проверки в нескольких местах, вплоть до DAO, но в целом вы должны потерпеть неудачу как можно раньше, поэтому эти проверки должны выполняться на уровне контроллера (обработчика конечной точки). В случае Java и Spring вы можете использовать аннотации и HandlerInterceptor для автоматизации этого. В случае любого другого языка или структуры, существуют похожие подходы — какой-то подключаемый способ описания отношений собственности, которые необходимо проверить.

Ниже приведен пример аннотации для каждого метода контроллера:

1
2
3
4
public @interface VerifyEntityOwnership {
    String entityIdParam() default "id";
    Class<?> entityType();
}

Затем вы определяете перехватчик (который, конечно, должен быть настроен для выполнения)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Component
public class VerifyEntityOwnershipInterceptor extends HandlerInterceptorAdapter {
 
    private static final Logger logger = LoggerFactory.getLogger(VerifyEntityOwnershipInterceptor.class);
     
    @Autowired
    private OrganizationService organizationService;
 
    @Autowired
    private MessageService MessageService;
     
    @Autowired
    private UserService userService;
     
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
 
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // assuming spring-security with a custom authentication token type
        if (authentication instanceof ApiAuthenticationToken) {
            AuthenticationData authenticationData = ((ApiAuthenticationToken) authentication).getAuthenticationData();
 
            UUID clientId = authenticationData.getClientId();
            HandlerMethod handlerMethod = (HandlerMethod) handler;
             
            VerifyEntityOwnership annotation = handlerMethod.getMethodAnnotation(VerifyEntityOwnership.class);
            if (annotation == null) {
                logger.warn("No VerifyEntityOwnership annotation found on method {}", handlerMethod.getMethod().getName());
                return true;
            }
             
            String entityId = getParam(request, annotation.entityIdParam());
            if (entityId != null) {
                if (annotation.entityType() == User.class) {
                    User user = userService.get(entityId);
                    if (!user.getClientId().equals(clientId)) {
                       return false;
                    }
                } else if (annotation.entityType() == Message.class) {
                    Message record = messageService.get(entityId);
                    if (!message.getClientId().equals(clientId) {
                        return false;
                    }
                } // .... more
            }
        }
 
        return true;
    }
     
    @SuppressWarnings("unchecked")
    private String getParam(HttpServletRequest request, String paramName) {
        String value = request.getParameter(paramName);
        if (value != null) {
            return value;
        }
        Map<String, String> pathVariables = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        return pathVariables.get(paramName);
    }
}

Вы видите, что это предполагает необходимость настраиваемой логики для каждого типа. Если ваша модель проста, вы можете сделать это универсальным — заставить все ваши сущности реализовывать некоторый Owned интерфейс с getClientId() который все они определяют. Тогда просто имейте dao.get(id, entityClass); и избегайте логики, специфичной для сущности.

Обратите внимание на предупреждение, которое выводится на печать, когда в методе нет аннотации — это означает, что вы могли забыть добавить его. Некоторые конечные точки могут не требовать проверки прав собственности — для них вы можете иметь специальную аннотацию @IgnoreEntityOwnership . Смысл в том, чтобы принять сознательное решение не проверять право собственности, а не забывать об этом и представлять проблему безопасности.

То, что я говорю, может быть очевидным. Но я видел много примеров этого упущения, включая правительственные проекты производства. И, как я сказал, фреймворки не заставляют вас рассматривать этот аспект, потому что они не могут сделать это в общем виде — веб-фреймворки обычно не имеют отношения к вашей модели сущностей, а ваш ORM не имеет отношения к вашим контроллерам. Существуют всеобъемлющие структуры, которые обрабатывают все эти аспекты, но даже у них нет общих механизмов для этого (по крайней мере, я не знаю).

Безопасность включает в себя применение ряда передовых методов и принципов к системе. Но это также включает процедуры и автоматизацию, которые помогают разработчикам и администраторам не пропускать то, о чем они обычно знают, но время от времени забывают. И чем менее утомительным является принцип безопасности, тем более вероятно, что он будет последовательно применяться.

Опубликовано на Java Code Geeks с разрешения Божидара Божанова, партнера нашей программы JCG . См. Оригинальную статью здесь: Автоматизация контроля доступа для пользовательских объектов

Мнения, высказанные участниками Java Code Geeks, являются их собственными.