Как показано в листинге 3.5, параметр RowMapper для findAll() и findById() задается как ссылка на метод mapRowToIngredient(). Ссылки на методы Java 8 и лямбда-выражения удобны при работе с JdbcTemplate в качестве альтернативы явной реализации RowMapper. Но если по какой-то причине вы хотите или нуждаетесь в явном RowMapper, то следующая реализация findAll() показывает, как это сделать:

@Override


public Ingredient findOne(String id) {


return jdbc.queryForObject(


"select id, name, type from Ingredient where id=?",


new RowMapper() {


public Ingredient mapRow(ResultSet rs, int rowNum) throws SQLException {


return new Ingredient(


rs.getString("id"),


rs.getString("name"),


Ingredient.Type.valueOf(rs.getString("type")));


};


}, id);


}

Чтение данных из базы данных-это только часть истории. В какой-то момент данные должны быть записаны в базу данных, чтобы их можно было прочитать. Итак, давайте рассмотрим реализацию метода save().

ВСТАВКА СТРОКИ

Метод update() JdbcTemplate может использоваться для любого запроса, который записывает или обновляет данные в базе данных. И, как показано в следующем листинге, его можно использовать для вставки данные в базу данных.

Листинг 3.6 вставка данных с использованием JdbcTemplate

@Override


public Ingredient save(Ingredient ingredient) {


jdbc.update(


"insert into Ingredient (id, name, type) values (?, ?, ?)",


ingredient.getId(),


ingredient.getName(),


ingredient.getType().toString());


return ingredient;


}

Поскольку нет необходимости сопоставлять данные ResultSet с объектом, метод update() намного проще, чем query() или queryForObject (). Для него требуется только String, содержащий SQL, а также значения, для параметров запроса. В данном случае запрос имеет три параметра, которые соответствуют последним трем параметрам запроса метода save() - ID ингредиента, name и type.

С JdbcIngredientRepository закончили, теперь вы можете внедрить его в DesignTacoController и использовать его для предоставления списка объектов Ingredient вместо использования жестко закодированных значений (как вы сделали в главе 2). Изменения в DesignTacoController показаны далее.

Листинг 3.7 Внедрение и использование репозитория в контроллере

@Controller


@RequestMapping("/design")


@SessionAttributes("order")


public class DesignTacoController {


private final IngredientRepository ingredientRepo;



@Autowired


public DesignTacoController(IngredientRepository ingredientRepo) {


this.ingredientRepo = ingredientRepo;


}



@GetMapping


public String showDesignForm(Model model) {


List ingredients = new ArrayList<>();


ingredientRepo.findAll().forEach(i -> ingredients.add(i));


Type[] types = Ingredient.Type.values();


for (Type type : types) {


model.addAttribute(type.toString().toLowerCase(),


filterByType(ingredients, type));


}

return "design";

}


...


}

Обратите внимание, что вторая строка метода showDesignForm() теперь вызывает метод findAll() внедренного IngredientRepository. Метод findAll() извлекает все ингредиенты из базы данных перед их фильтрацией в различные типы в модели.

Вы почти готовы запустить приложение и попробовать эти изменения. Но прежде чем начать чтение данных из таблицы Ingredient, которая используется в запросе, вероятно, следует создать эту таблицу и заполнить ее хоть какими-то данными ингредиентов.

3.1.3 Создание схемы и предварительная загрузка данных

Помимо таблицы Ingredient, вам также понадобятся некоторые таблицы, содержащие информацию о заказах и составе тако. На рисунке 3.1 показаны необходимые таблицы, а также связи между ними.


Рисунок 3.1 таблицы схемы Taco Cloud

Таблицы на рис. 3.1 служат следующим целям:

Ingredient - содержит информацию об ингредиенте

Taco - содержит важную информацию о составе taco

Taco_Ingredients - содержит одну или несколько строк для каждой строки в Taco, сопоставляя taco с ингредиентами для этого taco

Taco_Order-содержит важные данные order

Taco_Order_Tacos - содержит одну или несколько строк для каждой строки в Taco_Order, сопоставляя order с taco-ми в заказе

Следующий листинг показывает SQL, который создает таблицы.

Листинг 3.8 Схема Taco Cloud

create table if not exists Ingredient (


id varchar(4) not null,


name varchar(25) not null,


type varchar(10) not null


);



create table if not exists Taco (


id identity,


name varchar(50) not null,


createdAt timestamp not null


);



create table if not exists Taco_Ingredients (


taco bigint not null,


ingredient varchar(4) not null


);


alter table Taco_Ingredients add foreign key (taco) references Taco(id);


alter table Taco_Ingredients add foreign key (ingredient) references Ingredient(id);


create table if not exists Taco_Order (


id identity,


deliveryName varchar(50) not null,


deliveryStreet varchar(50) not null,


deliveryCity varchar(50) not null,


deliveryState varchar(2) not null,


deliveryZip varchar(10) not null,


ccNumber varchar(16) not null,


ccExpiration varchar(5) not null,


ccCVV varchar(3) not null,


placedAt timestamp not null


);



create table if not exists Taco_Order_Tacos (


tacoOrder bigint not null,


taco bigint not null


);



alter table Taco_Order_Tacos add foreign key (tacoOrder) references Taco_Order(id);


alter table Taco_Order_Tacos add foreign key (taco) references Taco(id);

Большой вопрос заключается в том, куда поместить это определение схемы. Как оказалось, Spring Boot содержит ответ на этот вопрос.

Если есть файл с именем schema.sql в корне classpath приложения, а затем SQL в этом файле будет выполняться в базе данных при запуске приложения. Поэтому содержимое листинга 3.8 следует поместить в проект в виде файла с именем schema.sql в папке src/main/resources.

Вам также необходимо предварительно загрузить базу данных с данными по ингредиентам. К счастью, Spring Boot также выполнит файл с именем data.sql из корня classpath при запуске приложения. Таким образом, вы можете заполнить базу данных данными ингредиентов, используя инструкции insert в следующем листинге, размещенном в src/main/resources/data.sql.

Листинг 3.9 Предзаполнение БД

delete from Taco_Order_Tacos;


delete from Taco_Ingredients;


delete from Taco;


delete from Taco_Order;


delete from Ingredient;



insert into Ingredient (id, name, type) values ('FLTO', 'Flour Tortilla', 'WRAP');


insert into Ingredient (id, name, type) values ('COTO', 'Corn Tortilla', 'WRAP');


insert into Ingredient (id, name, type) values ('GRBF', 'Ground Beef', 'PROTEIN');


insert into Ingredient (id, name, type) values ('CARN', 'Carnitas', 'PROTEIN');


insert into Ingredient (id, name, type) values ('TMTO', 'Diced Tomatoes', 'VEGGIES');


insert into Ingredient (id, name, type) values ('LETC', 'Lettuce', 'VEGGIES');


insert into Ingredient (id, name, type) values ('CHED', 'Cheddar', 'CHEESE');


insert into Ingredient (id, name, type) values ('JACK', 'Monterrey Jack', 'CHEESE');


insert into Ingredient (id, name, type) values ('SLSA', 'Salsa', 'SAUCE');


insert into Ingredient (id, name, type) values ('SRCR', 'Sour Cream', 'SAUCE');

Несмотря на то, что вы только разработали хранилище для данных ингредиентов, вы можете запустить приложение Taco Cloud на этом этапе и посетить страницу проектирования, чтобы увидеть JdbcIngredientRepository в действии. Ну же… попробуйте. Когда вы вернетесь к чтению, вы напишете репозитории для сохранения Taco, Order и data.

3.1.4 Вставка данных

Вы уже имели представление о том, как использовать JdbcTemplate для записи данных в базу данных. Метод save() в JdbcIngredientRepository использовал метод update() JdbcTemplate для сохранения объектов ингредиентов в базе данных.

Хотя это был хороший первый пример, возможно, это было слишком просто. Как вы скоро увидите, сохранение данных может быть более сложным, чем то, что необходимо JdbcIngredientRepository. Два способа сохранения данных с помощью JdbcTemplate включают следующее:

Напрямую, используя метод update()

С помощью класса-оболочки SimpleJdbcInsert

Давайте сначала посмотрим, как использовать метод update(), когда задача сложнее, чем та, что была при сохранение Ingredient.

СОХРАНЕНИЕ ДАННЫХ С ПОМОЩЬЮ JDBCTEMPLATE

На данный момент единственное, что нужно сделать в репозиториях taco и order, это сохранение их соответствующие объекты. Чтобы сохранить объекты Taco, TacoRepository объявляет метод save():

package tacos.data;



import tacos.Taco;


public interface TacoRepository {


Taco save(Taco design);


}

Аналогичным образом OrderRepository также объявляет метод save:

package tacos.data;



import tacos.Order;



public interface OrderRepository {


Order save(Order order);


}

Кажется достаточно простым, верно? Не так быстро. Сохранение состава taco требует, чтобы вы также сохранили ингредиенты, связанные с этим taco в таблицу Taco_Ingredients. Точно так же сохранение заказа требует, чтобы вы также сохранили tacos, привязанные к заказу в таблице Taco_Order_Tacos. Это делает сохранение тако и заказов немного более сложным, чем то, что требовалось для сохранения ингредиента.

Для реализации TacoRepository необходим метод save(), который начинается с сохранения основных деталей состава taco (например, имени и времени создания), а затем вставляет одну строку в Taco_Ingredients для каждого ингредиента в объекте Taco. Ниже приведен полный класс JdbcTacoRepository.

Листинг 3.10 реализация TacoRepository с JdbcTemplate

package tacos.data;



import java.sql.Timestamp;


import java.sql.Types;


import java.util.Arrays;


import java.util.Date;


import org.springframework.jdbc.core.JdbcTemplate;


import org.springframework.jdbc.core.PreparedStatementCreator;


import org.springframework.jdbc.core.PreparedStatementCreatorFactory;


import org.springframework.jdbc.support.GeneratedKeyHolder;


import org.springframework.jdbc.support.KeyHolder;


import org.springframework.stereotype.Repository;



import tacos.Ingredient;


import tacos.Taco;



@Repository


public class JdbcTacoRepository implements TacoRepository {



private JdbcTemplate jdbc;



public JdbcTacoRepository(JdbcTemplate jdbc) {


this.jdbc = jdbc;


}



@Override


public Taco save(Taco taco) {


long tacoId = saveTacoInfo(taco);


taco.setId(tacoId);


for (Ingredient ingredient : taco.getIngredients()) {


saveIngredientToTaco(ingredient, tacoId);


}


return taco;


}



private long saveTacoInfo(Taco taco) {


taco.setCreatedAt(new Date());


PreparedStatementCreator psc = new PreparedStatementCreatorFactory(


"insert into Taco (name, createdAt) values (?, ?)",


Types.VARCHAR, Types.TIMESTAMP


).newPreparedStatementCreator(


Arrays.asList(


taco.getName(),


new Timestamp(taco.getCreatedAt().getTime()))


);


KeyHolder keyHolder = new GeneratedKeyHolder();


jdbc.update(psc, keyHolder);


return keyHolder.getKey().longValue();


}



private void saveIngredientToTaco(Ingredient ingredient, long tacoId) {


jdbc.update(


"insert into Taco_Ingredients (taco, ingredient) " +


"values (?, ?)",


tacoId, ingredient.getId()


);


}


}

Как вы можете видеть, метод save() начинается с вызова private метода saveTacoInfo(), а затем использует ID taco, возвращенный из этого метода, для вызова saveIngredientToTaco(), который сохраняет каждый ингредиент. Дьявол кроется в деталях saveTacoInfo().

Когда вы вставляете строку в Taco, вам нужно знать идентификатор, сгенерированный базой данных, чтобы вы могли ссылаться на него в каждом из ингредиентов. Метод update(), используемый при сохранении данных ингредиента, не помогает вам получить сгенерированный идентификатор, поэтому вам нужен другой метод update().

Метод update () принимает PreparedStatementCreator и KeyHolder. Это KeyHolder, который предоставит сгенерированный идентификатор taco. Но чтобы использовать его, необходимо также создать PreparedStatementCreator.

Как видно из листинга 3.10, создание PreparedStatementCreator нетривиально. Начните с создания PreparedStatementCreatorFactory, предоставив ему SQL, который вы хотите выполнить, а также типы каждого параметра запроса. Затем вызовите newPreparedStatementCreator() на этой фабрике, передав значения, необходимые в параметрах запроса для создания PreparedStatementCreator.

С PreparedStatementCreator на руках, вы можете вызвать update(), передав в PreparedStatementCreator и KeyHolder (в этом случае экземпляр GeneratedKeyHolder). Как только update() закончено, можно возвратить ID taco, возвращая keyHolder.getKey().longValue().

Возвращаясь к save(), в цикле для каждого каждый Ingredient в Taco, вызывается saveIngredientToTaco(). В saveIngredientToTaco() методе используется простая форма update() для сохранения списка ингредиентв таблицу Taco_Ingredients.

