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