Eclipse,SpringBoot,Embedded Tomcat

18.05.2021
Последняя модификация:25.08.2021
Иванов Аркадий

 

Здесь я опишу:

- Как сделать заготовку для Maven SpringBoot проекта и импортировать его в Eclipse.

- Как сменить порт встроенного Tomcat.

- Добавить к проекту Thymeleaf.

- Добавить basic аутентификацию к проекту.

- Добавить form аутентификацию к проекту.

- Настройка формы аутентификации.

- Сделать доступ только по https.

 

Создание SpringBoot проекта.

SpringBoot позволяет развёртывать и запускать программу со встроенным сервером приложений как обычную Java-программу, указав для исполнения просто JAR-файл.

Используя Eclipse и Maven это делается так:

1. На странице http://start.spring.io/ создаём набор файлов, которые представляют собой Maven-проект:

Здесь надо:

- выбрать "Maven project" и Java, Packaging:Jar
- указать нужные Group, Artifact
- добавить зависимости "WEB" и "DEVELOPER TOOLS"

2. Нажать GENERATE и получить файл example.zip

3. Раскрыть example.zip

4. В Eclipse: Import - Existing Maven Projects, указать каталог, куда раскрыт example.zip, импортировать проект.

5. В проекте добавить пакет ru.arccomm.springboot.example.controller

6. Создать новый контроллер HelloWorldController:

package ru.arccomm.springboot.example.controller;

import ...

@RestController
public class HelloWorldController {

  @RequestMapping("/helloworld")
  String helloworld() {
    return "Hello, world!";
  }
}

7. Запустить приложение в Eclipse как Java application.

8. В браузере в строке адреса указать:

localhost:8080/helloworld

    В ответе от сервера получаем:

Hello, world!

Ладно, SpringBoot со встроенным Tomcat-ом из Eclipse запускается.

9. В окне терминала перейду в каталог example с моим Maven-проектом и дам команду:

mvn clean install

в результате в подкаталоге target получаю файл example-0.0.1-SNAPSHOT.jar

10. Это уже законченное приложение, которое можно запустить из командной строки:

java -jar example-0.0.1-SNAPSHOT.jar

Если обратиться из браузера к ранее указанному адресу, получим тот же ответ.
Т.е. мы получили WEB-приложение со встроенным Tomcat-сервером внутри одного JAR-файла.

 

Смена порта Tomcat.

По умолчанию Tomcat использует порт 8080. Указать другой можно в файле src/main/resources/application.properties:

server.port=8180

 

Добавляю Thymeleaf к проекту.

Thymeleaf используется для динамического изменения контента при отдаче клиенту HTML-страницы.

Подключение Thymeleaf в проект:

1. В POM.xml вставляется зависимость:

  <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>

Этого достаточно, чтобы все классы Thymeleaf были инициализированы в приложении.

 

2. В контроллере меняется аннотация RestController на Controller:

package ru.arccomm.springboot.example.controller;

import ...

@Controller
public class HelloWorldController {

  @RequestMapping("/th")
  String th() {
    return "hello";
  }
}

return "hello" указывает, что перед возвратом клиенту HTML-страницы, файл

src/main/resources/templates/hello.html будет обработан Thymeleaf.

 

3. Файлы шаблонов Thymeleaf создаются в каталоге src/main/resources/templates. Для этого примера я создал hello.html:

<!DOCTYPE html>
<html lang="ru" xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Это пример</title>
</head>
<body>
<p>Thymeleaf hello</p>
</body>
</html>

Thymeleaf практически ничего не будет заменять в этом шаблоне, поскольку это чистый HTML-код.

 

4. Обращение из браузера к localhost:8080/hello даст:

Thymeleaf hello

 

Базовая аутентификация

Делаю доступ к страницам проекта через login/password.

 

В ресурсах у меня теперь есть:

templates/hello.html

templates/open/open.html

templates/adm/adm.html

Для каждого из этих ресурсов я сделаю отличающиеся права доступа.

 

1. В pom.xml добавляю зависимость:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

2. Создаю новый пакет ru.arccomm.springboot.example.config и в нём класс SecurityConfig который унаследован от WebSecurityConfigurerAdapter :