Все, что осталось сделать с TacoRepository, - это внедрить его в DesignTacoController и использовать его при сохранении Taco. В следующем листинге показаны изменения, необходимые для внедрения репозитория.

Листинг 3.11 Внедрение и использование TacoRepository

@Controller


@RequestMapping("/design")


@SessionAttributes("order")


public class DesignTacoController {


private final IngredientRepository ingredientRepo;


private TacoRepository designRepo;



@Autowired


public DesignTacoController( IngredientRepository ingredientRepo, TacoRepository designRepo) {


this.ingredientRepo = ingredientRepo;


this.designRepo = designRepo;


}



}

Как вы можете видеть, конструктор принимает как IngredientRepository, так и TacoRepository. Он назначает переменные экземпляра, чтобы их можно было использовать в методах showDesignForm() и processDesign().

Говоря о методе processDesign(), его изменения немного более обширны, чем изменения, которые вы сделали в showDesignForm(). В следующем списке показан новый метод processDesign().

Листинг 3.12 сохранение состава taco и их привязка к заказам

@Controller


@RequestMapping("/design")


@SessionAttributes("order")


public class DesignTacoController {



@ModelAttribute(name = "order")


public Order order() {


return new Order();


}



@ModelAttribute(name = "taco")


public Taco taco() {


return new Taco();


}



@PostMapping


public String processDesign( @Valid Taco design, Errors errors, @ModelAttribute Order order) {


if (errors.hasErrors()) {


return "design";


}


Taco saved = designRepo.save(design);


order.addDesign(saved);


return "redirect:/orders/current";


}


...


}

Первое, что можно заметить в коде в листинге 3.12, это то, что DesignTacoController теперь аннотируется SessionAttributes("order") и что у него есть новый аннотированный @ModelAttribute метод order(). Как и в случае с методом taco(), аннотация @ModelAttribute для order() гарантирует, что объект Order будет создан в модели. Но в отличие от объекта Taco в сеансе необходимо, чтобы order присутствовал в нескольких запросах, чтобы можно было создать несколько тако и добавить их в заказ. Аннотация @SessionAttributes на уровне класса задает любые объекты модели, такие как атрибут order, которые должны храниться в сеансе и доступны для нескольких запросов.

Реальная обработка состава taco происходит в методе processDesign(), который теперь принимает объект Order в качестве параметра, в дополнение к объектам Taco и Errors. Параметр Order аннотируется @ModelAttribute, чтобы указать, что его значение должно исходить из модели и что Spring MVC не должен пытаться привязать к нему параметры запроса.

После проверки на наличие ошибок, processDesign() использует внедренный TacoRepository для сохранения taco. Затем он добавляет объект Taco в Order, который сохраняется в сеансе.

Фактически объект Order остается в сеансе и не сохраняется в базе данных до тех пор, пока пользователь не завершит и не отправит форму заказа. В этот момент OrderController должен вызвать реализацию OrderRepository, чтобы сохранить заказ. Давайте напишем эту реализацию.

ВСТАВКА ДАННЫХ С SempleJdbcInsert

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

Когда дело доходит до сохранения заказов, существует аналогичное обстоятельство. Необходимо не только сохранить данные заказа в таблице Taco_Order, но также и ссылки на каждый taco в заказе в таблице Taco_Order_Tacos. Но вместо того, чтобы использовать громоздкий PreparedStatementCreator, позвольте мне представить вам SimpleJdbcInsert, объект, который обертывает JdbcTemplate, чтобы упростить вставку данных в таблицу.

Начнем с создания JDBCOrderRepository, implementation OrderRepository. Но прежде чем писать реализацию метода save(), давайте сосредоточимся на конструкторе, где вы создадите пару экземпляров SimpleJdbcInsert для вставки значений в таблицы Taco_Order и Taco_Order_Tacos. В следующем листинге показан JDBCOrderRepository (без метода save ()).

Листинг 3.13 создание SimpleJdbcInsert из JdbcTemplate

package tacos.data;



import java.util.Date;


import java.util.HashMap;


import java.util.List;


import java.util.Map;


import org.springframework.beans.factory.annotation.Autowired;


import org.springframework.jdbc.core.JdbcTemplate;


import org.springframework.jdbc.core.simple.SimpleJdbcInsert;


import org.springframework.stereotype.Repository;


import com.fasterxml.jackson.databind.ObjectMapper;


import tacos.Taco;


import tacos.Order;



@Repository


public class JdbcOrderRepository implements OrderRepository {


private SimpleJdbcInsert orderInserter;


private SimpleJdbcInsert orderTacoInserter;


private ObjectMapper objectMapper;



@Autowired


public JdbcOrderRepository(JdbcTemplate jdbc) {


this.orderInserter = new SimpleJdbcInsert(jdbc)


.withTableName("Taco_Order")


.usingGeneratedKeyColumns("id");


this.orderTacoInserter = new SimpleJdbcInsert(jdbc)


.withTableName("Taco_Order_Tacos");


this.objectMapper = new ObjectMapper();


}


...


}

Как и JdbcTacoRepository, JdbcOrderRepository inject-ид JdbcTemplate через его конструктор. Но вместо назначения JdbcTemplate непосредственно переменной экземпляра конструктор использует его для создания нескольких экземпляров SimpleJdbcInsert.

Первый экземпляр, который назначен переменной экземпляра orderInserter, настроен для работы с таблицей Taco_Order и предполагает, что свойство id будет предоставлено или сгенерировано базой данных. Второй экземпляр, назначенный orderTacoInserter, настроен для работы с таблицей Taco_Order_Tacos, но не содержит никаких инструкций о том, как ID будут генерироваться в этой таблице.

Конструктор также создает экземпляр Jackson ObjectMapper и присваивает его переменной экземпляра. Хотя Jackson предназначен для обработки JSON, вы увидите, как мы перепрофилируем его, чтобы помочь вам сохранять заказы и связанные с ними тако.

Теперь давайте посмотрим, как метод save() использует экземпляры SimpleJdbcInsert. В следующем списке показан метод save(), а также несколько частных методов save(), которые делегируются для реальной работы.

Листинг 3.14 использование SimpleJdbcInsert для вставки данных

@Override


public Order save(Order order) {


order.setPlacedAt(new Date());


long orderId = saveOrderDetails(order);


order.setId(orderId);


List tacos = order.getTacos();


for (Taco taco : tacos) {


saveTacoToOrder(taco, orderId);


}


return order;


}



private long saveOrderDetails(Order order) {


@SuppressWarnings("unchecked")


Map values = objectMapper.convertValue(order, Map.class);


values.put("placedAt", order.getPlacedAt());


long orderId =orderInserter


.executeAndReturnKey(values)


.longValue();


return orderId;


}



private void saveTacoToOrder(Taco taco, long orderId) {


Map values = new HashMap<>();


values.put("tacoOrder", orderId);


values.put("taco", taco.getId());


orderTacoInserter.execute(values);


}

Метод save() ничего не сохраняет. Он определяет поток для сохранения Order и связанных с ним объектов Taco и делегирует работу saveOrderDetails() и saveTacoToOrder().

SimpleJdbcInsert имеет несколько полезных методов для выполнения вставкиt: execute() и executeAndReturnKey(). Оба принимают Map, где map-ключи соответствуют именам столбцов в таблице, в которую вставляются данные. Map-значения вставляются в эти столбцы.

Такую Map легко создать, скопировав значения из Order в записи Map. Но у Order есть несколько свойств, и все они имеют одинаковое имя со столбцами, в которые они входят. Из-за этого в saveOrderDetails() я решил использовать Jackson ObjectMapper и его метод convertValue() для преобразования Order в Map(Я признаю, что это хакерское использование ObjectMapper, но у вас уже есть Jackson в classpath; Spring Boot’s web starter включает его. Кроме того, использование ObjectMapper для сопоставления объекта с Map намного проще, чем копирование каждого свойства из объекта в Map. Не стесняйтесь заменить использование ObjectMapper любым кодом, который вы предпочитаете, который строит Map, который вы передадите на вставку объектов.). После создания Map задайте для записи placedAt значение свойства placedAt объекта Order. Это необходимо, поскольку в противном случае ObjectMapper преобразует свойство Date в long, что несовместимо с полем placedAt в таблице Taco_Order.

С заполненным Map данными заказов, вы можете вызвать executeAndReturnKey() в orderInserter. Это сохраняет информацию о заказе в таблице Taco_Order и возвращает сгенерированный базой данных ID как объект типа Number, который преобразуется long с помощью вызова longValue(), и возвращается из метода.

Метод saveTacoToOrder() значительно проще. Вместо того чтобы использовать ObjectMapper для преобразования объекта в Map, создается Map и задаются соответствующие значения. Еще раз, ключи Map соответствуют именам столбцов в таблице. Вызов метода orderTacoInserter.execute() выполняет insert.

Теперь вы можете inject OrderRepository в OrderController и начать использовать его. Следующий листинг показывает OrderController, включая изменения для использования inject OrderRepository.

Листинг 3.15 использование OrderRepository в OrderController

package tacos.web;



import javax.validation.Valid;


import org.springframework.stereotype.Controller;


import org.springframework.validation.Errors;


import org.springframework.web.bind.annotation.GetMapping;


import org.springframework.web.bind.annotation.PostMapping;


import org.springframework.web.bind.annotation.RequestMapping;


import org.springframework.web.bind.annotation.SessionAttributes;


import org.springframework.web.bind.support.SessionStatus;


import tacos.Order;


import tacos.data.OrderRepository;



@Controller


@RequestMapping("/orders")


@SessionAttributes("order")


public class OrderController {


private OrderRepository orderRepo;



public OrderController(OrderRepository orderRepo) {


this.orderRepo = orderRepo;


}



@GetMapping("/current")


public String orderForm() {


return "orderForm";


}



@PostMapping


public String processOrder(@Valid Order order, Errors errors,SessionStatus sessionStatus) {


if (errors.hasErrors()) {


return "orderForm";


}


orderRepo.save(order);


sessionStatus.setComplete();


return "redirect:/";


}


}

Помимо внедрения OrderRepository в контроллер, единственные существенные изменения в OrderController происходят в методе processOrder(). Здесь объект Order, представленный в форме (который также является тем же самым объектом Order, поддерживаемым в сеансе), сохраняется с помощью метода save() внедренного OrderRepository.

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

Весь код сохранения JDBC закончен. Теперь вы можете запустить приложение Taco Cloud и проверить его работоспособность. Не стесняйтесь создавать столько тако и столько заказов, сколько вы хотите.

Вы также можете найти полезным покопаться в базе данных. Поскольку вы используете H2 в качестве встроенной базы данных, и поскольку у вас есть Spring Boot DevTools, вы можете обратиться в вашем браузере по адресу http://localhost:8080/h2-console чтобы увидеть консоль H2. По умолчанию поле JDBC URL должно быть установлено как jdbc:h2:mem:testdb. После входа в систему, вы должны быть в состоянии выполнить любой запрос к таблицам схемы Taco Cloud.

Spring JdbcTemplate, наряду с SimpleJdbcInsert, делает работу с реляционными базами данных значительно проще, чем простой vanilla JDBC. Но вы можете обнаружить, что JPA делает это еще проще. Давайте пересмотрим вашу работу и посмотрим, как использовать Spring Data, чтобы сделать сохранение данных еще проще.

3.2 Сохранение данных с помощью Spring Data JPA

Проект Spring Data представляет собой довольно крупный зонтичный (umbrella) проект, состоящий из нескольких подпроектов, большинство из которых сосредоточены на сохранении данных с различными типами баз данных. Некоторые из самых популярных Spring Data проектов включают в себя следующие:

Spring Data JPA — Сохранение JPA в реляционной базе данных

Spring Data MongoDB — Сохранение в базе данных документов Mongo

Spring Data Neo4j — Сохранение в базе данных графов Neo4j

Spring Data Redis — Сохранение в хранилище ключей и значений Redis

Spring Data Cassandra — Сохранение в базе данных Cassandra

Одной из наиболее интересных и полезных функций Spring Data для всех этих проектов является возможность автоматического создания репозиториев на основе интерфейса спецификации репозитория.

Чтобы увидеть, как работают Spring Data, потребуется начать все сначала, заменив репозитории на основе JDBC из ранее в этой главе репозиториями, созданными Spring Data JPA. Но сначала нужно добавить Spring Data JPA в сборку проекта.

3.2.1 Добавление Spring Data JPA в проект

Spring Data JPA доступны для Spring Boot приложений с JPA starter-ом. Эта зависимость стартера не только приносит в Spring Data JPA, но и транзитивно включает Hibernate как реализацию JPA:


org.springframework.boot


spring-boot-starter-data-jpa


Если вы хотите использовать другую реализацию JPA, то вам нужно, по крайней мере, исключить зависимость Hibernate и включить библиотеку JPA по вашему выбору. Например, чтобы использовать EclipseLink вместо Hibernate, необходимо изменить сборку следующим образом:


org.springframework.boot


