Статьи

React.js и Spring Data REST: безопасность

В  предыдущем сеансе приложение динамически отвечало на обновления от других пользователей с помощью встроенных обработчиков событий Spring Data REST и поддержки WebSocket в Spring Framework. Но ни одно приложение не будет полным без обеспечения всей безопасности, чтобы только надлежащие пользователи имели доступ к пользовательскому интерфейсу и ресурсам, стоящим за ним.

Не стесняйтесь  взять код  из этого хранилища и следуйте инструкциям. Этот сеанс основан на приложении предыдущего сеанса с добавлением дополнительных вещей.

Добавление Spring Security в проект

Прежде чем начать, вам нужно добавить пару зависимостей в файл pom.xml вашего проекта:

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

Это включает Spring Boot Spring Security Starter, а также некоторые дополнительные теги Thymeleaf для поиска безопасности на веб-странице.

Определение модели безопасности

На прошлой сессии вы работали с хорошей системой начисления заработной платы. Удобно объявить что-то на бэкэнде и позволить Spring Data REST сделать тяжелую работу. Следующим шагом является моделирование системы, в которой необходимо установить меры безопасности.

Если это система начисления заработной платы, то только менеджеры будут иметь к ней доступ. Итак, начните с моделирования  Manager объекта:

@Data
@ToString(exclude = "password")
@Entity
public class Manager {

public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();

private @Id @GeneratedValue Long id;

private String name;

private @JsonIgnore String password;

private String[] roles;

public void setPassword(String password) {
this.password = PASSWORD_ENCODER.encode(password);
}

protected Manager() {}

public Manager(String name, String password, String... roles) {

this.name = name;
this.setPassword(password);
this.roles = roles;
}

}

  • PASSWORD_ENCODER это средство для шифрования новых паролей или для ввода введенных паролей и их шифрования перед сравнением.
  • idnamepasswordИ  roles определить параметры , необходимые для ограничения доступа.
  • Индивидуальный  setPassword() гарантирует, что пароли никогда не хранятся в открытом виде.

При разработке уровня безопасности необходимо помнить о ключевых моментах. Защитите правильные биты данных (например, пароли) и НЕ позволяйте им печататься на консоли, в журналах или экспортироваться через сериализацию JSON.

  • @ToString(exclude = "password") гарантирует, что сгенерированный Lombok метод toString () НЕ распечатает пароль.
  • @JsonIgnore применяется к полю пароля защищает от сериализации этого поля Джексоном.

Создание репозитория менеджера

Spring Data настолько хорош в управлении сущностями. Почему бы не создать хранилище для этих менеджеров?

@RepositoryRestResource(exported = false)
public interface ManagerRepository extends Repository<Manager, Long> {

Manager save(Manager manager);

Manager findByName(String name);

}

Вместо расширения привычного  CrudRepositoryвам не нужно так много методов. Вместо этого вам нужно сохранить данные (которые также используются для обновлений), и вам нужно искать существующих пользователей. Следовательно, вы можете использовать минимальный Repository интерфейс маркера Spring Data Common  . Он поставляется без предопределенных операций.

Spring Data REST по умолчанию экспортирует любой найденный репозиторий. Вы НЕ хотите, чтобы этот репозиторий был открыт для операций REST! Примените @RepositoryRestResource(exported = false) аннотацию, чтобы заблокировать ее экспорт. Это предотвращает обслуживание хранилища, а также любые метаданные.

Связь сотрудников с их менеджерами

Последний аспект моделирования безопасности — связать сотрудников с менеджером. В этом домене сотрудник может иметь одного менеджера, а менеджер может иметь несколько сотрудников:

@Data
@Entity
public class Employee {

private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;

private @Version @JsonIgnore Long version;

private @ManyToOne Manager manager;

private Employee() {}

public Employee(String firstName, String lastName, String description, Manager manager) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
this.manager = manager;
}
}

  • Атрибут менеджера связан через JPA  @ManyToOne. Менеджер не нуждается в том, @OneToMany потому что вы не определили необходимость искать это.
  • Вызов конструктора утилит обновлен для поддержки инициализации.

