Jmix/Spring-приложение в IFrame

от автора

Предположим, у нас появилась задача встроить какой-то функционал, реализуемый системой на Jmix/Vaadin/Spring на другой сайт или в веб-приложение. Сейчас существует большое количество статических генераторов и других систем управления содержимым, где у разработчика есть доступ только к фронтенд-части. Если это не портальная система, обычным решением в таких случаях будет использовать встраивание через IFrame. 

Для того чтобы приложение с интерфейсом на Vaadin открывалось в айфрейме за пределами локалхоста, ему требуется включенная поддержка cookie, что по современным стандартам безопасности возможно только если и сайт и приложение, находящиеся на разных доменах, работают по протоколу HTTPS доверенного уровня и для сессионных кук включен параметр Secure и выключен SameSite. Поэтому нам придется немного заморочиться, что бы это все заработало в Spring Boot-приложении даже если речь идет о тестовых средах.  


Для примера возьмем сервер с ip-адресом10.5.44.89, на котором в докере будет работать Jmix приложение и статический сайт. Открываться они будут через фронтенд-прокси на nginx, отдающий их страницы по HTTPS. 

В продуктиве вы можете использовать купленные или сгенерированные LetsEncrypt/ACME сертификаты, но для наших тестовых задач, мы придумаем фиктивные доменные имена и добавим на локальном компьютере в файле /etc/hosts ассоциацию с ними для ip-адреса сервера. 

10.5.44.89 app.jmix site.jmix 

Теперь, когда мы будем вводить в браузере адрес https://app.jmix, он будет запрашивать наш тестовый сервер. 

Для удобства работы с сервером установим также публичную часть SSH-ключа на сервер выполнив команду: 

ssh-copy-id root@10.5.44.89

Сайт

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

 (function () {      let link = document.querySelector('a.app-link');      link.addEventListener('click', (event) => {          event.stopPropagation();          event.preventDefault();          let dialog = document.createElement('dialog');          dialog.style.width = '90%';          dialog.style.height = '80%';          dialog.insertAdjacentHTML('beforeend', `              <iframe src="https://app.jmix/pub-page" frameborder="0" height="100%" scrolling="no" width="100%" allow="cookies"></iframe>          `);          document.body.appendChild(dialog);          dialog.showModal();      });  })(); 

Если у вас нет сайта, этот код можно разместить в минимальном HTML-файле внутри тега script, а выше добавить тег ссылки: 

<a href=”#” class=”app-link”>Open</a>

Приложение

Современные браузеры не позволят встраивать приложения одних хостов в другие без указания специальных разрешающих заголовков по стандартам безопасности, на момент написания статьи это заголовок Content-Security-Policy и его параметр frame-ancestors, в котором мы должны перечислить все хосты, разрешенные для встраивания экранов нашего приложения.

Новые версии Spring Security позволяют добавлять конфигурации более модульно — по одной для каждой решаемой задачи и без борьбы с огромными цепочками вызовов.

Сделать это в Jmix или SpringBoot можно добавив Spring Security конфигурацию: 

import io.jmix.core.JmixSecurityFilterChainOrder;  import org.springframework.beans.factory.annotation.Value;  import org.springframework.context.annotation.Bean;  import org.springframework.context.annotation.Configuration;  import org.springframework.core.annotation.Order;  import org.springframework.security.config.annotation.web.builders.HttpSecurity;  import org.springframework.security.web.SecurityFilterChain;      @Configuration  public class IFrameSecurityConfiguration {        @Value("${application.iframeancestors}")      String iframeAncestors;        @Bean      @Order(JmixSecurityFilterChainOrder.CUSTOM)      SecurityFilterChain anonChatbotFilterChain(HttpSecurity http) throws Exception {          http.securityMatcher("/pub-page").headers(headers -> {              headers.contentSecurityPolicy(csp -> csp.policyDirectives("frame-ancestors %s".formatted(iframeAncestors)));                  });          return http.build();      }  } 

Теперь мы можем управлять перечнем разрешенных хостов через конфигурационный параметр приложения applcation.iframeancestors, укажем ему значение переменной $IFRAME_ANCESTORS для подстановки контейнерной системой. 

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

На нашем сайте атрибут src айфрейма ведет на https://app.jmix/pub-page. Чтобы сделать публично доступную страницу с таким адресом, из студии создадим новый экран с анотацией  @Route(“pub-page”) и добавим к нему также анотацию @AnonymousAllowed включающую экрану публичный доступ без авторизации. 

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

./gradlew -Pvaadin.productionMode=true bootBuildImage -imageName=sample-registry/app 

Для этого добавим docker-compose.yml 

