«1. Обзор

В нашей последней статье о Spring Cloud мы добавили поддержку Zipkin в наше приложение. В этой статье мы собираемся добавить интерфейсное приложение в наш стек.

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

Мы будем писать это приложение, используя Angular и Bootstrap. Стиль кода Angular 4 очень похож на кодирование приложения Spring, что является естественным кроссовером для разработчика Spring! Хотя внешний код будет использовать Angular, содержание этой статьи можно легко распространить на любой интерфейсный фреймворк с минимальными усилиями.

В этой статье мы собираемся создать приложение Angular 4 и подключить его к нашим облачным сервисам. Мы продемонстрируем, как интегрировать вход между SPA и Spring Security. Мы также покажем, как получить доступ к данным нашего приложения, используя поддержку Angular для связи по протоколу HTTP.

2. Изменения шлюза

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

2.1. Обновление HttpSecurity

Во-первых, давайте обновим метод configure(HttpSecurity http) в нашем классе шлюза SecurityConfig.java:

@Override
protected void configure(HttpSecurity http) {
    http
      .formLogin()
      .defaultSuccessUrl("/home/index.html", true)
      .and()
    .authorizeRequests()
      .antMatchers("/book-service/**", "/rating-service/**", "/login*", "/")
      .permitAll()
      .antMatchers("/eureka/**").hasRole("ADMIN")
      .anyRequest().authenticated()
      .and()
    .logout()
      .and()
    .csrf().disable();
}

Во-первых, мы добавим URL-адрес успеха по умолчанию, указывающий на /home/index.html, так как это будет где живет наше приложение Angular. Затем мы настраиваем сопоставители муравьев, чтобы разрешить любой запрос через шлюз, кроме ресурсов Eureka. Это делегирует все проверки безопасности внутренним службам.

Затем мы удалили URL-адрес успешного выхода из системы, так как перенаправление по умолчанию обратно на страницу входа будет работать нормально.

2.2. Добавить основную конечную точку

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

В проекте шлюза добавьте класс AuthenticationController:

@RestController
public class AuthenticationController {
 
    @GetMapping("/me")
    public Principal getMyUser(Principal principal) {
        return principal;
    }
}

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

2.3. Добавить целевую страницу

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

В src/main/resources/static добавим файл index.html со ссылкой на страницу входа:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Book Rater Landing</title>
</head>
<body>
    <h1>Book Rater</h1>
    <p>So many great things about the books</p>
    <a href="/login">Login</a>
</body>
</html>

3. Angular CLI и начальный проект

Перед запуском нового проекта Angular обязательно установите последние версии Node.js и npm.

3.1. Установите Angular CLI

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

npm install -g @angular/cli

Это загрузит и установит CLI глобально.

3.2. Установите новый проект

Пока еще находитесь в терминале, перейдите к проекту шлюза и перейдите в папку gateway/src/main. Создайте каталог под названием «angular» и перейдите к нему. Отсюда запустите:

ng new ui

Наберитесь терпения; CLI настраивает новый проект и загружает все зависимости JavaScript с помощью npm. Нередко этот процесс занимает много минут.

Команда ng является ярлыком для Angular CLI, новый параметр указывает этому CLI создать новый проект, а команда ui дает нашему проекту имя.

3.3. Запустите проект

После завершения новой команды. Перейдите к созданной папке ui и запустите:

ng serve

После сборки проекта перейдите по адресу http://localhost:4200. Мы должны увидеть это в браузере:

Поздравляем! Мы только что создали приложение Angular!

3.4. Установите Bootstrap

Давайте воспользуемся npm для установки bootstrap. Из каталога ui выполните эту команду:

npm install [email protected] --save

Это загрузит загрузчик в папку node_modules.

«В каталоге ui откройте файл .angular-cli.json. Это файл, который настраивает некоторые свойства нашего проекта. Найдите свойство apps \u003e styles и добавьте расположение файла нашего CSS-класса Bootstrap:

"styles": [
    "styles.css",
    "../node_modules/bootstrap/dist/css/bootstrap.min.css"
],

Это даст указание Angular включить Bootstrap в скомпилированный CSS-файл, созданный вместе с проектом.

3.5. Установите выходной каталог сборки

Далее нам нужно указать Angular, куда поместить файлы сборки, чтобы наше приложение Spring Boot могло их обслуживать. Spring Boot может обслуживать файлы из двух мест в папке ресурсов:

    src/main/resources/static src/main/resource/public

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

Снова откройте файл .angular-cli.json и найдите свойства apps \u003e outDir. Обновите эту строку:

"outDir": "../../resources/static/home",

Если проект Angular находится в папке src/main/angular/ui, он будет собран в папку src/main/resources/public. Если приложение находится в другой папке, эту строку необходимо изменить, чтобы правильно указать местоположение.

3.6. Автоматизируйте сборку с помощью Maven

Наконец, мы настроим автоматическую сборку, которая будет запускаться при компиляции нашего кода. Эта задача ant будет запускать задачу сборки Angular CLI всякий раз, когда запускается «mvn compile». Добавьте этот шаг в POM.xml шлюза, чтобы гарантировать, что каждый раз, когда мы компилируем, мы получаем последние изменения пользовательского интерфейса:


<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <phase>generate-resources</phase>
            <configuration>
                <tasks>
                    <exec executable="cmd" osfamily="windows"
                      dir="${project.basedir}/src/main/angular/ui">
                        <arg value="/c"/>
                        <arg value="ng"/>
                        <arg value="build"/>
                    </exec>
                    <exec executable="/bin/sh" osfamily="mac"
                      dir="${project.basedir}/src/main/angular/ui">
                        <arg value="-c"/>
                        <arg value="ng build"/>
                    </exec>
                </tasks>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Мы должны отметить, что эта настройка требует, чтобы Angular CLI был доступен в пути к классам. Отправка этого скрипта в среду, в которой нет этой зависимости, приведет к сбоям сборки.

Теперь давайте начнем создавать наше приложение Angular!

4. Angular

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

Пользователи имеют форму входа, где они могут ввести свое имя пользователя и пароль.

Затем мы используем их учетные данные для создания токена аутентификации base64 и запрашиваем конечную точку «/me». Конечная точка возвращает объект Principal, содержащий роли этого пользователя.

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

Давайте посмотрим, как это делается!

4.1. Шаблон

В проекте шлюза перейдите к src/main/angular/ui/src/app и откройте файл app.component.html. Это первый шаблон, который загружает Angular, и именно на него наши пользователи попадут после входа в систему.

Здесь мы добавим код для отображения панели навигации с формой входа:

<nav class="navbar navbar-toggleable-md navbar-inverse fixed-top bg-inverse">
    <button class="navbar-toggler navbar-toggler-right" type="button" 
      data-toggle="collapse" data-target="#navbarCollapse" 
      aria-controls="navbarCollapse" aria-expanded="false" 
      aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <a class="navbar-brand" href="#">Book Rater 
        <span *ngIf="principal.isAdmin()">Admin</span></a>
    <div class="collapse navbar-collapse" id="navbarCollapse">
    <ul class="navbar-nav mr-auto">
    </ul>
    <button *ngIf="principal.authenticated" type="button" 
      class="btn btn-link" (click)="onLogout()">Logout</button>
    </div>
</nav>

<div class="jumbotron">
    <div class="container">
        <h1>Book Rater App</h1>
        <p *ngIf="!principal.authenticated" class="lead">
        Anyone can view the books.
        </p>
        <p *ngIf="principal.authenticated && !principal.isAdmin()" class="lead">
        Users can view and create ratings</p>
        <p *ngIf="principal.isAdmin()"  class="lead">Admins can do anything!</p>
    </div>
</div>

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

Утверждения вроде (ngSubmit)=“onLogin(f)” просто указывают, что при отправке формы вызывается метод “onLogin(f)” и передается форма этой функции. В div jumbotron у нас есть теги абзаца, которые будут отображаться динамически в зависимости от состояния нашего основного объекта.

Далее давайте напишем файл Typescript, который будет поддерживать этот шаблон.

4.2. Typescript

В том же каталоге откройте файл app.component.ts. В этот файл мы добавим все свойства и методы машинописного текста, необходимые для того, чтобы наш шаблон функционировал:

import {Component} from "@angular/core";
import {Principal} from "./principal";
import {Response} from "@angular/http";
import {Book} from "./book";
import {HttpService} from "./http.service";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    selectedBook: Book = null;
    principal: Principal = new Principal(false, []);
    loginFailed: boolean = false;

    constructor(private httpService: HttpService){}

    ngOnInit(): void {
        this.httpService.me()
          .subscribe((response: Response) => {
              let principalJson = response.json();
              this.principal = new Principal(principalJson.authenticated,
              principalJson.authorities);
          }, (error) => {
              console.log(error);
        });
    }

    onLogout() {
        this.httpService.logout()
          .subscribe((response: Response) => {
              if (response.status === 200) {
                  this.loginFailed = false;
                  this.principal = new Principal(false, []);
                  window.location.replace(response.url);
              }
           }, (error) => {
               console.log(error);
       });
    }
}

Этот класс подключается к методу жизненного цикла Angular, ngOnInit(). В этом методе мы вызываем конечную точку /me, чтобы получить текущую роль и состояние пользователя. Это определяет то, что пользователь видит на главной странице. Этот метод будет запускаться всякий раз, когда будет создан этот компонент, что является прекрасным временем для проверки свойств пользователя на наличие разрешений в нашем приложении.

У нас также есть метод onLogout(), который выводит пользователя из системы и восстанавливает исходные настройки состояния этой страницы.

«Хотя здесь происходит какое-то волшебство. Свойство httpService, объявленное в конструкторе. Angular внедряет это свойство в наш класс во время выполнения. Angular управляет одноэлементными экземплярами классов обслуживания и внедряет их с помощью внедрения конструктора, как и Spring!

Далее нам нужно определить класс HttpService.

4.3. HttpService

В том же каталоге создайте файл с именем «http.service.ts». В этом файле добавьте этот код для поддержки методов входа и выхода:

import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {Response, Http, Headers, RequestOptions} from "@angular/http";
import {Book} from "./book";
import {Rating} from "./rating";

@Injectable()
export class HttpService {

    constructor(private http: Http) { }

    me(): Observable<Response> {
        return this.http.get("/me", this.makeOptions())
    }

    logout(): Observable<Response> {
        return this.http.post("/logout", '', this.makeOptions())
    }

    private makeOptions(): RequestOptions {
        let headers = new Headers({'Content-Type': 'application/json'});
        return new RequestOptions({headers: headers});
    }
}

В этом классе мы внедряем другую зависимость, используя конструкцию DI Angular. На этот раз это класс Http. Этот класс обрабатывает всю HTTP-связь и предоставляется нам фреймворком.

Каждый из этих методов выполняет HTTP-запрос с использованием HTTP-библиотеки angular. Каждый запрос также указывает тип содержимого в заголовках.

Теперь нам нужно сделать еще одну вещь, чтобы зарегистрировать HttpService в системе внедрения зависимостей. Откройте файл app.module.ts и найдите свойство provider. Добавьте HttpService в этот массив. Результат должен выглядеть так:

providers: [HttpService],

4.4. Add Principal

Далее давайте добавим наш объект Principal DTO в наш код Typescript. В тот же каталог добавьте файл с именем «principal.ts» и добавьте следующий код:

export class Principal {
    public authenticated: boolean;
    public authorities: Authority[] = [];
    public credentials: any;

    constructor(authenticated: boolean, authorities: any[], credentials: any) {
        this.authenticated = authenticated;
        authorities.map(
          auth => this.authorities.push(new Authority(auth.authority)))
        this.credentials = credentials;
  }

    isAdmin() {
        return this.authorities.some(
          (auth: Authority) => auth.authority.indexOf('ADMIN') > -1)
    }
}

export class Authority {
    public authority: String;

    constructor(authority: String) {
        this.authority = authority;
    }
}

Мы добавили класс Principal и класс Authority. Это два класса DTO, очень похожие на POJO в приложении Spring. Из-за этого нам не нужно регистрировать эти классы в системе DI в angular.

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

4.5. 404 Обработка

Давайте вернемся к коду Java для службы шлюза. В том месте, где находится класс GatewayApplication, добавьте новый класс с именем ErrorPageConfig:

@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
 
    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,
          "/home/index.html"));
    }

}

Этот класс идентифицирует любой ответ 404 и перенаправит пользователя на «/home/index.html». В одностраничном приложении именно так мы обрабатываем весь трафик, не направляемый на выделенный ресурс, поскольку клиент должен обрабатывать все навигационные маршруты.

Теперь мы готовы запустить это приложение и посмотреть, что мы создали!

4.6. Сборка и просмотр

Теперь запустите «mvn compile» из папки шлюза. Это скомпилирует наш исходный код Java и создаст приложение Angular в общедоступной папке. Запустим другие облачные приложения: config, discovery и zipkin. Затем запустите проект шлюза. Когда служба запустится, перейдите по адресу http://localhost:8080, чтобы увидеть наше приложение. Мы должны увидеть что-то вроде этого:

Далее переходим по ссылке на страницу авторизации:

Войдите в систему, используя учетные данные пользователя/пароля. Нажмите «Войти», и мы должны быть перенаправлены на /home/index.html, где загружается наше одностраничное приложение.

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

Выглядит хорошо! Теперь мы вошли в систему как администратор.

5. Заключение

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

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

Если вы хотите посмотреть, как устроена остальная часть сайта, вы можете найти исходный код на Github.