Обеспечение сотрудников их менеджерам

Spring Security поддерживает множество опций при определении политик безопасности. В этом сеансе вы хотите ограничить такие вещи, чтобы ТОЛЬКО менеджеры могли просматривать данные заработной платы сотрудников, а операции сохранения, обновления и удаления ограничивались менеджером сотрудника. Другими словами, любой менеджер может войти в систему и просмотреть данные, но только менеджер данного сотрудника может вносить любые изменения.

@PreAuthorize("hasRole('ROLE_MANAGER')")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {

@Override
@PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name")
Employee save(@Param("employee") Employee employee);

@Override
@PreAuthorize("@employeeRepository.findOne(#id)?.manager?.name == authentication?.name")
void delete(@Param("id") Long id);

@Override
@PreAuthorize("#employee?.manager?.name == authentication?.name")
void delete(@Param("employee") Employee employee);

}

@PreAuthorize в верхней части интерфейса доступ ограничен людьми с  ROLE_MANAGER .

Вкл.  save(), Либо менеджер сотрудника имеет значение null (первоначальное создание нового сотрудника, когда менеджер не был назначен), либо имя менеджера сотрудника совпадает с именем аутентифицированного в настоящее время пользователя. Здесь вы используете  SpEL-выражения Spring Security  для определения доступа. Это идет с удобным «?.» свойство навигатор для обработки нулевых проверок. Также важно отметить использование  @Param(…) аргументов on для связи HTTP-операций с методами.

Если  delete()метод либо имеет доступ к сотруднику, либо в случае, если у него есть только идентификатор, он должен найти  employeeRepository  в контексте приложения, выполнить a findOne(id), а затем проверить менеджера в отношении текущего пользователя, прошедшего проверку подлинности.

Написание  UserDetails службы

Общей точкой интеграции с безопасностью является определение  UserDetailsService. Это способ подключения хранилища данных вашего пользователя к интерфейсу Spring Security. Spring Security нужен способ поиска пользователей для проверки безопасности, и это мост. К счастью с Spring Data, усилия минимальны:

@Component
public class SpringDataJpaUserDetailsService implements UserDetailsService {

private final ManagerRepository repository;

@Autowired
public SpringDataJpaUserDetailsService(ManagerRepository repository) {
this.repository = repository;
}

@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Manager manager = this.repository.findByName(name);
return new User(manager.getName(), manager.getPassword(),
AuthorityUtils.createAuthorityList(manager.getRoles()));
}

}

SpringDataJpaUserDetailsService реализует Spring Security  UserDetailsService. Интерфейс имеет один метод:  loadByUsername(). Этот метод предназначен для возврата UserDetails объекта, чтобы Spring Security могла запрашивать информацию пользователя.

Поскольку у вас есть  ManagerRepository, нет необходимости писать какие-либо выражения SQL или JPA для извлечения этих необходимых данных. В этом классе он автоматически подключается с помощью инжектора конструктора.

loadByUsername() подключается к пользовательскому поисковику, который вы написали минуту назад  findByName(). Затем он заполняет User экземпляр Spring Security  , который реализует  UserDetailsинтерфейс. Вы также используете Spring Securiy это  AuthorityUtils для перехода от множества ролей на основе строки в Java  List из  GrantedAuthority.

Подключение вашей политики безопасности