spring-boot-starter-data-jpa




hibernate-entitymanager


org.hibernate






org.eclipse.persistence


eclipselink


2.5.2


Обратите внимание, что могут потребоваться другие изменения в зависимости от выбранного варианта реализации JPA. Обратитесь к документации к выбранной реализации JPA для деталей. Теперь давайте вернемся к вашим доменным объектам и аннотируем их для сохранения JPA.

3.2.2 Аннотирование домена как сущностей

Как вы скоро увидите, Spring Data делает удивительные вещи, когда дело доходит до создания репозиториев. Но, к сожалению, это не очень помогает, когда дело доходит до аннотирования объектов домена аннотациями сопоставления JPA. Вам нужно будет открыть Ingredient, Taco и Order классы и добавить в них несколько аннотаций. Первым изменим класс Ingredient.

Листинг 3.16 аннотирование Ingredient для сохранения JPA

package tacos;



import javax.persistence.Entity;


import javax.persistence.Id;


import lombok.AccessLevel;


import lombok.Data;


import lombok.NoArgsConstructor;


import lombok.RequiredArgsConstructor;



@Data


@RequiredArgsConstructor


@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)


@Entity


public class Ingredient {


@Id


private final String id;


private final String name;


private final Type type;



public static enum Type {


WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE


}


}

Чтобы объявить это как сущность JPA, Ingredient должен быть аннотирован @Entity. И его свойство id должно быть аннотировано @Id, чтобы обозначить его как свойство, которое будет однозначно идентифицировать сущность в базе данных.

В дополнение к аннотациям, специфичным для JPA, вы также заметили, что вы добавили аннотацию @NoArgsConstructor на уровне класса. JPA требует, чтобы сущности имели конструктор без аргументов, поэтому @NoArgsConstructor Lombok-а делает это за вас. Однако, вы не хотите, чтобы имелась возможность использовать его, поэтому сделайте его private, установив атрибут доступа AccessLevel.PRIVATE. И поскольку есть final свойства, которые должны быть установлены, вы также устанавливаете атрибут force в true, что приводит к тому, что конструктор, сгенерированный Lombok-ом, устанавливает их в null.

Также добавляется @RequiredArgsConstructor. @Data неявно добавляет конструктор обязательных аргументов, но при использовании @NoArgsConstructor этот конструктор удаляется. Явный @RequiredArgsConstructor гарантирует, что у вас все еще будет конструктор обязательных аргументов в дополнение к закрытому конструктору без аргументов.

Теперь давайте перейдем к классу Taco и посмотрим, как аннотировать его как сущность JPA.

Листинг 3.17 аннотирование Taco как сущности

package tacos;



import java.util.Date;


import java.util.List;


import javax.persistence.Entity;


import javax.persistence.GeneratedValue;


import javax.persistence.GenerationType;


import javax.persistence.Id;


import javax.persistence.ManyToMany;


import javax.persistence.OneToMany;


import javax.persistence.PrePersist;


import javax.validation.constraints.NotNull;


import javax.validation.constraints.Size;


import lombok.Data;



@Data


@Entity


public class Taco {


@Id


@GeneratedValue(strategy=GenerationType.AUTO)


private Long id;



@NotNull


@Size(min=5, message="Name must be at least 5 characters long")


private String name;


private Date createdAt;



@ManyToMany(targetEntity=Ingredient.class)


@Size(min=1, message="You must choose at least 1 ingredient")


private List ingredients;


@PrePersist


void createdAt() {


this.createdAt = new Date();


}


}

Как и в случае с Ingredient, класс Taco теперь аннотируется @Entity и имеет свойство id, аннотированное @Id. Поскольку вы полагаетесь на базу данных для автоматического создания значения идентификатора, вы также аннотируете свойство id с помощью @GeneratedValue, указывая стратегию AUTO.

Чтобы объявить связь между Taco и связанным с ним списком Ingredient-ов, аннотируйте ingredients с помощью @ManyToMany. Taco может иметь много объектов -Ingredient, и Ingredient может быть частью многих Taco.

Вы также заметили, что есть новый метод createdAt(), который аннотируется @PrePersist. Вы будете использовать его, чтобы установить свойство createdAt в текущую дату и время, прежде чем сохранить Taco. Наконец, давайте аннотируем объект Order как сущность. В следующем листинге показан новый класс Order.

Листинг 3.18 Аннотирование Order как сущность JPA

package tacos;



import java.io.Serializable;


import java.util.ArrayList;


import java.util.Date;


import java.util.List;


import javax.persistence.Entity;


import javax.persistence.GeneratedValue;


import javax.persistence.GenerationType;


import javax.persistence.Id;


import javax.persistence.ManyToMany;


import javax.persistence.OneToMany;


import javax.persistence.PrePersist;


import javax.persistence.Table;


import javax.validation.constraints.Digits;


import javax.validation.constraints.Pattern;


import org.hibernate.validator.constraints.CreditCardNumber;


import org.hibernate.validator.constraints.NotBlank;


import lombok.Data;



@Data


@Entity


@Table(name="Taco_Order")


public class Order implements Serializable {


private static final long serialVersionUID = 1L;



@Id


@GeneratedValue(strategy=GenerationType.AUTO)


private Long id;


private Date placedAt;


...



@ManyToMany(targetEntity=Taco.class)


private List tacos = new ArrayList<>();



public void addDesign(Taco design) {


this.tacos.add(design);


}



@PrePersist


void placedAt() {


this.placedAt = new Date();


}


}

Как вы можете видеть, изменения Order похожи на изменения в Taco. Но есть одна новая аннотация на уровне класса: @Table. Это указывает, что сущности Order должны сохраняться в таблице с именем Taco_Order в базе данных.

Хотя вы могли бы использовать эту аннотацию на любом из объектов, это необходимо с Order. Без него JPA по умолчанию сохранит сущности в таблице с именем Order, но order является зарезервированным словом в SQL и вызовет проблемы. Теперь, когда сущности должным образом аннотированы, пришло время написать ваши репозитории.

3.2.3 Объявление репозиториев JPA

В версиях репозиториев JDBC вы явным образом объявили методы, которые должен предоставить репозиторий. Но с Spring Data вместо этого можно расширить интерфейс CrudRepository. Например, вот новый интерфейс IngredientRepository:

package tacos.data;



import org.springframework.data.repository.CrudRepository;


import tacos.Ingredient;



public interface IngredientRepository


extends CrudRepository {


}

CrudRepository объявляет около десятка методов для операций CRUD (create, read, update, delete). Обратите внимание, что он параметризован, при этом первым параметром является тип сущности, который должен сохраняться в репозитории, а вторым параметром тип свойства entity ID. Для IngredientRepository параметрами должны быть Ingredient и String.

Аналогичным образом можно определить TacoRepository следующим образом:

package tacos.data;



import org.springframework.data.repository.CrudRepository;


import tacos.Taco;



public interface TacoRepository


extends CrudRepository {


}

Единственные существенные различия между IngredientRepositoryв и TacoRepository - это параметры CrudRepository. Здесь они установлены как Taco и Long, чтобы указать сущность Taco (и ее тип идентификатора) в качестве единицы сохранения для этого интерфейса репозитория. Наконец, те же изменения могут быть применены к OrderRepository:

package tacos.data;



import org.springframework.data.repository.CrudRepository;


import tacos.Order;



public interface OrderRepository


extends CrudRepository {


}

И теперь у вас есть три репозитория. Вы можете подумать, что вам нужно написать реализации для всех трех, включая дюжину методов для каждой реализации. Но в Spring Data JPA - нет необходимости писать реализацию! При запуске приложения Spring Data JPA автоматически создает реализацию на лету. Это означает, что репозитории готовы к использованию с самого начала. Просто вставьте их в контроллеры, как вы сделали для реализаций на основе JDBC, и все готово.

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

3.2.4 Настройка репозиториев JPA

Представьте, что в дополнение к основным операциям CRUD, предоставляемым CrudRepository, вам также необходимо получить все заказы, доставленные по заданному почтовому индексу. Как оказалось, это можно легко решить, добавив следующее объявление метода в OrderRepository:

List findByDeliveryZip(String deliveryZip);

При создании реализации репозитория Spring Data проверяет все методы в интерфейсе репозитория, анализирует имя метода и пытается понять назначение метода в контексте сохраняемого объекта (в данном случае-порядок). По сути, Spring Data определяет своего рода миниатюрный доменный язык (DSL), в котором сведения о сохраняемости выражаются в сигнатурах методов репозитория.

Spring Data знает, что этот метод предназначен для поиска заказов, потому что вы параметризовали CrudRepository с Order. Имя метода, findByDeliveryZip(), дает понять, что этот метод должен найти все сущности Order, сопоставив их свойство deliveryZip со значением, переданным в качестве параметра в метод.

Метод findByDeliveryZip() достаточно прост, но Spring Data может обрабатывать еще более интересные имена методов. Методы репозитория состоят из глагола, необязательного субъекта, слова By и предиката. В случае findByDeliveryZip() глагол find и предикат DeliveryZip; субъект не указан и подразумевается как Order.

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

List readOrdersByDeliveryZipAndPlacedAtBetween(String deliveryZip, Date startDate, Date endDate);

На рис. 3.2 показано, как Spring Data анализирует и понимает readOrdersByDeliveryZipAndPlacedAtBetween() при генерации реализации репозитория. Как вы можете видеть, глагол в readOrdersByDeliveryZipAndPlacedAtBetween() read. Spring Data также понимает find, read и get как синонимы для извлечения одной или нескольких сущностей. Кроме того, можно также использовать count в качестве глагола, если требуется, чтобы метод возвращал значение int с числом совпадающих сущностей.

-Этот метод будет считывать (read) данные (”get “и” find " также разрешены здесь).

-Обозначает начало свойств для соответствия

-...and…

-Значение должно находиться между заданными значениями.

-Соотвествие .deliveryZip или -.delivery.zip свойство

-Соотвествие .placedAt или .placed.at свойство


Рис. 3.2. Spring Data анализирует сигнатуры методов репозитория для определения запроса, который необходимо выполнить.

Хотя объект метода является необязательным, здесь он Orders. Spring Data игнорирует большинство слов в объекте, поэтому вы можете назвать метод readPuppiesBy... и он все равно найдет сущности Order, так как это тип, с которым параметризуется CrudRepository.

Сказуемое следует за словом в имени метода и является наиболее интересной частью сигнатуры метода. В этом случае предикат ссылается на два свойства заказа: deliveryZip и placedAt. Свойство deliveryZip должно быть равно значению, переданному в первый параметр метода. Ключевое слово Between указывает, что значение deliveryZip должно находиться между значениями, переданными в последние два параметра метода.

В дополнение к неявной операции Equals и Between, подписи метода Spring Data могут также включать любой из этих операторов:

-IsAfter,After,IsGreaterThan,GreaterThan

-IsGreaterThanEqual,GreaterThanEqual

-IsBefore,Before,IsLessThan,LessThan

-IsLessThanEqual,LessThanEqual

-IsBetween,Between

-IsNull,Null

-IsNotNull,NotNull

-IsIn,In

-IsNotIn,NotIn

-IsStartingWith,StartingWith,StartsWith

-IsEndingWith,EndingWith,EndsWith

-IsContaining,Containing,Contains

-IsLike,Like

-IsNotLike,NotLike

-IsTrue,True

-IsFalse,False

-Is,Equals

-IsNot,Not

-IgnoringCase,IgnoresCase

В качестве альтернативы IgnoringCase и IgnoresCase можно использовать AllIgnoringCase либо AllIgnoresCase в методе, чтобы игнорировать регистр для всех сравнений строк. Например, рассмотрим следующий метод:

List findByDeliveryToAndDeliveryCityAllIgnoresCase(String deliveryTo, String deliveryCity);

Наконец, можно также разместить OrderBy в конце имени метода для сортировки результатов по указанному столбцу. Например, deliveryTo в order:

List findByDeliveryCityOrderByDeliveryTo(String city);

Хотя правила именования могут быть полезны для относительно простых запросов, это не займет много воображения, чтобы увидеть, что имена методов могут выйти из-под контроля для более сложных запросы. В этом случае не стесняйтесь называть метод как угодно и аннотировать его @Query, чтобы явно указать запрос, который будет выполняться при вызове метода, как показано в этом примере:

@Query("Order o where o.deliveryCity='Seattle'")

List readOrdersDeliveredInSeattle();

В этом простом примере @Query вы запрашиваете все заказы, доставленные в Seattle. Но вы можете использовать @Query для выполнения практически любого запроса, который вы можете придумать, даже если трудно или невозможно выполнить запрос, следуя соглашению об именовании.

ИТОГ

-Spring JdbcTemplate значительно упрощает работу с JDBC.

-PreparedStatementCreator и KeyHolder можно использовать вместе, если необходимо узнать значение идентификатора, созданного базой данных.

-Для простого выполнения вставок данных, используйте SimpleJdbcInsert.

-Spring Data JPA делает сохранение JPA таким же простым, как написание интерфейса репозитория.

Spring in Action Covers Spring 5.0 перевод на русский. Глава 4

4. Securing Spring

Эта глава охватывает:

Автоконфигурирование Spring Security

Определение пользовательского хранилища пользователя

Настройка страницы входа

Защита от CSRF-атак

Определение ваших пользователей

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

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

4.1 Включение Spring Security

Самым первым шагом в обеспечении безопасности приложения Spring является добавление зависимости Spring Boot security starter в сборку. В pom.xml файле, добавьте следующую запись :


org.springframework.boot


spring-boot-starter-security


Если вы используете Spring Tool Suite, это еще проще. Щелкните правой кнопкой мыши на pom.xml файле и выберите Edit Starters из контекстного меню Spring. Откроется диалоговое окно Starter Dependencies. Выберите запись Security под категорией Core, как показано на рисунке 4.1.




Рис. 4.1 добавление стартер безопасности с Spring Tool Suite

Хотите верьте, хотите нет, но зависимость-это единственное, что требуется для защиты приложения. При запуске приложения автоконфигурация обнаружит, что Spring Security находится в пути к классам, и настроит базовую конфигурацию безопасности.

Если вы хотите попробовать, запустите приложение и попробуйте посетить домашнюю страницу (или любую другую страницу). Вам будет предложено выполнить проверку подлинности с помощью обычного HTTP диалогового окна. Чтобы пройти проверку, вам нужно будет предоставить имя пользователя и пароль. Имя пользователя user. Что касается пароля, то он генерируется случайным образом и записывается в файл журнала приложения. Запись журнала будет выглядеть примерно так:

Using default security password: 087cfc6a-027d-44bc-95d7-cbb3a798a1ea

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

Кажется, что обеспечение безопасности приложений Spring довольно простая работа. С защитой приложения Taco Cloud я полагаю, что закончено и я могу закончить эту главу сейчас и перейти к следующей теме. Но прежде чем мы забегаем вперед, давайте рассмотрим, какую автоконфигурацию безопасности мы обеспечили.

Не делая ничего, кроме добавления стартера безопасности в сборку проекта, вы получаете следующие функции безопасности:

-Все пути HTTP-запросов требуют аутентификации.

-Никаких конкретных ролей или полномочий не требуется.

-Нет страницы входа

-Проверка подлинности предлагается с обычной проверкой подлинности http.

-Есть только один пользователь; имя пользователя - user.

Это хорошее начало, но я думаю, что потребности в безопасности большинства приложений (включая Taco Cloud) будут сильно отличаться от этих элементарных функций безопасности.

От вас потребуется больше работы, если вы собираетесь правильно защитить приложение Taco Cloud. По крайней мере, необходимо настроить Spring Security для выполнения следующих действий:

-Запрашивать аутентификацию на странице входа, а не в диалоговом окне HTTP basic.

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

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

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

4.2 Конфигурирование Spring Security

На протяжении многих лет существует несколько способов настройки Spring Security, включая долгую настройку на основе XML. К счастью, несколько последних версий Spring Security поддерживают конфигурацию на основе Java, которая намного проще для чтения и записи.

Прежде чем эта глава будет закончена, вы настроите все все параметры безопасности Taco Cloud на основаннии Java-based Spring Security конфигурации. Но чтобы начать работу, вы упростите ее, написав класс заготовки (-barebones) конфигурации, показанный в следующем листинге.

Листинг 4.1 Класс barebones конфигурации для Spring Security

package tacos.security;



import org.springframework.context.annotation.Bean;


import org.springframework.context.annotation.Configuration;


import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;


import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;



@Configuration


@EnableWebSecurity


public class SecurityConfig extends WebSecurityConfigurerAdapter {


}

Что делает эта barebones конфигурация для вас? Ну, не так много, но это делает вас на шаг ближе к функционалу безопасности который вам нужен. Если вы попытаетесь снова попасть на домашнюю страницу Taco Cloud, вам все равно будет предложено войти. Но вместо запроса диалогового окна обычной проверки подлинности HTTP вам будет показана форма входа в систему, как показано на рис. 4.2.


Рисунок 4.2 Spring Security дает вам простую страницу входа бесплатно.

TIP Переход в инкогнито: при ручном тестировании безопасности может оказаться полезным перевести браузер в частный режим или режим инкогнито. Это гарантирует, что у вас будет новая сессия каждый раз, когда вы открываете частное окно/инкогнито. Вам придется каждый раз входить в приложение, но вы можете быть уверены, что все изменения, внесенные в безопасность, будут применены, и что нет никаких остатков старого сеанса, которые не позволят вам увидеть ваши изменения.

Это небольшое улучшение-запрос на вход с веб-страницы (даже если это довольно просто по внешнему виду) всегда более удобно для пользователя, чем диалоговое окно HTTP basic. Вы настроите страницу входа в раздел 4.3.2. Однако текущей задачей является настройка хранилища пользователей, которое может обрабатывать более одного пользователя.

Как оказалось, Spring Security предлагает несколько вариантов настройки пользовательского хранилища, включая следующие:

-Хранилище пользователей в памяти

-Хранилище пользователей на основе JDBC

-Хранилище пользователей в LDAP

-Пользовательская (собственная) служба сведений о пользователе

Независимо от того, какое пользовательское хранилище выбрано, его можно настроить, переопределив метод configure(), определенный в базовом классе конфигурации WebSecurityConfigurerAdapter. Для начала добавьте в класс SecurityConfig следующее переопределение метода:

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


...


}

Теперь вам просто нужно заменить эти многоточия с кодом, который использует данный AuthenticationManagerBuilder, чтобы указать, как пользователи будут авторизоваться (looked) во время аутентификации. Сначала вы реализуете хранилище пользователей в памяти.

4.2.1 Хранилище пользователей в памяти

Одно из мест, где пользовательская информация может храниться - это в памяти. Предположим, у вас есть только несколько пользователей, ни один из которых, скорее всего, не изменится. В этом случае может быть достаточно просто определить этих пользователей как часть конфигурации безопасности.

Листинг 4.2 Определение пользователей в хранилище пользователей в памяти

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.inMemoryAuthentication()


.withUser("buzz")


.password("infinity")


.authorities("ROLE_USER")


.and()


.withUser("woody")


.password("bullseye")


.authorities("ROLE_USER");


}

Как вы можете видеть, AuthenticationManagerBuilder использует builder-style API, чтобы настроить параметры проверки подлинности. В этом случае вызов метода inMemoryAuthentication() дает возможность указать сведения о пользователе непосредственно в самой конфигурации безопасности.

Каждый вызов withUser() запускает конфигурацию для пользователя. Значение, указанное в withUser() является имя пользователя, а пароль и полномочий указываются в методах password() и authorities(). Как показано в листинге 4.2, оба пользователя имеют права доступа ROLE_USER. Пользователь buzz настроен на infinity в качестве пароля. Аналогично, пароль woody -bullseye.

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

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

4.2.2 Хранилище пользователей на основе JDBC

Сведения о пользователях часто хранятся в реляционной базе данных, и хранилище пользователей на основе JDBC кажется подходящим. В следующем списке показано, как настроить Spring Security для проверки подлинности сведений о пользователях, хранящихся в реляционной базе данных с помощью JDBC.

Листинг 4.3 Идентификация через JDBC-базу

@Autowired


DataSource dataSource;



@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.jdbcAuthentication()


.dataSource(dataSource);


}

Эта реализация configure() вызывает jdbcAuthentication() на данном AuthenticationManagerBuilder. Для этого необходимо установить DataSource, чтобы он знал, как получить доступ к DataSource. DataSource, используемый здесь, обеспечивается магией autowiring.

ПЕРЕОПРЕДЕЛЕНИЕ ПОЛЬЗОВАТЕЛЬСКИХ ЗАПРОСОВ ПО УМОЛЧАНИЮ

Хотя эта минимальная конфигурация будет работать, она делает некоторые предположения о схеме базы данных. Предполагается, что существуют определенные таблицы, в которых будут храниться пользовательские данные. В частности, следующий фрагмент кода из внутренних компонентов Spring Security показывает SQL-запросы, которые будут выполняться при поиске сведений о пользователе:

public static final String DEF_USERS_BY_USERNAME_QUERY =


"select username,password,enabled " +


"from users " +


"where username = ?";



public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =


"select username,authority " +


"from authorities " +


"where username = ?";



public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =


"select g.id, g.group_name, ga.authority " +


"from groups g, group_members gm, group_authorities ga " +


"where gm.username = ? " +


"and g.id = ga.group_id " +


"and g.id = gm.group_id";

Первый запрос извлекает имя пользователя, пароль и информацию о том, включены они или нет. Эта информация используется для аутентификации пользователя. Следующий запрос ищет предоставленные полномочия пользователя для целей авторизации, а последний запрос ищет полномочия, предоставленные пользователю как члену группы.

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

Листинг 4.4 Настройка запросов сведений о пользователе

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.jdbcAuthentication()


.dataSource(dataSource)


.usersByUsernameQuery(


"select username, password, enabled from Users " +


"where username=?")


.authoritiesByUsernameQuery(


"select username, authority from UserAuthorities " +


"where username=?");


}

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

При замене SQL-запросов по умолчанию, запросами собственной разработки важно придерживаться базового контракта запросов. Все они принимают имя пользователя в качестве единственного параметра. Запрос проверки подлинности выбирает username, password и enabled. Запрос привилегий выбирает ноль или более строк, содержащих username и authority. Запрос привилегий группы выбирает ноль или более строк, каждая с идентификатором группы, group_name и authority.

РАБОТА С ЗАКОДИРОВАННЫМИ ПАРОЛЯМИ

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

Для решения этой проблемы, вам нужно указать кодировщик пароля, обратившись к методу passwordEncoder():

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.jdbcAuthentication()


.dataSource(dataSource)


.usersByUsernameQuery(


"select username, password, enabled from Users " +


"where username=?")


.authoritiesByUsernameQuery(


"select username, authority from UserAuthorities " +


"where username=?")


.passwordEncoder(new StandardPasswordEncoder("53cr3t");


}

Метод passwordEncoder() принимает любую реализацию интерфейса PasswordEncoder Spring Security. Криптографический модуль Spring Security включает в себя несколько таких реализаций:

-BCryptPasswordEncoder—применяется bcrypt строгое шифрование хэширования (Applies bcrypt strong hashing encryption)

-NoOpPasswordEncoder—не применяется шифрование

-Pbkdf2PasswordEncoder—применяется PBKDF2 шифрования

-SCryptPasswordEncoder—применяется scrypt ширование хэширования (Applies scrypt hashing encryption)

-StandardPasswordEncoder—применяется SHA-256 ширование хэширования (Applies SHA-256 hashing encryption)

Предыдущий код использует StandardPasswordEncoder. Но можно выбрать любую из других реализаций или даже предоставить собственную пользовательскую реализацию, если ни одна из готовых реализаций не соответствует вашим потребностям. Интерфейс PasswordEncoder довольно прост:

public interface PasswordEncoder {


String encode(CharSequence rawPassword);


boolean matches(CharSequence rawPassword, String encodedPassword);


}

Независимо от того, какой кодировщик паролей вы используете, важно понимать, что пароль в базе данных никогда не декодируется. Вместо этого пароль, который пользователь вводит при входе в систему, кодируется с использованием того же алгоритма, а затем сравнивается с закодированным паролем в базе данных. Это сравнение выполняется в Passwordencoder методе matches().

В конечном счете, вы будете хранить пользовательские данные Taco Cloud в базе данных. Однако вместо того, чтобы использовать jdbcAuthentication(), у меня есть другой вариант аутентификации. Но прежде чем мы к нему перейдем, давайте посмотрим, как можно настроить Spring Security полагаться на еще один источник данных пользователей: LDAP (облегченный протокол доступа к каталогам).

4.2.3 Хранилище пользователей в LDAP

Для настройки Spring Security для аутентификации на основе LDAP можно использовать метод ldapAuthentication(). Этот метод является LDAP аналогом jdbcAuthentication(). Следующий метод configure() показывает простую конфигурацию для аутентификации LDAP:

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchFilter("(uid={0})")


.groupSearchFilter("member={0}");


}

Методы userSearchFilter() и groupSearchFilter() используются для предоставления фильтров для базовых запросов LDAP, которые используются для поиска пользователей и групп. По умолчанию базовые запросы как для пользователей, так и для групп пусты, что указывает на то, что поиск будет выполняться из корня иерархии LDAP. Но вы можете изменить это, указав базовый запрос:

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchBase("ou=people")


.userSearchFilter("(uid={0})")


.groupSearchBase("ou=groups")


.groupSearchFilter("member={0}");


}

Метод userSearchBase() предоставляет базовый запрос для поиска пользователей. Аналогичным образом, метод groupSearchBase() определяет базовый запрос для поиска групп. Вместо поиска в корневом каталоге в этом примере указывается, что пользователей следует искать в организационном разделе людей. Группы следует искать в том месте, где находится организационный раздел групп.

НАСТРОЙКА СРАВНЕНИЯ ПАРОЛЕЙ

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

Если вы предпочитаете аутентификацию путем сравнения паролей, вы можете реализваоть это с помощью метода passwordCompare():

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchBase("ou=people")


.userSearchFilter("(uid={0})")


.groupSearchBase("ou=groups")


.groupSearchFilter("member={0}")


.passwordCompare();


}

По умолчанию пароль, указанный в форме входа, будет сравниваться со значением атрибута userPassword в записи LDAP пользователя. Если пароль хранится в другом атрибуте, можно указать имя атрибута пароля с помощью passwordAttribute():

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchBase("ou=people")


.userSearchFilter("(uid={0})")


.groupSearchBase("ou=groups")


.groupSearchFilter("member={0}")


.passwordCompare()


.passwordEncoder(new BCryptPasswordEncoder())


.passwordAttribute("passcode");


}

В этом примере указывается, что атрибут passcode должен сравниваться с заданным паролем. Кроме того, вы также указываете кодировщик паролей. Приятно, что фактический пароль хранится в секрете на сервере при сравнении паролей на стороне сервера. Но попытка ввода пароля все равно передается по проводам на сервер LDAP и может быть перехвачена хакером. Чтобы предотвратить это, можно указать стратегию шифрования, вызвав метод passwordEncoder().

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

ОБРАЩЕНИЕ К УДАЛЕННОМУ СЕРВЕРУ LDAP

Единственное, я до сих пор так и не указал местоположение, где находится LDAP-сервер и на котором нужные данные фактически находяться. Вы успешно настроили Spring для аутентификации на сервере LDAP, но где этот сервер?

По умолчанию аутентификация LDAP Spring Security предполагает, что сервер LDAP прослушивает порт 33389 на localhost. Но если ваш сервер LDAP находится на другой машине, вы можете использовать метод contextSource() для настройки его местоположения:

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchBase("ou=people")


.userSearchFilter("(uid={0})")


.groupSearchBase("ou=groups")


.groupSearchFilter("member={0}")


.passwordCompare()


.passwordEncoder(new BCryptPasswordEncoder())


.passwordAttribute("passcode")


.contextSource()


.url("ldap://tacocloud.com:389/dc=tacocloud,dc=com");


}

Метод contextSource() возвращает ContextSourceBuilder, который, помимо прочего, предлагает метод url(), позволяющий указать расположение сервера LDAP.

НАСТРОЙКА ВСТРОЕННОГО СЕРВЕРА LDAP

Если у вас нет сервера LDAP, ожидающего аутентификации, Spring Security может предоставить вам встроенный сервер LDAP. Вместо установки URL-адреса удаленного сервера LDAP можно указать корневой суффикс для встроенного сервера с помощью метода root():

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchBase("ou=people")


.userSearchFilter("(uid={0})")


.groupSearchBase("ou=groups")


.groupSearchFilter("member={0}")


.passwordCompare()


.passwordEncoder(new BCryptPasswordEncoder())


.passwordAttribute("passcode")


.contextSource()


.root("dc=tacocloud,dc=com");


}

Когда сервер LDAP запустится, он попытается загрузить данные из любых LDIF-файлов, которые он может найти в пути к классам. LDIF (формат обмена данными LDAP) - это стандартный способ представления данных LDAP в текстовом файле. Каждая запись состоит из одной или нескольких строк, каждая из которых содержит имя и значение. Записи отделяются друг от друга пустыми строками.

Если вы предпочитаете, чтобы Spring не рылась в вашем classpath в поисках любых LDIF-файлов, которые он может найти, вы можете более четко указать, какой LDIF-файл загружается, вызвав метод ldif():

@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.ldapAuthentication()


.userSearchBase("ou=people")


.userSearchFilter("(uid={0})")


.groupSearchBase("ou=groups")


.groupSearchFilter("member={0}")


.passwordCompare()


.passwordEncoder(new BCryptPasswordEncoder())


.passwordAttribute("passcode")


.contextSource()


.root("dc=tacocloud,dc=com")


.ldif("classpath:users.ldif");


}

Здесь вы специально просите сервер LDAP загрузить его содержимое о пользователях. ldif-файл в корне classpath приложения. Если вам интересно, вот файл LDIF, который вы можете использовать для загрузки встроенного сервера LDAP с пользовательскими данными:

dn: ou=groups,dc=tacocloud,dc=com


objectclass: top


objectclass: organizationalUnit


ou: groups


dn: ou=people,dc=tacocloud,dc=com


objectclass: top


objectclass: organizationalUnit


ou: people


dn: uid=buzz,ou=people,dc=tacocloud,dc=com


objectclass: top


objectclass: person


objectclass: organizationalPerson


objectclass: inetOrgPerson


cn: Buzz Lightyear


sn: Lightyear


uid: buzz


userPassword: password


dn: cn=tacocloud,ou=groups,dc=tacocloud,dc=com


objectclass: top


objectclass: groupOfNames


cn: tacocloud


member: uid=buzz,ou=people,dc=tacocloud,dc=com

Spring Security’s built-in хранилища пользователей удобны и покрывают некоторые общие случаи использования. Но приложение Taco Cloud нуждается в чем-то особенном. Если готовые пользовательские хранилища не соответствуют вашим потребностям, вам потребуется создать и настроить собственную службу сведений о пользователе.

4.2.4 Собственная служба сведений о пользователе

В последней главе вы остановились на использовании Spring Data JPA в качестве опции сохранения для всех данных taco, ingredient и order. Таким образом, было бы целесообразно сохранить пользовательские данные таким же образом. Если вы сделаете это, данные будут в конечном счете находиться в реляционной базе данных, поэтому вы можете использовать аутентификацию на основе JDBC. Но было бы еще лучше использовать хранилище данных Spring, используемое для хранения пользователей.

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

ОПРЕДЕЛЕНИЕ ДОМЕНА ПОЛЬЗОВАТЕЛЯ И СОХРАНЯЕМОСТИ(PERSISTENCE)

Когда клиенты Taco Cloud регистрируются в приложении, им необходимо предоставить не только имя пользователя и пароль. Они также дадут вам свое полное имя, адрес и номер телефона. Эта информация может быть использована для различных целей, включая предварительное заполнение формы заказа (не говоря уже о потенциальных маркетинговых возможностях).

Чтобы получить всю эту информацию, создайте класс User, как показано ниже.

Листинг 4.5 Описание сущности user

package tacos;



import java.util.Arrays;


import java.util.Collection;


import javax.persistence.Entity;


import javax.persistence.GeneratedValue;


import javax.persistence.GenerationType;


import javax.persistence.Id;


import org.springframework.security.core.GrantedAuthority;


import org.springframework.security.core.authority.


SimpleGrantedAuthority;


import org.springframework.security.core.userdetails.UserDetails;


import lombok.AccessLevel;


import lombok.Data;


import lombok.NoArgsConstructor;


import lombok.RequiredArgsConstructor;



@Entity


@Data


@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)


@RequiredArgsConstructor


public class User implements UserDetails {


private static final long serialVersionUID = 1L;



@Id


@GeneratedValue(strategy=GenerationType.AUTO)


private Long id;



private final String username;


private final String password;


private final String fullname;


private final String street;


private final String city;


private final String state;


private final String zip;


private final String phoneNumber;



@Override


public Collection getAuthorities() {


return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));


}



@Override


public boolean isAccountNonExpired() {


return true;


}



@Override


public boolean isAccountNonLocked() {


return true;


}



@Override


public boolean isCredentialsNonExpired() {


return true;


}


@Override


public boolean isEnabled() {


return true;


}


}

Вы, несомненно, заметили, что класс User немного более связанный, чем любая другая сущность, определенная в главе 3. В дополнение к определению нескольких свойств, пользователь также реализует интерфейс UserDetails из Spring Security.

Implementations UserDetails предоставит некоторую важную информацию о пользователе от framework, например, какие полномочия предоставлены пользователю и включена ли учетная запись пользователя.

Метод getAuthorities() должен возвращать коллекцию полномочий, предоставленных пользователю. Различные методы is___Expired () возвращают логическое значение, указывающее, включена или нет учетная запись пользователя.

Для сущности User метод getAuthorities () просто возвращает коллекцию, указывающую, что всем пользователям будут предоставлены полномочия ROLE_USER. И, по крайней мере, на данный момент, Taco Cloud не нужно отключать пользователей, поэтому все методы is___Expired() возвращают true, чтобы указать, что пользователи активны.

Определив сущность User, вы можете определить интерфейс репозитория:

package tacos.data;



import org.springframework.data.repository.CrudRepository;


import tacos.User;



public interface UserRepository extends CrudRepository {


User findByUsername(String username);


}

Помимо операций CRUD, предоставляемых расширением CrudRepository, UserRepository определяет метод findByUsername(), который будет использоваться в сервисе сведений о пользователе для поиска пользователя по его имени пользователя.

Как вы узнали в главе 3, Spring Data JPA будет автоматически генерировать реализацию этого интерфейса во время выполнения. Таким образом, теперь можно написать пользовательский сервис ,сведений о пользователе использующую этот репозиторий.

СОЗДАНИЕ СЕРВИСА СВЕДЕНИЙ О ПОЛЬЗОВАТЕЛЕ

UserDetailsService Spring Security - это довольно простой интерфейс:

public interface UserDetailsService {


UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;


}

Как вы можете видеть, реализации этого интерфейса даны имя пользователя и должны либо вернуть объект UserDetails или бросить UsernameNotFoundException, если данный логин не появляется никаких результатов.

Поскольку класс User implements UserDetails, а UserRepository предоставляет метод findByUsername(), они идеально подходят для использования в пользовательской реализации UserDetailsService. В следующем листинге показан сервис сведений о пользователе, который вы будете использовать в приложении Taco Cloud.

Листинг 4.6 Определение собственного сервиса сведений о пользователе

package tacos.security;



import org.springframework.beans.factory.annotation.Autowired;


import org.springframework.security.core.userdetails.UserDetails;


import org.springframework.security.core.userdetails.UserDetailsService;


import org.springframework.security.core.userdetails.UsernameNotFoundException;


import org.springframework.stereotype.Service;


import tacos.User;


import tacos.data.UserRepository;



@Service


public class UserRepositoryUserDetailsService implements UserDetailsService {


private UserRepository userRepo;



@Autowired


public UserRepositoryUserDetailsService(UserRepository userRepo) {


this.userRepo = userRepo;


}



@Override


public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


User user = userRepo.findByUsername(username);


if (user != null) {


return user;


}


throw new UsernameNotFoundException("User '" + username + "' not found");


}


}

UserRepositoryUserDetailsService внедряется с экземпляром UserRepository через его конструктор. Затем в методе loadByUsername(), вызывается findByUsername () объекта UserRepository для поиска User.

Метод loadByUsername () имеет одно простое правило: он никогда не должен возвращать null. Поэтому, если вызов findByUsername() возвращает null, loadByUsername() будет бросать UsernameNotFoundException. В противном случае будет возвращен найденный пользователь.

Вы видите, что UserRepositoryUserDetailsService аннотируется @Service. Это еще одна из аннотаций стереотипа Spring, которые помечают его для включения в сканирование компонентов Spring, поэтому нет необходимости явно объявлять этот класс как bean. Spring автоматически обнаружит его и создаст его как bean.

Однако по-прежнему необходимо настроить сервис сведений о пользователе с Spring Security. Таким образом, вы вернетесь к методу configure() еще раз:

@Autowired


private UserDetailsService userDetailsService;



@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.userDetailsService(userDetailsService);


}

На этот раз просто вызовите метод userDetailsService (), передав экземпляр UserDetailsService, который был autowired к SecurityConfig.

Как и при аутентификации на основе JDBC, вы можете (и должны) также настроить кодировщик паролей, чтобы пароль мог быть закодирован в базе данных. Это можно сделать, сначала объявив bean типа PasswordEncoder, а затем внедрив (injecting) его в конфигурацию сервиса сведений о пользователе, вызвав passwordEncoder():

@Bean


public PasswordEncoder encoder() {


return new StandardPasswordEncoder("53cr3t");


}



@Override


protected void configure(AuthenticationManagerBuilder auth) throws Exception {


auth.userDetailsService(userDetailsService)


.passwordEncoder(encoder());


}

Важно обсудить последнюю строку в методе configure(). Казалось бы, вы вызываете метод encoder() и передаете его возвращаемое значение в passwordEncoder(). В действительности, однако, поскольку метод encoder() аннотируется @Bean, он будет использоваться для объявления компонента PasswordEncoder в контексте приложения Spring. Любые вызовы encoder() будут перехвачены, чтобы возвратить экземпляр bean из контекста приложения.

РЕГИСТРАЦИЯ ПОЛЬЗОВАТЕЛЕЙ

Несмотря на то, что Spring Security обрабатывает многие аспекты безопасности, она не принимает непосредственного участия в процессе регистрации пользователей, поэтому вы будете полагаться на Spring MVC для обработки этой задачи. Класс RegistrationController в следующем листинге представляет и обрабатывает регистрационные формы.

Листинг 4.7 Контроллер регистрации пользователей

package tacos.security;



import org.springframework.security.crypto.password.PasswordEncoder;


import org.springframework.stereotype.Controller;


import org.springframework.web.bind.annotation.GetMapping;


import org.springframework.web.bind.annotation.PostMapping;


import org.springframework.web.bind.annotation.RequestMapping;


import tacos.data.UserRepository;



@Controller


@RequestMapping("/register")


public class RegistrationController {


private UserRepository userRepo;


private PasswordEncoder passwordEncoder;



public RegistrationController(UserRepository userRepo, PasswordEncoder passwordEncoder) {


this.userRepo = userRepo;


this.passwordEncoder = passwordEncoder;


}



@GetMapping


public String registerForm() {


return "registration";


}



@PostMapping


public String processRegistration(RegistrationForm form) {


userRepo.save(form.toUser(passwordEncoder));


return "redirect:/login";


}


}

Как любой типичный Spring MVC controller, RegistrationController аннотирован @Controller для того чтобы обозначить его как controller и отметить его для сканирования компонентов (component scanning). Он также аннотируется @RequestMapping, так что он будет обрабатывать запросы, путь которых /register.

В частности, запрос GET для /register будет обрабатываться методом registerForm (), который просто возвращает логическое имя страницы регистрации. В следующем списке показан шаблон Thymeleaf, определяющий страницу регистрации.

Листинг 4.8 Thymeleaf структура формы регистрации



xmlns:th="http://www.thymeleaf.org">



Taco Cloud




Register


































После отправки данных формы, запрос HTTP POST будет обработан методом processRegistration(). Объект RegistrationForm, передаваемый в processRegistration(), привязан к данным запроса и описывается следующим классом:

package tacos.security;



import org.springframework.security.crypto.password.PasswordEncoder;


import lombok.Data;


import tacos.User;



@Data


public class RegistrationForm {


private String username;


private String password;


private String fullname;


private String street;


private String city;


private String state;


private String zip;


private String phone;



public User toUser(PasswordEncoder passwordEncoder) {


return new User(


username, passwordEncoder.encode(password),


fullname, street, city, state, zip, phone);


}


}

По большей части RegistrationForm - это простой класс с поддержкой Lombok и несколькими свойствами. Но метод toUser() использует эти свойства для создания нового пользовательского объекта, который будет сохранен с помощью внедренного UserRepository.

Вы, без сомнения, заметили, что RegistrationController внедряется с PasswordEncoder. Это точно тот же bean PasswordEncoder который вы объявили ранее. При обработке данных формы, RegistrationController передает ее методу toUser(), который использует ее для кодирования пароля перед сохранением в базу данных. Таким образом, отправленный пароль записывается в закодированном виде, и сервис сведений о пользователе сможет пройти проверку подлинности с использованием этого закодированного пароля.

Теперь приложение Taco Cloud имеет полную поддержку регистрации и аутентификации пользователей. Но если вы запустите его сейчас, вы заметите, что вы даже не можете попасть на страницу регистрации без запроса на вход. Потому что, по умолчанию, все запросы требуют проверки подлинности. Давайте посмотрим, как веб-запросы перехватываются и защищаются, чтобы вы могли исправить эту странную ситуацию с курицей и яйцом.

4.3 Защита веб-запросов

Требования безопасности для Taco Cloud должны требовать, чтобы пользователь аутентифицировался перед созданием тако или размещением заказов. Но домашняя страница, страница входа и страница регистрации должны быть доступны неавторизованным пользователям.

Чтобы настроить эти правила безопасности, позвольте мне представить вам другой метод configure() класса WebSecurityConfigurerAdapter:

@Override


protected void configure(HttpSecurity http) throws Exception {


...


}

Этот метод configure() принимает объект HttpSecurity, который может использоваться для настройки обработки безопасности на веб-уровне. Среди многих вещей, которые вы можете настроить с помощью HttpSecurity следующие:

-Требование соблюдения определенных условий безопасности перед обработкой запроса

-Настройка пользовательской страницы входа

-Возможность выхода пользователей из приложения

-Настройка защиты от подделки межсайтовых запросов

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

4.3.1 Защита запросов

Необходимо убедиться, что запросы /design и /orders доступны только авторизованным пользователям; все остальные запросы должны быть разрешены для всех пользователей. Следующая реализация configure() делает именно это:

@Override


protected void configure(HttpSecurity http) throws Exception {


http.authorizeRequests()


.antMatchers("/design", "/orders")


.hasRole("ROLE_USER")


.antMatchers(“/”, "/**").permitAll();


}

Вызов метода AuthorizationRequests() возвращает объект (ExpressionInterceptUrlRegistry), в котором можно указать URL-пути, шаблоны и требования безопасности для этих путей. В этом случае необходимо указать два правила безопасности:

-Запросы /design и /orders должны быть доступны для пользователей с предоставленными полномочиями ROLE_USER.

-Все запросы должны быть разрешены всем пользователям.

Порядок этих правил важен. Правила безопасности, объявленные первыми, имеют приоритет над правилами, объявленными ниже. Если бы вы поменяли порядок этих двух правил безопасности, все запросы имели бы permitAll(), примененный к ним; правило для /design и /orders запросов не имело бы никакого эффекта.

Методы hasRole() и allowAll() - это всего лишь несколько методов объявления требований безопасности для путей запросов. Таблица 4.1 описывает все доступные методы.

Табл. 4.1 Методы настройки для определения способа защиты пути


Метод

Описание


access(String)

Разрешает доступ, если данное выражение SpEL имеет значение true


anonymous()

Разрешает доступ анонимным пользователям


authenticated()

Разрешает доступ аутентифицированным пользователям


denyAll()

Безоговорочно запрещает доступ


fullyAuthenticated()

Разрешает доступ, если пользователь полностью аутентифицирован (не запоминается)


hasAnyAuthority(String...)

Разрешает доступ, если у пользователя есть какие-либо из указанных полномочий


hasAnyRole(String...)

Разрешает доступ, если пользователь имеет любую из указанных ролей


hasAuthority(String)

Разрешает доступ, если пользователь имеет полномочия


hasIpAddress(String)

Разрешает доступ, если запрос поступает с указанного IP-адреса


hasRole(String)

Разрешает доступ, если пользователь имеет данную роль


not()

Отрицает эффект любого другого метода доступа


permitAll()

Разрешает безоговорочный доступ


rememberMe()

Разрешает доступ пользователям, прошедшим проверку подлинности с помощью remember-me (запомни меня)


Большинство методов, описанных в табл. 4.1, предоставляют основные правила безопасности для обработки запросов, но они являются самоограничивающимися и разрешают только правила безопасности, определенные этими методами. Кроме того, можно использовать метод access() для предоставления выражения SpEL для объявления расширенных правил безопасности. Spring Security расширяет SpEL, чтобы включить несколько определенных для безопасности значений и функций, как указано в таблице 4.2.

Таблица 4.2 Spring Security расширения Spring Expression Language


Выражение безопасности

Возвращаемое значение


authentication

Объект проверки подлинности пользователя


denyAll

Всегда имеет значение false


hasAnyRole(list of roles)

true, если пользователь имеет любую из заданных ролей


hasRole(role)

true, если пользователь имеет заданную роль


hasIpAddress(IP address)

true, если запрос пришел с заданого IP-адреса


isAnonymous()

true, если пользователь является анонимным


isAuthenticated()

true, если пользователь прошел проверку подлинности


isFullyAuthenticated()

true, если пользователь полностью аутентифицирован (не включая аутентификацию с remember-me (запомни меня))


isRememberMe()

true, если пользователь прошел аутентификацию через remember-me (запомни меня)


permitAll

Всегда принимает значение true


principal

Основной объект пользователя


Как вы можете видеть, большинство расширений выражений безопасности в таблице 4.2 соответствуют аналогичным методам в таблице 4.1. На самом деле, используя метод access() вместе с выражениями hasRole() и permitAll, вы можете переписать configure() следующим образом.

Листинг 4.9 Использование выражений Spring для определения правил авторизации

@Override


protected void configure(HttpSecurity http) throws Exception {


http.authorizeRequests()


.antMatchers("/design", "/orders")


.access("hasRole('ROLE_USER')")


.antMatchers(“/”, "/**").access("permitAll");


}

Сначала это может показаться не таким уж большим делом. В конце концов, эти выражения отражают только то, что вы уже сделали с вызовами методов. Но выражения могут быть гораздо более гибкими. Например, предположите, что (по какой-то сумасшедшей причине) вы только хотели позволить пользователям с полномочиями ROLE_USER создавать новые тако в вторникам (например, Taco Tuesday); вы могли переписать выражение как показано в этой модифицированной версии configure():

@Override


protected void configure(HttpSecurity http) throws Exception {


http.authorizeRequests()


.antMatchers("/design", "/orders")


.access("hasRole('ROLE_USER') && " +


"T(java.util.Calendar).getInstance().get("+


"T(java.util.Calendar).DAY_OF_WEEK) == " +


"T(java.util.Calendar).TUESDAY")


.antMatchers(“/”, "/**").access("permitAll");


}

С SpEL-based ограничениями безопасности возможности практически безграничны. Бьюсь об заклад, что вы уже придумываете интересные ограничения безопасности, основанные на SpEL.

Потребности в авторизации для приложения Taco Cloud удовлетворяются простым использованием access() и выражений SpEL в листинге 4.9. Теперь давайте посмотрим настройки страницы входа в систему приложения Taco Cloud.

4.3.2 Создание пользовательской страницы входа

Default-ная страница входа намного лучше, чем неуклюжее диалоговое окно HTTP basic, с которого вы начали, но она все еще довольно просто и не совсем вписывается в стиль остальной части приложения Taco Cloud.

Чтобы заменить встроенную страницу входа, сначала необходимо сообщить Spring Security, по какому пути будет находиться ваша пользовательская страница входа. Это можно сделать, вызвав formLogin() для объекта HttpSecurity, переданного в configure():

@Override


protected void configure(HttpSecurity http) throws Exception {


http.authorizeRequests()


.antMatchers("/design", "/orders")


.access("hasRole('ROLE_USER')")


.antMatchers(“/”, "/**").access("permitAll")


.and()


.formLogin()


.loginPage("/login");


}

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

После перемычки and() вызовите formLogin(), чтобы начать настройку пользовательской формы входа. Вызов loginPage() после этого определяет путь, где будет предоставлена ваша пользовательская страница входа. Когда Spring Security определит, что пользователь не прошел проверку подлинности и должен войти в систему, он перенаправит их по этому пути.

Теперь необходимо предоставить контроллер, обрабатывающий запросы по этому пути. Поскольку ваша страница входа в систему будет довольно простой (ничего, кроме представления) достаточно легко объявить ее контроллером представления в WebConfig. Следующие метод addViewControllers() устанавливает login page view контроллер наряду с контроллером представления, для "/" home контроллера:

@Override


public void addViewControllers(ViewControllerRegistry registry) {


registry.addViewController("/").setViewName("home");


registry.addViewController("/login");


}

Наконец, необходимо определить само представление страницы входа. Поскольку вы используете Thymeleaf в качестве механизма шаблонов, следующий шаблон Thymeleaf должен нам подойти:




Taco Cloud




Login



Unable to login. Check your username and password.


New here? Click


here to register.














Ключевые вещи, которые следует отметить об этой странице входа в систему, - это путь, который она публикует, и имена полей имени пользователя и пароля. По умолчанию Spring Security прослушивает запросы на вход в /login и ожидает, что поля username и password будут называться username и password. Но это можено настроить. Например, следующая конфигурация настраивает путь и имена полей :

.and().formLogin()


.loginPage("/login")


.loginProcessingUrl("/authenticate")


.usernameParameter("user")


.passwordParameter("pwd")

Здесь вы указываете, что Spring Security должна прослушивать запросы /authenticate для обработки запросов на вход. Кроме того, поля username и password теперь должны быть названы user и pwd.

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

.and().formLogin()


.loginPage("/login")


.defaultSuccessUrl("/design")

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

При желании вы можете принудительно заставить пользователя перейти на страницу design после входа в систему, даже если он находился на другой странце до входа в систему, передав значение true в качестве второго параметра в defaultSuccessUrl:

.and().formLogin()


.loginPage("/login")


.defaultSuccessUrl("/design", true)

Теперь, когда вы имеете дело с пользовательской страницей входа, давайте перейдем на другую сторону монеты аутентификации и посмотрим, как вы можете позволить пользователю выйти из системы.

4.3.3 Выход из системы

Выход из системы так же важен, как и вход в приложение. Чтобы выйти, -вам просто нужно вызвать logout для объекта HttpSecurity:

.and().logout()


.logoutSuccessUrl("/")

Это настраивает фильтр безопасности, который перехватывает запросы POST для /logout. Поэтому, чтобы обеспечить возможность выхода из системы, вам просто нужно добавить форму и кнопку выхода из системы в представлениях вашего приложения:



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

.and().logout()


.logoutSuccessUrl("/")

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

4.3.4 Предотвращение подделки межсайтовых запросов

Подделка межсайтовых запросов (CSRF) является обычной атакой безопасности. Он включает в себя предоставление пользователю кода на злонамеренно разработанной веб-странице, которая автоматически (и обычно тайно) отправляет форму другому приложению от имени пользователя, который часто является жертвой атаки. Например, пользователю может быть представлена форма на веб-сайте злоумышленника, которая автоматически публикует URL-адрес на банковском веб-сайте пользователя (который предположительно плохо спроектирован и уязвим для такой атаки) для перевода денег. Пользователь может даже не знать, что атака произошла, пока не заметит пропажу денег со своего счета.

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

К счастью, Spring Security имеет встроенную защиту CSRF. Еще более удачным является то, что она включена по умолчанию, и вам не нужно явно настраивать её. Необходимо только убедиться, что все формы, отправляемые приложением, содержат поле с именем _csrf, содержащее токен CSRF.

Spring Security даже упрощает это, помещая токен CSRF в атрибут запроса с именем _csrf. Таким образом можно отобразить токен CSRF в скрытом поле в шаблоне Thymeleaf:

Если вы используете библиотеку тегов Spring MVC’s JSP или Thymeleaf с Spring Security dialect, вам даже не нужно явно создавать скрытое поле. Скрытое поле будет создано автоматически.

В Thymeleaf, вам просто нужно убедиться, что один из атрибутов элемента

с префиксом в качестве атрибута Thymeleaf. Это обычно не проблема, так как довольно часто Thymeleaf отображает путь как относительный контекст. Например, атрибут th:action-это все, что вам нужно для отображения скрытого поля Thymeleaf:

Можно отключить поддержку CSRF, но я не решаюсь показать вам, как это сделать. CSRF-защита важна и легко обрабатывается в формах, поэтому нет причин ее отключать. Но если вы настаиваете на его отключении, вы можете сделать это, вызвав disable() следующим образом:

.and().csrf().disable()

Опять же, я предупреждаю вас не отключать защиту CSRF, особенно для производственных приложений.

Вся ваша безопасность веб-уровня теперь настроена для Taco Cloud. Помимо прочего, теперь у вас есть настраиваемая страница входа и возможность аутентификации пользователей в репозитории пользователей с поддержкой JPA. Теперь давайте посмотрим, как можно получить информацию о зарегистрированных пользователей.

4.4 Определение пользователя

Часто недостаточно просто знать, что пользователь вошел в систему. Обычно важно также знать, кто он, чтобы вы могли представить вид и функционал приложения в зависимости от вошедшего пользователя.

Например, в OrderController при первоначальном создании объекта Order, связанного с формой заказа, было бы неплохо предварительно заполнить заказ именем и адресом пользователя, чтобы ему не пришлось повторно вводить его для каждого заказа. Возможно, еще более важно, что при сохранении заказа необходимо связать сущность Order с пользователем, создавшим заказ.

Для достижения желаемой связи между сущностью Order и сущностью User необходимо добавить новое свойство в класс Order:

@Data


@Entity


@Table(name="Taco_Order")


public class Order implements Serializable {


...



@ManyToOne


private User user;


...


}

Аннотация @ManyToOne этого свойства указывает, что заказ принадлежит одному пользователю, и, наоборот, что у пользователя может быть много заказов. (Поскольку вы используете Lombok, вам не нужно явно определять методы доступа для свойства.)

В OrderController метод processOrder() отвечает за сохранение заказа. Его необходимо изменить, чтобы определить, кто является аутентифицированным пользователем, и вызвать setUser () у объекта Order, чтобы связать заказ с пользователем.

Существует несколько способов определить пользователя. Вот несколько из наиболее распространенных способов:

-Inject основные(Principal) объекты в метод контроллера

-Inject объект аутентификации(Authentication) в метод контроллера

-Использовать SecurityContextHolder, чтобы получить в контексте безопасности

-Используйте аннотированный метод @AuthenticationPrincipal

Например, можно изменить processOrder(), чтобы он принимал java.security.Principal в качестве параметра. Затем вы можете использовать основное(principal) имя для поиска пользователя используя UserRepository:

@PostMapping


public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus,


Principal principal) {


...


User user = userRepository.findByUsername(


principal.getName());


order.setUser(user);


...


}

Это отлично работает, но засоряет код, который иначе не связан с безопасностью с кодом безопасности. Можно урезать часть кода безопасности, изменив processOrder(), чтобы он принимал Authentication объект в качестве параметра вместо Principal:

@PostMapping


public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus,


Authentication authentication) {


...


User user = (User) authentication.getPrincipal();


order.setUser(user);


...


}

С Authentication в руках, вы можете вызвать getPrincipal(), чтобы получить основной (principal) объект, который в этом случае является User. Обратите внимание, что getPrincipal() возвращает java.util.Object, поэтому вам нужно привести его к User.

Однако, возможно, самым чистым решением является просто принимать на вход объект User в методе processOrder(), но аннотировать его @AuthenticationPrincipal, чтобы он был субъектом проверки подлинности:

@PostMapping


public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus,


@AuthenticationPrincipal User user) {


if (errors.hasErrors()) {


return "orderForm";


}


order.setUser(user);


orderRepo.save(order);


sessionStatus.setComplete();


return "redirect:/";


}

Что хорошо в @AuthenticationPrincipal, так это то, что он не требует приведения (как с Authentication), и он ограничивает код безопасности самой аннотацией. К моменту получения объекта User в processOrder() он готов к использованию для Оrder.

Есть еще один способ определить, кто является аутентифицированным пользователем, хотя это немного грязно в том смысле, что он очень тяжел для кода, специфичного для безопасности. Вы можете получить объект аутентификации из контекста безопасности и затем запросить его участника (principal) следующим образом:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();


User user = (User) authentication.getPrincipal();

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

Итог:

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

-User details могут управляться в хранилищах пользователей, поддерживаемых реляционными базами данных,

LDAP или полностью настраиваемыми реализациями.

-Spring Security автоматически защищает от CSRF-атак.

-Информация об аутентифицированном пользователе может быть получена через объект SecurityContext (возвращается из SecurityContextHolder.getContext () ) или внедряется в контроллеры с помощью @AuthenticationPrincipal.

Spring in Action Covers Spring 5.0 перевод на русский. Глава 5

5. Работа со свойствами конфигурации

В этой главе рассматриваются

Тонкая настройка автоконфигурирования bean-ов

Применение свойств конфигурации к компонентам приложения

Работа с Spring профилями.

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

В некотором смысле автоконфигурация Spring Boot выглядит также. Автоматическая конфигурация значительно упрощает разработку приложений Spring. Но после десятилетия установки значений свойств в конфигурации Spring XML и вызова методов setter в экземплярах bean не сразу видно, как установить свойства bean, для которых нет явной конфигурации.

К счастью, Spring Boot предоставляет способ со свойствами конфигурации. Свойства конфигурации - это не что иное, как свойства компонентов в контексте приложения Spring, которые можно задать из нескольких источников свойств, включая системные свойства JVM, аргументы командной строки и переменные среды.

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

5.1 Тонкая настройка автоконфигурации

Прежде чем мы слишком глубоко погрузимся в свойства конфигурации, важно установить, что в Spring: есть два разных (но связанных) типа конфигураций:

-Bean wiring - конфигурация, которая объявляет компоненты приложения, которые будут созданы как bean в контексте приложения Spring и как они должны быть внедрены друг в друга.

-Property injection - конфигурация, задающая значения для компонентов в контексте приложения Spring.

И в конфигурации Spring на основе XML и Java конфигурации, эти два типа конфигураций часто объявляются явно в одном и том же месте. В конфигурации Java метод @Bean - аннотированный, вероятно, создаст экземпляр bean, а затем установит значения его свойств. Например, рассмотрим следующий метод @Bean, объявляющий источник данных для встроенной базы данных H2:

@Bean

public DataSource dataSource() {

return new EmbeddedDataSourceBuilder()

.setType(H2)

.addScript("taco_schema.sql")

.addScripts("user_data.sql", "ingredient_data.sql")

.build();

}

Здесь методы addScript() и addScripts() задают некоторые строковые свойства с именами SQL-скриптов, которые должны применяться к базе данных после того, как источник данных готов. Таким образом вы можете настроить bean-компонент DataSource, если вы не используете Spring Boot. Автоконфигурация делает этот метод совершенно ненужным.

Если зависимость (dependency) H2 доступна в пути к классам во время выполнения, Spring Boot автоматически создает соответствующий компонент DataSource в контексте приложения Spring. Bean применяет сценарии SQL schema.sql и data.sql.

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

5.1.1 Понимание абстракции среды Spring

Абстракция среды Spring - это универсальный магазин для любого настраиваемого свойства. Он абстрагирует происхождение свойств, так что bean-ы, нуждающиеся в этих свойствах, могут получать их из среды Spring. Окружающая среда Spring получает их от нескольких источников свойств, включая:

-Свойства системы JVM

-Переменные среды операционной системы

-Аргумент командной строки

-Конфигурационные файлы приложения

Затем он агрегирует эти свойства в один источник, из которого можно производить внедрения Spring bean-ов. На рис. 5.1 показано, как свойства из источников свойств перетекают через абстракцию среды Spring в Spring beans.

Рисунок 5.1 Spring окружение подтягивает свойства из различных источников, и делает их доступными для bean-ов в контексте приложения.

Bean-компоненты, которые автоматически настраиваются Spring Boot, настраиваются свойствами, полученными из среды Spring. В качестве простого примера предположим, что вы хотите, чтобы базовый контейнер сервлета приложения прослушивал запросы на каком-либо порту, отличном от порта по умолчанию 8080. Для этого укажите другой порт, установив свойство server.port в src/main/resources/application.properties:

server.port=9090

Лично я предпочитаю использовать YAML при настройке свойств конфигурации. Поэтому, вместо того, чтобы использовать application.properties, я мог бы создал server.port в src / main/resources/application.yml:

server:

port: 9090

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

$ java -jar tacocloud-0.0.5-SNAPSHOT.jar --server.port=9090

Если вы хотите, чтобы приложение всегда запускалось на определенном порту, вы можете установить его один раз в качестве переменной среды операционной системы:

$ export SERVER_PORT=9090

Обратите внимание, что при установке свойств в качестве переменных среды стиль именования немного отличается, чтобы учесть ограничения, накладываемые операционной системой на имена переменных среды. Все нормально. Spring может интерпретировать SERVER_PORT как server.port без проблем.

Как я уже сказал, существует несколько способов настройки свойств конфигурации. И когда мы перейдем к главе 14, вы увидите еще один способ установки свойств конфигурации на централизованном сервере конфигурации. Фактически, есть несколько сотен свойств конфигурации, которые вы можете использовать для настройки, в том числе и для настройки поведения Spring bean-ов. Вы уже видели несколько: server.port в этой главе и security.user.name и security.user.password в предыдущей главе.

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

5.1.2 Конфигурация источника данных

На данный момент приложение Taco Cloud все еще не завершено, но у вас будет еще несколько глав, чтобы позаботиться об этом, прежде чем вы будете готовы развернуть приложение. Таким образом, встроенная база данных H2, которую вы используете в качестве источника данных, идеально подходит для ваших нужд. Но как только вы возьмете приложение в производство, вы, вероятно, захотите рассмотреть более постоянное решение для базы данных.

Хотя вы можете явно настроить свой собственный компонент источника данных, это обычно не требуется. Вместо этого проще настроить URL-адрес и учетные данные для базы данных с помощью свойства конфигурации. Например, если бы вы должны были начать использовать базу данных MySQL, вы могли бы добавить следующие свойства конфигурации к приложению в формате YML:

spring:

datasource:

url: jdbc:mysql://localhost/tacocloud

username: tacodb

password: tacopassword

Хотя вам нужно будет добавить соответствующий драйвер JDBC в сборку, вам обычно не нужно будет указывать класс драйвера JDBC; Spring Boot может выяснить это из структуры URL базы данных. Но если возникнет проблема, вы можете установить свойство spring.datasource.driver-class-name :

spring:

datasource:

url: jdbc:mysql://localhost/tacocloud

username: tacodb

password: tacopassword

driver-class-name: com.mysql.jdbc.Driver

Spring Boot использует эти данные подключения при автоконфигурировании компонента DataSource. Компонент DataSource будет объединен с помощью пула соединений JDBC Tomcat, если он доступен в пути к классам. Если нет, Spring Boot ищет и использует одну из этих реализаций пула соединений в пути к классам:

-HikariCP

-Commons DBCP 2

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

Ранее в этой главе мы предположили, что можно указать сценарии инициализации базы данных для запуска при запуске приложения. В этом случае,будут полезны свойства spring.datasource.schema и spring.datasource.data:

spring:

datasource:

schema:

- order-schema.sql

- ingredient-schema.sql

- taco-schema.sql

- user-schema.sql

data:

- ingredients.sql

Возможно, явная конфигурация источника данных не ваш стиль. Вместо этого, возможно, вы предпочтете настроить источник данных в JNDI и Spring найдет его там. В этом случае настройте источник данных, настроив spring.datasource.jndi-name:

spring:

datasource:

jndi-name: java:/comp/env/jdbc/tacoCloudDS

Если вы установите свойство spring.datasource.jndi-name, другие свойства соединения с источником данных (если задано) игнорируются.

5.1.3 Настройка встроенного сервера

Вы уже видели, как установить порт контейнера сервлета, установив server.port. Я не показал вам, что происходит, если server.port 0:

server:

port: 0

Но базовый сервер-это не просто порт. Одна из наиболее распространенных вещей, которые вам нужно сделать с базовым контейнером, - настроить его для обработки HTTPS-запросов. Чтобы сделать это, первое, что вы должны сделать, это создать хранилище ключей с помощью утилиты командной строки keytool JDK:

$ keytool -keystore mykeys.jks -genkey -alias tomcat -keyalg RSA

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

Затем вам нужно будет установить несколько свойств для включения HTTPS на встроенном сервере. Вы можете указать их все в командной строке, но это было бы ужасно неудобно. Вместо этого вы, вероятно, установите их в файле application.properties или In application.yml формате YML. В In application.yml, свойства могут выглядеть следующим образом:

server:

port: 8443

ssl:

key-store: file:///path/to/mykeys.jks

key-store-password: letmein

key-password: letmein

Тут свойство server.port имеет значение 8443, что является общепринятым значением для разработки HTTPS-серверов. Свойство server.ssl.key-store должно быть задано значением пути, где расположен создаваемый файл keystore. Здесь он задан с file:// URL для загрузки его из файловой системы, но если вы упакуете его в файл JAR приложения, вы должны использовать classpath: URL для ссылки на него. И оба свойства server.ssl.key-store-password и задаются значением пароля, который был задан при создании хранилища ключей.

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

5.1.4 Конфигурация логирования

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

По умолчанию Spring Boot настраивает ведение журнала через Logback (http://logback.qos.ch) для записи на консоль на информационном уровне. Вероятно, вы уже видели множество записей INFO уровня в журналах приложений при запуске приложения и других примерах.

Для полного контроля над конфигурацией ведения журнала можно создать logback.xml-файл в корневом каталоге пути к классам (в src/main/resources). Вот пример простого logback.xml-файл, который можно использовать:

%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

Помимо шаблона, используемого для ведения журнала, эта конфигурация Logback более или менее эквивалентна используемой по умолчанию, которое вы получите, если у вас нет logback.xml файл. Но путем редактирования logback.XML вы можете получить полный контроль над log файлами приложения.

ПРИМЕЧАНИЕ: Особенности настройки logback.xml выходят за рамки этой книги. Обратитесь к документации Logback для получения дополнительной информации.

Наиболее распространенные изменения, которые вы внесете в конфигурацию логирования, - это изменение уровней логирования и, возможно, указание файла, в который должны быть записаны логи. С помощью свойств конфигурации Spring Boot вы можете вносить эти изменения без необходимости создания файла logback.xml.

Чтобы установить уровень логирования, нужно задать свойства, которые начинаются с logging.level, за которым следует имя регистратора, для которого вы хотите установить уровень ведения журнала. Например, предположим, что вы хотите установить корневой уровень (root logging level) логирования WARN, а логирование Spring Security в уровень DEBUG. Следующие записи в application.yml позаботятся об этом для вас:

logging:

level:

root: WARN

org:

springframework:

security: DEBUG

При необходимости можно свернуть имя пакета Spring Security в одну строку для удобства чтения:

logging:

level:

root: WARN

org.springframework.security: DEBUG

Теперь предположим, что вы хотите записать log-и в файл TacoCloud.log в /var/logs/. Свойства logging.path и logging.file могут помочь в этом:

logging:

path: /var/logs/

file: TacoCloud.log

level:

root: WARN

org:

springframework:

security: DEBUG

Предполагая, что приложение имеет разрешения на запись в /var/logs/, записи журнала будут записаны в /var/logs/TacoCloud.log. По умолчанию log файлы создаются новые по достижении размера 10 МБ.

5.1.5 Использование специальных значений свойств

При настройке свойств вы не ограничиваетесь объявлением их значений как жестко закодированных строковых и числовых значений. Вместо этого можно получить их значения из других свойств конфигурации.

Например, предположим (по какой-либо причине), что вы хотите установить свойство с именем greeting.welcome на основе значение другого свойства с именем spring.application.name. Для этого при настройке приветствия можно использовать маркеры-заполнители $ {} для задания свойства greeting.welcome:

greeting:

welcome: ${spring.application.name}

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

greeting:

welcome: You are using ${spring.application.name}.

Как вы уже видели, настройка собственных компонентов Spring со свойствами конфигурации позволяет легко вводить значения в свойства этих компонентов и настраивать автоконфигурацию. Свойства конфигурации не являются эксклюзивными для bean-компонентов, которые создает Spring. Приложив небольшое усилие, вы можете использовать свойства конфигурации в ваших собственных bean-компонентах. Посмотрим как.

5.2 Создание ваших собственных свойств конфигурации

Как я упоминал ранее, свойства конфигурации - это не что иное, как свойства bean-компонентов, предназначенных для получения конфигураций из абстракции среды Spring. Что я не упомянул, так это то, как эти компоненты предназначены для использования этих конфигураций.

Для поддержки внедрения свойств конфигурации Spring Boot предоставляет аннотацию @ConfigurationProperties. При указании аннотации в любом Spring bean-компонен туказывается, что свойства этого bean-компонента могут быть внедрены из свойств среды Spring.

Чтобы продемонстрировать, как работает @ConfigurationProperties, предположим, что вы добавили следующий метод в OrderController, чтобы вывести список прошлых заказов аутентифицированного пользователя:

@GetMapping

public String ordersForUser(

@AuthenticationPrincipal User user, Model model) {

model.addAttribute("orders",

orderRepo.findByUserOrderByPlacedAtDesc(user));

return "orderList";

}

Наряду с этим вы также добавили необходимый метод findByUser() в OrderRepository:

List findByUserOrderByPlacedAtDesc(User user);

Обратите внимание, что этот метод хранилища (репозитория) имеет в названии OrderByPlacedAtDesc. Часть OrderBy указывает свойство, по которому будут упорядочены результаты - в данном случае, свойство placeAt. Desc в конце заставляет упорядочение быть в порядке убывания. Таким образом, список возвращаемых заказов будет отсортирован от самых последних до наименее последних.

Этот метод контроллера может быть полезен после того, как пользователь разместил несколько заказов. Но это может стать немного громоздким для самых заядлых ценителей тако. Несколько заказов, отображаемых в браузере, полезны; бесконечный список из сотен заказов - это просто шум. Допустим, вы хотите ограничить количество отображаемых заказов самыми последними 20 заказами. Вы можете изменить ordersForUser()

@GetMapping

public String ordersForUser(

@AuthenticationPrincipal User user, Model model) {

Pageable pageable = PageRequest.of(0, 20);

model.addAttribute("orders",

orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));

return "orderList";

}

вместе с соответствующими изменениями в OrderRepository:

List findByUserOrderByPlacedAtDesc(User user, Pageable pageable);

Вы изменили сигнатуру метода findByUserOrderByPlacedAtDesc(), чтобы принимать на вход Pageable в качестве параметра. Pageable - это способ Spring Data выбрать некоторое подмножество результатов по номеру страницы и размеру страницы. В методе контроллера ordersForUser() вы создали объект PageRequest, который реализовал Pageable для запроса первой страницы (нулевой страницы) с размером страницы 20, чтобы получить до 20 самых последних размещенных заказов для пользователя.

Хотя это работает фантастически, мне немного неловко, что вы жестко закодировали размер страницы. Что, если позже вы решите, что 20 - это слишком много заказов, и вы решите изменить его на 10? Поскольку он жестко запрограммирован, вам придется пересобрать и повторно развернуть приложение.

Вместо того, чтобы жестко задавать размер страницы, вы можете установить его с помощью пользовательского свойства конфигурации. Сначала вам нужно добавить новое свойство с именем pageSize в OrderController, а затем аннотировать OrderController с помощью @ConfigurationProperties, как показано в следующем листинге.

Листинг 5.1 Включение свойств конфигурации в OrderController

@Controller

@RequestMapping("/orders")

@SessionAttributes("order")

@ConfigurationProperties(prefix="taco.orders")

public class OrderController {

private int pageSize = 20;

public void setPageSize(int pageSize) {

this.pageSize = pageSize;

}

...

@GetMapping

public String ordersForUser(

@AuthenticationPrincipal User user, Model model) {

Pageable pageable = PageRequest.of(0, pageSize);

model.addAttribute("orders",

orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));

return "orderList";

}

}

Наиболее значительным изменением, внесенным в листинг 5.1, является добавление аннотации @ConfigurationProperties. Его префиксный атрибут имеет значение taco.orders, что означает, что при установке свойства pageSize необходимо использовать свойство конфигурации с именем taco.orders.pageSize.

Новое свойство pageSize по умолчанию равно 20. Но вы можете легко изменить его на любое желаемое значение, установив свойство taco.orders.pageSize. Например, вы можете установить это свойство в application.yml следующим образом:

taco:

orders:

pageSize: 10

Или, если вам нужно сделать быстрые изменения во время работы, вы можете сделать это без необходимости rebuild и redeploy приложения, задав свойство taco.orders.pageSize в качестве переменной среды:

$ export TACO_ORDERS_PAGESIZE=10

Любое средство, с помощью которого можно установить свойство конфигурации, можно использовать для настройки размера страницы последних заказов. Далее мы рассмотрим, как устанавливать данные конфигурации в хранилищах свойств (property holders).

5.2.1 Определение хранилища свойств конфигурации (configuration properties holders)

Ничто не указывает на то, что @ConfigurationProperties должен быть установлен на контроллере или любом другом конкретном компоненте. @ConfigurationProperties на самом деле часто размещаются на bean-компонентах, единственная цель которых в приложении - хранить данные конфигурации. Это исключает детали конфигурации из контроллеров и других классов приложений. Это также упрощает совместное использование общих свойств конфигурации несколькими компонентами, которые могут использовать эту информацию.

В случае свойства pageSize в OrderController вы можете перенести его в отдельный класс. Следующий листинг демонстрирует такой класс OrderProps.

Листинг 5.2. Извлечение pageSize в класс хранилища свойств

package tacos.web;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

import lombok.Data;

@Component

@ConfigurationProperties(prefix="taco.orders")

@Data

public class OrderProps {

private int pageSize = 20;

}

Как и в случае с OrderController, для свойства pageSize по умолчанию установлено значение 20, а для OrderProps добавлен @ConfigurationProperties с префиксом taco.orders. Он также помечен @Component, так что сканирование компонентов Spring автоматически обнаружит его и создаст как компонент в контексте приложения Spring. Это важно, так как следующим шагом является внедрение bean-компонента OrderProps в OrderController.

В хранилище конфигурации нет ничего особенного. Это bean, чьи свойства поступают из среды Spring. Они могут быть введены в любой другой компонент, которому нужны эти свойства. Для OrderController это означает удаление свойства pageSize из OrderController и вместо этого внедрение и использование компонента OrderProps:

@Controller

@RequestMapping("/orders")

@SessionAttributes("order")

public class OrderController {

private OrderRepository orderRepo;

private OrderProps props;

public OrderController(OrderRepository orderRepo,

OrderProps props) {

this.orderRepo = orderRepo;

this.props = props;

}

...

@GetMapping

public String ordersForUser(

@AuthenticationPrincipal User user, Model model) {

Pageable pageable = PageRequest.of(0, props.getPageSize());

model.addAttribute("orders",

orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));

return "orderList";

}

...

}

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

Например, давайте представим, что вы используете свойство pageSize в нескольких других bean-компонентах, когда вдруг решили, что было бы лучше применить некоторую проверку к этому свойству, чтобы ограничить его значения не менее чем 5 и не более 25. Без отдельного bean-компонента вам нужно будет применить аннотации проверки к OrderController, свойству pageSize и во всех других классах, использующих это свойство. Но поскольку вы создали pageSize в OrderProps, вам нужно только внести изменения в OrderProps:

package tacos.web;

import javax.validation.constraints.Max;

import javax.validation.constraints.Min;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

import org.springframework.validation.annotation.Validated;

import lombok.Data;

@Component

@ConfigurationProperties(prefix="taco.orders")

@Data

@Validated

public class OrderProps {

@Min(value=5, message="must be between 5 and 25")

@Max(value=25, message="must be between 5 and 25")

private int pageSize = 20;

Загрузка...