package ru.arccomm.springboot.example.config;

import ...

@Configuration

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    // login/passw хранятся в памяти
    // Здесь заданы login,passw,role для каждого пользователя системы
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth
        .inMemoryAuthentication()
        .withUser("normaluser").password(passwordEncoder().encode("123456")).roles("USERS")
        .and()
        .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN")
        ;
    }

    @Override
    // Указываю, что доступ ко всем страницам должен проходить только после аутентификации
    protected void configure(final HttpSecurity http) throws Exception {
        http
        .csrf().disable()
        .authorizeRequests()
        .antMatchers("/adm/**").hasRole("ADMIN")   // /adm/** только для пользователей группы "ADMIN"
        .antMatchers("/open/**").permitAll()       // /open/** разрешено всем без входа в систему
        .anyRequest().authenticated()              // остальные страницы только после входа в систему
        .and()
        .httpBasic();                              // использую Basic аутентификацию
        ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

- Класс обозначен аннотациями @Configuration и @EnableWebSecurity. Spring будет использовать его при старте.

- В config() для мененеджера аутентификации данные двух пользователей "normaluser" и "admin" хранятся в памяти. При создании записей о пользователях и паролях используется шифрование Bcrypt. У этих пользователей заданы разные роли.

- В config() для http указаны права доступа к конкретным группам ресурсов. Здесь важна последовательность вызова разрешений. Например, к ресурсу "/open/*" можно обращаться любому пользователи, а следом идёт требование аутентификации для всех остальных ресурсов. Последовательность указания прав доступа важна.

 

3. Описание маппирования ресурсов:

package ru.arccomm.springboot.example.controller;

import ...

@Controller
public class HelloWorldController {

  @RequestMapping("/hello")
  String hello() {
    return "hello";
  }

  @RequestMapping("/adm/adm")
  String adm() {
    return "adm/adm";
  }

  @RequestMapping("/open/open")
  String open() {
    return "open/open";
  }
}

 

4. Доступ к ресурсам из браузера:

- /open/open будет доступен всем пользователям без аутентификации

- /hello потребует аутентификации. Можно аутентифицироваться как "normaluser", так и "admin".

- /adm/adm разрешит доступ только "admin" после успешной аутентификации.

Сами HTML-файлы я здесь не привожу, чтобы не раздувать описание.

 

5. Basic authentication не очень-то предполагает logout. Браузеры будут упрямо посылать заголовки и cookie с данными аутентификации.

Полноценный вход/выход пользователя в приложении нужно реализовывать через FORM authentication.

 

Добавить FORM аутентификацию к проекту.

Basic authentication не позволяет по простому выйти пользователю из приложения. Простейшая Form authentication делается в классе SecurityConfig небольшой правкой:

    @Override
    // Указываю, что доступ ко всем страницам должен проходить только после аутентификации
    protected void configure(final HttpSecurity http) throws Exception {
        http
        .csrf().disable()
        .authorizeRequests()
        .antMatchers("/adm/*").hasRole("ADMIN")
        .antMatchers("/open/*").permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin()                                 // использую страндартную форму Spring для login/password
        .permitAll()
        ;
    }

Больше ничего не исправляю.

При обращении к любой защищённой странице получаю приглашение ввести имя и пароль:

После успешной аутентификации попадаю на ту страницу, которая была в запросе.

 

Чтобы выйти из приложения достаточно в строке адреса указать страницу localhost:8180/logout

 

Настройка формы аутентификации

Стандартная форма для входа в систему нас не всегда удоволетворяет. Делаю свою:

1. Класс SecurityConfig теперь выглядит так:

 

package ru.arccomm.springboot.example.config;

import ...

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    // login/passw хранятся в памяти
    // Здесь заданы login,passw,role для каждого пользователя системы
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth
        .inMemoryAuthentication()
        .withUser("normaluser").password(passwordEncoder().encode("123456")).roles("USERS")
        .and()
        .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN")
        ;
    }

    @Override
    // Указываю, что доступ ко всем страницам должен проходить только после аутентификации
    protected void configure(final HttpSecurity http) throws Exception {
        http
        .csrf().disable()
        .authorizeRequests()
        .antMatchers("/adm/*").hasRole("ADMIN")
        .antMatchers("/open/*").permitAll()
        .anyRequest().authenticated()
        .and()

        .formLogin()
        .loginPage("/login")                         // url для запроса login
        .failureUrl("/login?error=true")             // url, куда отправиться при ошибке аутентификации
        .defaultSuccessUrl("/hello")                 // страница по умолчанию, если пользователь просто обратился к странице "/login"
        .permitAll()

        .and()
        .logout()
        .logoutUrl("/logout")                        // url для выхода из системы
        .logoutSuccessUrl("/logoutDone")             // куда отправиться после успешного выхода из системы
        .permitAll()
        ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  

Новые методы я прокомментировал в коде. Здесь заданы параметры для login и для logout.

 

2. Класс HelloWorldController теперь выглядит так:

package ru.arccomm.springboot.example.controller;

import ...

@Controller

public class HelloWorldController {

  @RequestMapping("/hello")
  String th() {
    return "hello";
  }

  @RequestMapping("/adm/adm")
  String adm() {
    return "adm/adm";
  }

  @RequestMapping("/open/open")
  String open() {
    return "open/open";
  }

// Login form
  @RequestMapping("/login")
  public String login() {
      return "auth/loginForm";
  }


//Successfull logout page
 @RequestMapping("/logoutDone")
 public String logoutDone() {
     return "auth/logoutDone";
 }
}

 

Здесь добавлено маппирование "/login" и "/logoutDone".

 

3. В каталоге src/main/resources/templates добавляю подкаталог auth, в котором будут все HTML-файлы, касающиеся аутентификации.

Файл templates/auth/loginForm.html:

 

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Login form</title>
</head>
<body>
    <h2>Login form</h2>
    <p style="color:red" th:if="${param.error}">Error</p>
    <form th:action="@{/login}" method="post">
        <label for="username">Login: </label>
        <input type="text" id="username" name="username" autofocus="autofocus" />
        <br><label for="password">Passw: </label>
        <input type="password" id="password" name="password" />
        <br><input id="submit" type="submit" value="Log in" />
    </form>
</body>
</html>

При ошибке аутентификации будет вызвана страница "/login?error=true". Spring передаст в форму саму ошибку. Текст ошибки высветится красным. Обычно это "Invalid username and password".

 

Файл templates/auth/logoutDone.html:

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Logout done</title>
</head>
<body>
<h2>Logout done</h2>
</body>
</html>

 

HTML/CSS-красоту я здесь не привожу. Это уже на вкус и цвет разработчиков.

 

Сделать доступ только по https.

Сегодня аутентифицироваться в приложении по открытому http-протоколу гарантированно небезопасно. Поэтому добавлю в проект доступ к нему только через защищённый HTTPS.

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

Вот командная строка в Linux:

keytool -genkey -alias tomcat -keyalg RSA -keysize 2048 -keystore keystore.file -validity 3650

  - Пароль ввожу changeit
  - first and last name ввожу localhost

В домашнем каталоге пользователя будет создан keystore.file

2. Созданный keystore.file из домашнего каталога пользователя надо скопировать в каталог src/main/resources приложения.

3. В файле src/main/resources/application.properties добавляю и изменяю:

server.port=8543
server.ssl.key-store-type=JKS
server.ssl.key-store=classpath:keystore.file
server.ssl.key-store-password=changeit
server.ssl.key-alias=tomcat
security.require-ssl=true

Порт заменил.
Формат хранилища ключей JKS.
Указал, где брать файл хранилища - keystore.file
Указал пароль.
Имя сертификата - tomcat, посколку он нужен именно Tomcat-у, встроенному в наше приложение.
Указал, что при соединеннии требуется использовать именно SSL-коннектор для Tomcat-а.

4. Теперь подключаться надо по URL:

https://localhost:8543/hello

5. Браузер будет рычать на самоподписанный сертификат, но защищённый канал работать будет.
Чтобы пользователям не приходилось проходить процедуру согласия на самоподписанный сертификат, стоит получить его для вашего сайта на Letsencrypt.