Как реализовать проверку входных данных для ресурсов REST
Платформа SaaS, над которой я работаю, имеет интерфейс RESTful, который принимает полезные нагрузки XML.
Реализация ресурсов REST
Для магазина Java, такого как мы, имеет смысл использовать JAX-B для генерации классов JavaBean из схемы XML. Работа с полезными нагрузками XML (и JSON) с использованием JAX-B очень проста в среде JAX-RS, такой как Джерси :
01
02
03
04
05
06
07
08
09
10
11
12
|
@Path ( "orders" ) public class OrdersResource { @POST @Consumes ({ "application/xml" , "application/json" }) public void place(Order order) { // Jersey marshalls the XML payload into the Order // JavaBean, allowing us to write type-safe code // using Order's getters and setters. int quantity = order.getQuantity(); // ... } } |
(Обратите внимание, что вы не должны использовать эти универсальные типы носителей, но это обсуждение другого дня.)
В оставшейся части этого поста предполагается использование JAX-B, но его основной смысл относится и к другим технологиям. Что бы вы ни делали, пожалуйста, не используйте XMLDecoder
, так как это открыто для множества уязвимостей .
Защита ресурсов REST
Давайте предположим, что quantity
заказа используется для выставления счетов, и мы хотим, чтобы люди не крали наши деньги, вводя отрицательную сумму .
Мы можем сделать это с помощью проверки входных данных , одного из самых важных инструментов в наборе инструментов AppSec . Давайте посмотрим на некоторые способы его реализации.
Проверка ввода с помощью схемы XML
Мы можем полагаться на XML-схему для проверки , но XML-схема может проверять только столько.
Проверка отдельных свойств, вероятно, будет работать нормально, но когда мы хотим проверить отношения между свойствами, дела идут плохо. Для максимальной гибкости мы хотели бы использовать Java для выражения ограничений.
Что еще более важно, проверка схемы обычно не является хорошей идеей в службе REST .
Основная цель REST — разделить клиент и сервер, чтобы они могли развиваться отдельно.
Если мы проверим по схеме, то новый клиент, отправляющий новое свойство, сломается со старым сервером, который не понимает новое свойство. Обычно лучше игнорировать свойства, которые вы не понимаете.
JAX-B делает это правильно, а также наоборот: свойства, которые не отправляются старым клиентом, заканчиваются null
. Следовательно, новый сервер должен быть осторожным, чтобы правильно обрабатывать null
значения.
Проверка входных данных с проверкой bean-компонентов
Если мы не можем использовать проверку схемы, то как насчет использования JSR 303 Bean Validation ?
Jersey поддерживает Bean Validation, добавляя jersey-bean-validation
jar к вашему classpath.
Существует неофициальный плагин Maven для добавления аннотаций Bean Validation к классам, сгенерированным JAX-B, но я бы предпочел использовать что-то лучше поддерживаемое, и это работает с Gradle .
Итак, давайте все изменим. Мы разработаем наш JavaBean -компонент и сгенерируем XML-схему из bean-компонента для документации:
1
2
3
4
5
6
|
@XmlRootElement (name = "order" ) public class Order { @XmlElement @Min ( 1 ) public int quantity; } |
1
2
3
4
5
6
7
8
9
|
@Path ( "orders" ) public class OrdersResource { @POST @Consumes ({ "application/xml" , "application/json" }) public void place( @Valid Order order) { // Jersey recognizes the @Valid annotation and // returns 400 when the JavaBean is not valid } } |
Любая попытка POST
заказа с неположительным количеством теперь дает статус 400 Bad Request
.
Теперь предположим, что мы хотим разрешить клиентам изменять отложенные ордера. Мы будем использовать PATCH
или PUT
для обновления отдельных свойств заказа, таких как количество:
01
02
03
04
05
06
07
08
09
10
|
@Path ( "orders" ) public class OrdersResource { @Path ( "{id}" ) @PUT @Consumes ( "application/x-www-form-urlencoded" ) public Order update( @PathParam ( "id" ) String id, @Min ( 1 ) @FormParam ( "quantity" ) int quantity) { // ... } } |
Нам также нужно добавить аннотацию @Min
, которая является дублированием. Чтобы сделать это СУХОЙ , мы можем превратить quantity
в класс, который отвечает за проверку:
01
02
03
04
05
06
07
08
09
10
11
|
@Path ( "orders" ) public class OrdersResource { @Path ( "{id}" ) @PUT @Consumes ( "application/x-www-form-urlencoded" ) public Order update( @PathParam ( "id" ) String id, @FormParam ( "quantity" ) Quantity quantity) { // ... } } |
1
2
3
4
5
|
@XmlRootElement (name = "order" ) public class Order { @XmlElement public Quantity quantity; } |
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
|
public class Quantity { private int value; public Quantity() { } public Quantity(String value) { try { setValue(Integer.parseInt(value)); } catch (ValidationException e) { throw new IllegalArgumentException(e); } } public int getValue() { return value; } @XmlValue public void setValue( int value) throws ValidationException { if (value < 1 ) { throw new ValidationException( "Quantity value must be positive, but is: " + value); } this .value = value; } } |
Нам нужен общедоступный конструктор без аргументов для JAX-B, чтобы иметь возможность демонтировать полезную нагрузку в JavaBean и другой конструктор, который принимает String
для работы @FormParam
.
setValue()
javax.xml.bind.ValidationException
чтобы JAX-B прекратил демаршаллинг. Тем не менее, Джерси возвращает 500 Internal Server Error
когда он видит исключение.
Мы можем исправить это путем сопоставления исключений проверки с 400
кодами состояния с помощью средства отображения исключений . Пока мы на этом, давайте сделаем то же самое для IllegalArgumentException
:
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
|
@Provider public class DefaultExceptionMapper implements ExceptionMapper<Throwable> { @Override public Response toResponse(Throwable exception) { Throwable badRequestException = getBadRequestException(exception); if (badRequestException != null ) { return Response.status(Status.BAD_REQUEST) .entity(badRequestException.getMessage()) .build(); } if (exception instanceof WebApplicationException) { return ((WebApplicationException)exception) .getResponse(); } return Response.serverError() .entity(exception.getMessage()) .build(); } private Throwable getBadRequestException( Throwable exception) { if (exception instanceof ValidationException) { return exception; } Throwable cause = exception.getCause(); if (cause != null && cause != exception) { Throwable result = getBadRequestException(cause); if (result != null ) { return result; } } if (exception instanceof IllegalArgumentException) { return exception; } if (exception instanceof BadRequestException) { return exception; } return null ; } } |
Проверка входных данных по объектам домена
Несмотря на то, что описанный выше подход будет работать достаточно хорошо для многих приложений, он в корне ошибочен.
На первый взгляд, сторонникам доменно-управляемого дизайна (DDD) может понравиться идея создания класса « Quantity
».
Но классы Order
и Quantity
не моделируют понятия предметной области; они моделируют REST-представления. Это различие может быть тонким, но оно важно.
DDD имеет дело с понятиями предметной области, в то время как REST имеет дело с представлениями этих понятий. Понятия предметной области открыты, но представления разработаны и подвержены всевозможным компромиссам.
Например, ресурс REST коллекции может использовать пейджинг, чтобы предотвратить отправку слишком большого количества данных по сети. Другой ресурс REST может объединять несколько концепций домена, чтобы сделать протокол клиент-сервер менее разговорчивым.
Ресурс REST может даже вообще не иметь соответствующей концепции домена. Например, POST
может возвратить 202 Accepted
и указать на ресурс REST, который представляет ход асинхронной транзакции.
Доменные объекты должны как можно точнее охватить вездесущий язык и быть свободными от компромиссов, чтобы функциональность работала.
С другой стороны, при проектировании ресурсов REST необходимо найти компромиссные решения для удовлетворения нефункциональных требований, таких как производительность, масштабируемость и эволюционируемость.
Вот почему я не думаю, что такой подход, как RESTful Objects, будет работать. (По тем же причинам я не верю в Naked Objects для пользовательского интерфейса.)
Добавление проверки к JavaBean-компонентам, которые являются нашими представлениями ресурсов, означает, что у этих bean-компонентов теперь есть две причины для изменения, что является явным нарушением принципа единой ответственности .
Мы получаем гораздо более чистую архитектуру, когда используем JAX-B JavaBeans только для наших REST-представлений и создаем отдельные доменные объекты, которые обрабатывают проверку.
Внедрение проверки в доменные объекты — это то, что Дэн Берг Джонссон называет « Управляемая доменом безопасность» .
В этом подходе примитивные типы заменяются объектами значений. (Некоторые люди даже возражают против использования каких-либо String
).
Поначалу может показаться излишним создание целого нового класса, содержащего одно целое число, но я призываю вас попробовать. Вы можете обнаружить, что избавление от примитивной одержимости дает ценность даже вне проверки.
Что вы думаете?
Как вы справляетесь с проверкой ввода в ваших сервисах RESTful? Что вы думаете о доменной безопасности? Пожалуйста, оставьте комментарий.