В  @PreAuthorize выражении , применяемое в ваше хранилище являются  правилами доступа . Эти правила ни для чего без политики безопасности.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private SpringDataJpaUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(Manager.PASSWORD_ENCODER);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/bower_components/**", "/*.js",
"/*.jsx", "/main.css").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.logout()
.logoutSuccessUrl("/");
}

}

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

  • @EnableWebSecurity сообщает Spring Boot об удалении автоматически настроенной политики безопасности и использовании этой. Для быстрых демонстраций с автоматически настроенной безопасностью все в порядке. Но для чего-то реального, вы должны написать политику самостоятельно.
  • @EnableGlobalMethodSecurity включает защиту на уровне методов с помощью сложных аннотаций @Pre и @Post в Spring Security  .
  • Это расширяет  WebSecsurityConfigurerAdapter, удобный базовый класс для написания политики.
  • Он автоматически подключил  SpringDataJpaUserDetailsService поле ввода, а затем подключил его через  configure(AuthenticationManagerBuilder) метод. PASSWORD_ENCODERОт  Manager также установки.
  • Основная политика безопасности написана на чистом Java с  configure(HttpSecurity).

Политика безопасности требует авторизовать все запросы, используя правила доступа, определенные ранее.

  • Пути, перечисленные в,  antMatchers() получают безусловный доступ, поскольку нет никаких причин блокировать статические веб-ресурсы.
  • Все, что не соответствует, что входит в  anyRequest().authenticated() смысл, требует аутентификации.
  • После настройки этих правил доступа Spring Security приказывает использовать аутентификацию на основе форм, по умолчанию «/» в случае успеха, и предоставлять доступ к странице входа в систему.
  • Базовый логин также настроен с отключенной CSRF. Это в основном для демонстраций и не рекомендуется для производственных систем без тщательного анализа.
  • Выход из системы настроен на переход пользователя в «/».
Предупреждение Базовая аутентификация удобна, когда вы экспериментируете с curl. Использование curl для доступа к системе на основе форм просто утомительно. Важно понимать, что аутентификация с использованием любого механизма через HTTP (не HTTPS) подвергает вас риску прослушивания учетных данных по проводам. CSRF — хороший протокол, чтобы оставить его без изменений. Он просто отключен, чтобы упростить взаимодействие с Бейсиком и скручиванием. В производстве лучше оставить его включенным.

Автоматическое добавление сведений о безопасности

Хороший пользовательский опыт — это когда приложение может автоматически применять контекст. В этом примере, если вошедший в систему менеджер создает новую запись о сотруднике, имеет смысл для этого менеджера владеть ею. В обработчиках событий Spring Data REST пользователю не нужно явно связывать его. Это также гарантирует, что пользователь не случайно записывает неправильный менеджер.

@Component
@RepositoryEventHandler(Employee.class)
public class SpringDataRestEventHandler {

private final ManagerRepository managerRepository;

@Autowired
public SpringDataRestEventHandler(ManagerRepository managerRepository) {
this.managerRepository = managerRepository;
}

@HandleBeforeCreate
public void applyUserInformationUsingSecurityContext(Employee employee) {

String name = SecurityContextHolder.getContext().getAuthentication().getName();
Manager manager = this.managerRepository.findByName(name);
if (manager == null) {
Manager newManager = new Manager();
newManager.setName(name);
newManager.setRoles(new String[]{"ROLE_MANAGER"});
manager = this.managerRepository.save(newManager);
}
employee.setManager(manager);
}
}

@RepositoryEventHandler(Employee.class) помечает этот обработчик событий как применяемый только к Employee объектам. @HandleBeforeCreate Аннотаций дает вам шанс изменить поступающую  Employee запись , прежде чем она будет записана в базу данных.

В этом разделе вы просматриваете контекст безопасности текущего пользователя, чтобы получить имя пользователя. Затем найдите связанный менеджер с помощью  findByName() и примените его к менеджеру. Существует небольшой дополнительный код для создания нового менеджера, если он или она еще не существует в системе. Но это в основном для поддержки инициализации базы данных. В реальной производственной системе этот код должен быть удален, и вместо этого он должен зависеть от администраторов баз данных или службы безопасности, чтобы правильно поддерживать хранилище пользовательских данных.

Предварительная загрузка данных менеджера

Загрузка менеджеров и привязка сотрудников к этим менеджерам довольно проста:

@Component
public class DatabaseLoader implements CommandLineRunner {

private final EmployeeRepository employees;
private final ManagerRepository managers;

@Autowired
public DatabaseLoader(EmployeeRepository employeeRepository,
  ManagerRepository managerRepository) {

this.employees = employeeRepository;
this.managers = managerRepository;
}

@Override
public void run(String... strings) throws Exception {

Manager greg = this.managers.save(new Manager("greg", "turnquist",
"ROLE_MANAGER"));
Manager oliver = this.managers.save(new Manager("oliver", "gierke",
"ROLE_MANAGER"));

SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("greg", "doesn't matter",
AuthorityUtils.createAuthorityList("ROLE_MANAGER")));

this.employees.save(new Employee("Frodo", "Baggins", "ring bearer", greg));
this.employees.save(new Employee("Bilbo", "Baggins", "burglar", greg));
this.employees.save(new Employee("Gandalf", "the Grey", "wizard", greg));

SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("oliver", "doesn't matter",
AuthorityUtils.createAuthorityList("ROLE_MANAGER")));

this.employees.save(new Employee("Samwise", "Gamgee", "gardener", oliver));
this.employees.save(new Employee("Merry", "Brandybuck", "pony rider", oliver));
this.employees.save(new Employee("Peregrin", "Took", "pipe smoker", oliver));

SecurityContextHolder.clearContext();
}
}

Единственный недостаток заключается в том, что Spring Security активен с правилами доступа в полную силу при запуске этого загрузчика. Таким образом, чтобы сохранить данные о сотрудниках, вы должны использовать setAuthentication() API Spring Security  для аутентификации этого загрузчика с правильным именем и ролью. В конце контекст безопасности очищается.

Туризм по вашей защищенной службе REST

Имея все эти моды на месте, вы можете запустить приложение ( ./mvnw spring-boot:run) и проверить моды с помощью cURL.

$ curl -v -u greg:turnquist localhost:8080/api/employees/1
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'greg'
> GET /api/employees/1 HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Z3JlZzp0dXJucXVpc3Q=
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Set-Cookie: JSESSIONID=E27F929C1836CC5BABBEAB78A548DF8C; Path=/; HttpOnly
< ETag: "0"
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 25 Aug 2015 15:57:34 GMT
<
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "description" : "ring bearer",
  "manager" : {
    "name" : "greg",
    "roles" : [ "ROLE_MANAGER" ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/employees/1"
    }
  }
}

Это показывает гораздо больше деталей, чем во время первой сессии. Прежде всего, Spring Security включает несколько протоколов HTTP для защиты от различных векторов атак (Pragma, Expires, X-Frame-Options и т. Д.). Вы также выдаете базовые учетные данные, с помощью  -u greg:turnquist которых отображается заголовок авторизации.

Среди всех заголовков вы можете увидеть   заголовок ETag из вашего версионного ресурса.

Наконец, внутри самих данных вы можете увидеть новый атрибут:  менеджер . Вы можете видеть, что он включает в себя имя и роли, но НЕ пароль. Это связано с использованием  @JsonIgnore в этом поле. Поскольку Spring Data REST не экспортировал этот репозиторий, его значения встроены в этот ресурс. Это будет полезно при обновлении пользовательского интерфейса в следующем разделе.

Отображение информации о менеджере в пользовательском интерфейсе

Со всеми этими модами в бэкэнде вы можете перейти к обновлению вещей в веб-интерфейсе. Прежде всего, покажите менеджера сотрудника внутри компонента `<Employee />` React:

var Employee = React.createClass({
    handleDelete: function () {
        this.props.onDelete(this.props.employee);
    },
    render: function () {
        return (
            <tr>
                <td>{this.props.employee.entity.firstName}</td>
                <td>{this.props.employee.entity.lastName}</td>
                <td>{this.props.employee.entity.description}</td>
                <td>{this.props.employee.entity.manager.name}</td>
                <td>
                    <UpdateDialog employee={this.props.employee}
                                  attributes={this.props.attributes}
                                  onUpdate={this.props.onUpdate}/>
                </td>
                <td>
                    <button onClick={this.handleDelete}>Delete</button>
                </td>
            </tr>
        )
    }
});

Это просто добавляет столбец для  this.props.employee.entity.manager.name.

Фильтрация метаданных схемы JSON

Если в выводе данных отображается поле, можно предположить, что оно имеет запись в метаданных схемы JSON. Вы можете увидеть это в следующем отрывке:

{
...
    "manager" : {
      "readOnly" : false,
      "$ref" : "#/descriptors/manager"
    },
    ...
  },
  ...
  "$schema" : "http://json-schema.org/draft-04/schema#"
}

Поле менеджера — это не то, что вы хотите, чтобы люди редактировали напрямую. Поскольку он встроенный, его следует рассматривать как атрибут только для чтения. Чтобы отфильтровать встроенные записи из  CreateDialog и UpdatDialog, просто удалите такие записи после извлечения метаданных схемы JSON.

/**
 * Filter unneeded JSON Schema properties, like uri references and
 * subtypes ($ref).
 */
Object.keys(schema.entity.properties).forEach(function (property) {
    if (schema.entity.properties[property].hasOwnProperty('format') &&
        schema.entity.properties[property].format === 'uri') {
        delete schema.entity.properties[property];
    }
    if (schema.entity.properties[property].hasOwnProperty('$ref')) {
        delete schema.entity.properties[property];
    }
});

this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;

Этот код обрезает как отношения URI, так и записи $ ref.

Ловушка для несанкционированного доступа

С проверками безопасности, настроенными на бэкэнде, добавьте обработчик на случай, если кто-то попытается обновить запись без авторизации:

onUpdate: function (employee, updatedEmployee) {
    client({
        method: 'PUT',
        path: employee.entity._links.self.href,
        entity: updatedEmployee,
        headers: {
            'Content-Type': 'application/json',
            'If-Match': employee.headers.Etag
        }
    }).done(response => {
        /* Let the websocket handler update the state */
    }, response => {
        if (response.status.code === 403) {
            alert('ACCESS DENIED: You are not authorized to update ' +
                employee.entity._links.self.href);
        }
        if (response.status.code === 412) {
            alert('DENIED: Unable to update ' + employee.entity._links.self.href +
                '. Your copy is stale.');
        }
    });
},

У вас был код для отлова ошибки HTTP 412. Это перехватывает код состояния HTTP 403 и обеспечивает соответствующее предупреждение.

Сделайте то же самое для операций удаления:

onDelete: function (employee) {
    client({method: 'DELETE', path: employee.entity._links.self.href}
    ).done(response => {/* let the websocket handle updating the UI */},
    response => {
        if (response.status.code === 403) {
            alert('ACCESS DENIED: You are not authorized to delete ' +
                employee.entity._links.self.href);
        }
    });
},

Это закодировано аналогично специальным сообщениям об ошибках.

Добавьте некоторые детали безопасности в пользовательский интерфейс

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

<div>
    Hello, <span th:text="${#authentication.name}">user</span>.
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Log Out"/>
    </form>
</div>

Собираем все вместе

С этими изменениями во внешнем интерфейсе перезапустите приложение и перейдите по адресу http: // localhost: 8080 .

Вы сразу же будете перенаправлены на форму входа. Эта форма предоставляется Spring Security, хотя вы можете  создать свою собственную,  если хотите. Войдите как greg / turnquist.

безопасность 1

Вы можете увидеть недавно добавленный столбец менеджера. Пройдите пару страниц, пока не найдете сотрудников, принадлежащих  Оливеру .

безопасность 2

Нажмите «  Обновить» , внесите некоторые изменения и нажмите «  Обновить» . Должно произойти сбой со следующим всплывающим окном:

безопасность 3

Если вы попробуете  удалить , это должно произойти сбой с аналогичным сообщением Создайте нового сотрудника, и он должен быть назначен вам.

Рассмотрение

В этой сессии:

  • Вы определили модель менеджера и связали ее с сотрудником через отношение 1-ко-многим.
  • Вы создали хранилище для менеджеров и сказали Spring Data REST не экспортировать.
  • Вы написали набор правил доступа для репозитория empoyee, а также написали политику безопасности.
  • Вы написали другой обработчик событий Spring Data REST, чтобы перехватить события создания до того, как они произойдут, чтобы их текущий пользователь мог быть назначен менеджером сотрудника.
  • Вы обновили пользовательский интерфейс, чтобы показать менеджера сотрудника, а также отобразить всплывающие сообщения об ошибках при выполнении несанкционированных действий.

Вопросы?

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

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