services:    app-postgres:      container_name: app-postgres      image: postgres:latest      ports:        - "5432:5432"      volumes:        - postgres:/var/lib/postgresql/data      environment:        - POSTGRES_USER=app        # change this        - POSTGRES_PASSWORD=app        - POSTGRES_DB=app    app:      container_name: app      image: sample-registry/app      ports:        - "8081:8080"      environment:        - SITE_URL=https://app.jmix        - DB_HOST=app-postgres        - DB_USER=app        # right hacking now!        - DB_PASSWORD=app        - DB_PORT=5432        - DB_NAME=app        - IFRAME_ANCESTORS=site.jmix        depends_on:        - app-postgres  volumes:    postgres: {}    

Для того чтобы переменные из docker-compose подставлялись, надо заменить ими хардкод в файла application.properties или   

Запускаться он будет командой: 

docker compose -f docker/docker-compose.yml up –d 

А смотреть логи можно командой: 

docker logs --tail -f app 

SSL-сертификаты

Для более удобного использования выпущенных самостоятельно сертификатов, установим утилиту mkcert. Ее дистрибутивы есть под все популярные операционные системы. 

На сервере сгенерируем корневой сертификат 

mkcert --install 

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

mkcert -key-file /etc/nginx/ssl/mkcert/key.pem -cert-file /etc/nginx/ssl/mkcert/cert.pem app.jmix site.jmix 10.5.44.89 

Установим сертификат и ключ сервера на локальный компьютер. Для этого на нем надо также установить утилиту mkcert. 

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

mkcert --CAROOT 

Должно показаться что-то типа:

/root/.local/share/mkcert

Теперь по scp заберем файлы на локальный компьютер

scp root@10.5.44.89:/root/.local/share/mkcert/rootCA.pem /home/user/.local/share/mkcert  scp root@10.5.44.89:/root/.local/share/mkcert/rootCA-key.pem /home/user/.local/share/mkcert 

И на локальном ПК выполним установку скопированных сертификатов

mkcert --import

Настройка фронтенд-сервера

На сервере установим nginx и создадим конфигурацию /etc/nginx/sites-available/10.5.44.89.conf  

upstream jmix-app { server 172.17.0.1:8081; }      # app.jmix http configuration   server {      listen 80;     server_name app.jmix;      location / {      proxy_pass http://jmix-app;      proxy_set_header Host $host;      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;           }   }    # app.jmix https configuration   server {      listen 443 ssl;     listen [::]:443 ssl;     ssl_certificate /etc/nginx/ssl/mkcert/cert.pem;     ssl_certificate_key /etc/nginx/ssl/mkcert/key.pem;     server_name app.jmix;     location / {      proxy_pass http://jmix-app;      proxy_cookie_path / "/; SameSite=None; HTTPOnly; Secure";      proxy_http_version 1.1;      proxy_cache_bypass $http_upgrade;      proxy_set_header Upgrade $http_upgrade;       proxy_set_header Connection "upgrade";      proxy_set_header Host $host;      proxy_set_header X-Real-IP $remote_addr;      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;      proxy_set_header X-Forwarded-Host $host:443;      proxy_set_header X-Forwarded-Port 443;      proxy_set_header X-Forwarded-Server $host;      proxy_set_header X-Forwarded-Proto https;        }    }       # site.jmix configuration   server {      listen 443 ssl;     listen [::]:443 ssl;     ssl_certificate /etc/nginx/ssl/mkcert/cert.pem;     ssl_certificate_key /etc/nginx/ssl/mkcert/key.pem;     root    /srv/site;    server_name site.jmix;      location / {       try_files $uri $uri/ =404;             }   } 

Что для нас тут важно понимать? Первая же строчка: 

upstream jmix-app { server 172.17.0.1:8080; } 

указывает на ip-адрес 172.17.0.1, это стандартный для докера внешний и внутренний для ОС адрес. 

Слещующая строчка: 

    proxy_cookie_path / "/; SameSite=None; HTTPOnly; Secure"; 

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

Если возможности влиять на конфигурацию фронтенд-сервера нет, можно добавить в конфигурацию приложения applicaiton.properties следующие строчки: 

server.servlet.session.cookie.same-site=none  server.servlet.session.cookie.secure=true 

Директивы ssl_certificate и ssl_certificate_key в конфигурации nginx 

  ssl_certificate /etc/nginx/ssl/mkcert/cert.pem;     ssl_certificate_key /etc/nginx/ssl/mkcert/key.pem; 

указывают на сертификат и ключ, которые мы создали. 

Строчка вида: 

  root    /srv/site; 

указывает на путь на сервере, в котором должен лежать код статичного сайта или просто index.html с ссылкой и джаваскриптом открывающим айфрейм, с которого мы начали эту историю. 

Строчки: 

    proxy_set_header Upgrade $http_upgrade;       proxy_set_header Connection "upgrade"; 

Задают конфигурацию для активации вебсокета, используемого Vaadin-фронтендом.  

Осталось перезапустить браузер, открыть в нем адрес https://site.jmix и на странице нажать кнопку Open. 


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *