Разработка Java-приложений: устранение недостатков в системе безопасности

Источник: Hackernoon Несмотря на то, что Java считается одной из наиболее безопасных платформ для разработки программного обеспечения, необходимо учитывать вероятность появления в коде уязвимостей. Java Development Kit, в отличие от некоторых других платформ, уделяет большое внимание безопасности, автоматизированной сборке мусора и типобезопасности (Type-Safe).Кофе-брейк #40. Как устранять недостатки системы безопасности во время разработки Java-приложений. Параллелизм в Java: несколько способов создания потока - 1В то время как типобезопасность сводит ошибочные операции к минимуму, тем самым обеспечивая устойчивость существующего кода, автоматическая сборка мусора предназначена для реализации более чистого фрагмента кода.

Почему безопасность Java по-прежнему важна?

Бдительность по-прежнему нужна, учитывая угрозы безопасности, распространенные в эпоху пандемии. Мы должны понимать, что разработка приложений — одна из наиболее сложных задач, и избежать появления в ней одной или нескольких уязвимостей не так просто. Чтобы защититься от появления лазеек, разработчикам, использующим JDK, необходимо регулярно устанавливать обновления и патчи. К сожалению, отчет, опубликованный Secunia ApS еще в 2015 году, показывает, что почти 48% пользователей JDK не устанавливали последние версии или исправления. Эти данные актуальны и сейчас, ведь 2020 год стал самым сложным годом с точки зрения уязвимостей систем и данных. Поскольку Java является одной из самых востребованных платформ разработки, обеспечение безопасности приложений на этом языке остается важным.

Как обезопасить разработку Java-приложений?

1. Используйте чистый код

Излишняя сложность кода открывает путь к появлению уязвимостей. Поэтому придерживайтесь минимализма в кодировании для соблюдения существующих стандартов безопасности. Рассмотрите возможность использования методов, классов или выбора модификаторов доступа для лучшей инкапсуляции кода, которая защищает жизненно важные элементы кода. Чтобы избежать нестабильности кода, рекомендуется избегать самодиагностики кода. Особенно когда ваш цикл разработки не требует передовых методов. Еще один недооцененный совет по кодированию — выбор простых API. Это помогает обеспечить минимальное взаимодействие компонентов. Соответственно, вы сможете избежать крупномасштабных сбоев в работе своего приложения. Наконец, избегайте сериализации. Поскольку сериализация объединяет неизвестные наборы данных в виде байтовых потоков, это может быть небезопасным методом. Для Java-разработчиков лучше выбрать такие форматы сериализации, как YAML, BSON или JSON.

2. Проверяйте внешние библиотеки

Чаще всего разработчики сами виноваты в хаосе своего кода, поскольку безответственно подключают внешние библиотеки, исходя из собственных предпочтений. Хотя многие внешние библиотеки, такие как Flexjson или GSON, могут не содержать вредоносного кода, всегда рекомендуется их проверять. Это позволит исключить вероятность появления уязвимостей в софте. Если же вы решили использовать библиотеку с закрытым исходным кодом, то лучше посмотреть аудиторские отчеты о ней.

3. Библиотеки шифрования значений

На рынке представлены надежные компании по разработке программного обеспечения, поэтому многие разработчики полагаются на эффективность библиотек шифрования вместо того, чтобы разрабатывать свои коды шифрования. Чтобы избежать потери данных и предотвратить появление угроз, вам необходимо понимать все эти громоздкие API-интерфейсы и сообщения об ошибках, возвращаемые библиотеками шифрования, а не слепо полагаться на их мощные алгоритмы и математические вычисления.

4. Используйте ключи шифрования

Java-разработчику необходимо найти идеальный баланс между удобством использования приложения и безопасностью. Вот почему мы должны рассматривать шифрование как неотъемлемую часть приложения. Одним из таких примеров является AWS Key Management Service (KMS) — хороший способ интеграции программных ключей шифрования в соответствии с нормативными стандартами.

5. Проверяйте вводимые данные

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

Параллелизм в Java: несколько способов создания потока

Источник: Javarevisited Java Thread похож на виртуальный процессор, который может выполнять код внутри вашего Java-приложения. При запуске такого приложения его основной метод выполняется главным потоком (main thread), который создает виртуальная машина Java. Внутри такого приложения вы можете создавать и запускать дополнительные потоки, которые могут выполнять части кода приложения параллельно с основным потоком.Кофе-брейк #40. Как устранять недостатки системы безопасности во время разработки Java-приложений. Параллелизм в Java: несколько способов создания потока - 2Потоки Java — такие же объекты, как и любой другой объект Java. Потоки — это экземпляры класса java.lang.Thread или экземпляры подклассов этого класса. Помимо того, что потоки Java являются объектами, они также могут выполнять код.

Как создать поток Java

Java позволяет создавать поток одним из трех способов:
  • При реализации в интерфейсе Runnable.
  • При реализации в интерфейсе Callable.
  • При расширении потока.
Давайте подробнее рассмотрим каждый из них.

Интерфейс Runnable

Самый простой способ создать поток — это создать класс, реализующий интерфейс Runnable. Объект Java, который реализует интерфейс Runnable, может быть выполнен с помощью Java Thread. Интерфейс Runnable — это стандартный интерфейс Java, который поставляется с платформой Java. Интерфейс Runnable имеет только один метод run (). Вот пример реализации интерфейса Runnable:

class MyRunnable implements Runnable {
	
    private String text;
	
    @Override
    public void run() {
        System.out.println(text);
    }
}
Вышеупомянутый MyRunnable — это просто задача, которую мы хотим запустить в отдельном потоке. Существуют разные способы ее запустить.

Запуск с Thread

Один из них — использование класса Thread:

@Test
public void myRunnaleTest() throws Exception {
    Thread thread = new Thread(new MyRunnale("Test task"));
    thread.start();
    thread.join();
}

Запуск с ExecutorService

Кроме того, мы можем запустить нашу задачу с помощью Executor Service (фреймворк, предоставляемый JDK, который упрощает выполнение задач в асинхронном режиме):

@Test
public void myRunnableTest() throws Exception {
    executorService.submit(new MyRunnale("Test task")).get();
}
Поскольку сейчас мы реализуем интерфейс, то мы можем при необходимости расширить еще один базовый класс.

Запуск в виде Lambda

Начиная с Java 8, любой интерфейс, который предоставляет единственный абстрактный метод, рассматривается как функциональный интерфейс. Это делает его допустимым целевым лямбда-выражением. Мы можем переписать приведенный выше Runnable код, используя лямбда-выражение:

@Test
public void myRunnableTest() throws Exception {
    executorService.submit(() -> System.out.println("Lambda test task."));
}

Интерфейс Callable

Callable — это улучшенная версия Runnable, которую добавили ​​в Java 1.5. Оба интерфейса предназначены для представления задачи, которая может выполняться несколькими потоками. Задачи Runnable можно запускать с помощью класса Thread или ExecutorService, тогда как Callables можно запускать только с использованием ExecutorService. Callable — универсальный интерфейс, содержащий единый метод call (), который возвращает общее значение:

public class PowTask implements Callable {
    double number;
    double pow;
  
    public PowTask(double number, double pow){
      this.number = bumber;
      this.pow = pow;
    }
 
    public Double call() throws Exception {
        return Math.pow(number, pow);
    }
}
Метод call () вызывается для выполнения асинхронной задачи. Этот метод также может возвращать результат. Если задача выполняется асинхронно, результат обычно передается обратно создателю задачи через Java Future. Это тот случай, когда Callable передается в ExecutorService для одновременного выполнения. Мы можем запустить нашу задачу с помощью ExecutorService:

@Test
public void callableTest(){
    PowTask task = new PowTask(2, 3);
    Future future = executorService.submit(task);
    assertEquals(8, future.get().doubleValue());
}
Довольно часто операции ввода-вывода, такие как чтение, запись на диск или в сеть, — хорошие кандидаты для задач, которые могут выполняться одновременно. В операциях ввода-вывода часто есть длительное время ожидания между чтением и записью блоков данных. Выполняя такие задачи в отдельном потоке, вы можете избежать излишней блокировки основного потока приложения.

Подкласс Thread

Еще один способ состоит в создании подкласса Thread и переопределении метода run (). Метод run () — это то, что выполняется потоком после вызова start (). Вот пример создания подкласса Java Thread:

public class MyThread extends Thread {
 
    private String text;
 
    @Override
    public void run() {
        System.out.println(text);
    }
}
Также обратите внимание, что MyThread не может расширять какой-либо другой класс, поскольку Java не поддерживает множественное наследование.

Запуск через start

Есть еще два способа запустить Thread. Один из них — вызов метода start ():

@Test
public void myThreadTest()
  throws Exception {
 
    Thread thread = new MyThread("Test task");
    thread.start();
    thread.join();
}
Start () будет возвращаться, как только запустится поток. Он не будет ждать, пока будет выполнен метод run (). Метод run () будет выполняться так, как если бы он выполнялся другим процессором. Когда выполняется метод run (), он печатает предоставленный текст.

Запуск с ExecutorService

Еще один способ запустить поток — использовать ExecutorService:

@Test
public void myThreadtest() throws Exception {
    executorService.submit(new MyThread("Test task")).get();
}

Callable или Runnable

Интерфейс Callable похож на интерфейс Runnable, поскольку оба они представляют задачу, которая предназначена для одновременного выполнения отдельным потоком. Callable отличается от Runnable тем, что метод run () интерфейса Runnable не возвращает значение и не может генерировать проверенные исключения (только RunTimeException). Кроме того, Runnable изначально разработан для длительного многопоточного выполнения, например, одновременного запуска сетевого сервера или отслеживания каталога для новых файлов. Интерфейс Callable больше предназначен для одноразовых задач, которые возвращают один результат.

Thread или Runnable?

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