Предположим, у нас появилась задача встроить какой-то функционал, реализуемый системой на 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. 

Комментарии (0)