В предыдущем сеансе приложение динамически отвечало на обновления от других пользователей с помощью встроенных обработчиков событий Spring Data REST и поддержки WebSocket в Spring Framework. Но ни одно приложение не будет полным без обеспечения всей безопасности, чтобы только надлежащие пользователи имели доступ к пользовательскому интерфейсу и ресурсам, стоящим за ним.
Не стесняйтесь взять код из этого хранилища и следуйте инструкциям. Этот сеанс основан на приложении предыдущего сеанса с добавлением дополнительных вещей.
Добавление Spring Security в проект
Прежде чем начать, вам нужно добавить пару зависимостей в файл pom.xml вашего проекта:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
Это включает Spring Boot Spring Security Starter, а также некоторые дополнительные теги Thymeleaf для поиска безопасности на веб-странице.
Определение модели безопасности
На прошлой сессии вы работали с хорошей системой начисления заработной платы. Удобно объявить что-то на бэкэнде и позволить Spring Data REST сделать тяжелую работу. Следующим шагом является моделирование системы, в которой необходимо установить меры безопасности.
Если это система начисления заработной платы, то только менеджеры будут иметь к ней доступ. Итак, начните с моделирования Manager
объекта:
@Data
@ToString(exclude = "password")
@Entity
public class Manager {
public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
private @Id @GeneratedValue Long id;
private String name;
private @JsonIgnore String password;
private String[] roles;
public void setPassword(String password) {
this.password = PASSWORD_ENCODER.encode(password);
}
protected Manager() {}
public Manager(String name, String password, String... roles) {
this.name = name;
this.setPassword(password);
this.roles = roles;
}
}
PASSWORD_ENCODER
это средство для шифрования новых паролей или для ввода введенных паролей и их шифрования перед сравнением.id
,name
,password
Иroles
определить параметры , необходимые для ограничения доступа.- Индивидуальный
setPassword()
гарантирует, что пароли никогда не хранятся в открытом виде.
При разработке уровня безопасности необходимо помнить о ключевых моментах. Защитите правильные биты данных (например, пароли) и НЕ позволяйте им печататься на консоли, в журналах или экспортироваться через сериализацию JSON.
@ToString(exclude = "password")
гарантирует, что сгенерированный Lombok метод toString () НЕ распечатает пароль.@JsonIgnore
применяется к полю пароля защищает от сериализации этого поля Джексоном.
Создание репозитория менеджера
Spring Data настолько хорош в управлении сущностями. Почему бы не создать хранилище для этих менеджеров?
@RepositoryRestResource(exported = false)
public interface ManagerRepository extends Repository<Manager, Long> {
Manager save(Manager manager);
Manager findByName(String name);
}
Вместо расширения привычного CrudRepository
вам не нужно так много методов. Вместо этого вам нужно сохранить данные (которые также используются для обновлений), и вам нужно искать существующих пользователей. Следовательно, вы можете использовать минимальный Repository
интерфейс маркера Spring Data Common . Он поставляется без предопределенных операций.
Spring Data REST по умолчанию экспортирует любой найденный репозиторий. Вы НЕ хотите, чтобы этот репозиторий был открыт для операций REST! Примените @RepositoryRestResource(exported = false)
аннотацию, чтобы заблокировать ее экспорт. Это предотвращает обслуживание хранилища, а также любые метаданные.
Связь сотрудников с их менеджерами
Последний аспект моделирования безопасности — связать сотрудников с менеджером. В этом домене сотрудник может иметь одного менеджера, а менеджер может иметь несколько сотрудников:
@Data
@Entity
public class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;
private @Version @JsonIgnore Long version;
private @ManyToOne Manager manager;
private Employee() {}
public Employee(String firstName, String lastName, String description, Manager manager) {
this.firstName = firstName;
this.lastName = lastName;
this.description = description;
this.manager = manager;
}
}
- Атрибут менеджера связан через JPA
@ManyToOne
. Менеджер не нуждается в том,@OneToMany
потому что вы не определили необходимость искать это. - Вызов конструктора утилит обновлен для поддержки инициализации.
Обеспечение сотрудников их менеджерам
Spring Security поддерживает множество опций при определении политик безопасности. В этом сеансе вы хотите ограничить такие вещи, чтобы ТОЛЬКО менеджеры могли просматривать данные заработной платы сотрудников, а операции сохранения, обновления и удаления ограничивались менеджером сотрудника. Другими словами, любой менеджер может войти в систему и просмотреть данные, но только менеджер данного сотрудника может вносить любые изменения.
@PreAuthorize("hasRole('ROLE_MANAGER')")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
@Override
@PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name")
Employee save(@Param("employee") Employee employee);
@Override
@PreAuthorize("@employeeRepository.findOne(#id)?.manager?.name == authentication?.name")
void delete(@Param("id") Long id);
@Override
@PreAuthorize("#employee?.manager?.name == authentication?.name")
void delete(@Param("employee") Employee employee);
}
@PreAuthorize
в верхней части интерфейса доступ ограничен людьми с ROLE_MANAGER .
Вкл. save()
, Либо менеджер сотрудника имеет значение null (первоначальное создание нового сотрудника, когда менеджер не был назначен), либо имя менеджера сотрудника совпадает с именем аутентифицированного в настоящее время пользователя. Здесь вы используете SpEL-выражения Spring Security для определения доступа. Это идет с удобным «?.» свойство навигатор для обработки нулевых проверок. Также важно отметить использование @Param(…)
аргументов on для связи HTTP-операций с методами.
Если delete()
метод либо имеет доступ к сотруднику, либо в случае, если у него есть только идентификатор, он должен найти employeeRepository в контексте приложения, выполнить a findOne(id)
, а затем проверить менеджера в отношении текущего пользователя, прошедшего проверку подлинности.
Написание UserDetails
службы
Общей точкой интеграции с безопасностью является определение UserDetailsService
. Это способ подключения хранилища данных вашего пользователя к интерфейсу Spring Security. Spring Security нужен способ поиска пользователей для проверки безопасности, и это мост. К счастью с Spring Data, усилия минимальны:
@Component
public class SpringDataJpaUserDetailsService implements UserDetailsService {
private final ManagerRepository repository;
@Autowired
public SpringDataJpaUserDetailsService(ManagerRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Manager manager = this.repository.findByName(name);
return new User(manager.getName(), manager.getPassword(),
AuthorityUtils.createAuthorityList(manager.getRoles()));
}
}
SpringDataJpaUserDetailsService
реализует Spring Security UserDetailsService
. Интерфейс имеет один метод: loadByUsername()
. Этот метод предназначен для возврата UserDetails
объекта, чтобы Spring Security могла запрашивать информацию пользователя.
Поскольку у вас есть ManagerRepository
, нет необходимости писать какие-либо выражения SQL или JPA для извлечения этих необходимых данных. В этом классе он автоматически подключается с помощью инжектора конструктора.
loadByUsername()
подключается к пользовательскому поисковику, который вы написали минуту назад findByName()
. Затем он заполняет User
экземпляр Spring Security , который реализует UserDetails
интерфейс. Вы также используете Spring Securiy это AuthorityUtils
для перехода от множества ролей на основе строки в Java List
из GrantedAuthority
.
Подключение вашей политики безопасности
В @PreAuthorize
выражении , применяемое в ваше хранилище являются правилами доступа . Эти правила ни для чего без политики безопасности.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private SpringDataJpaUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(Manager.PASSWORD_ENCODER);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/bower_components/**", "/*.js",
"/*.jsx", "/main.css").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/", true)
.permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.logout()
.logoutSuccessUrl("/");
}
}
Этот код очень сложен, поэтому давайте пройдемся по нему, сначала поговорим о аннотациях и API. Затем мы обсудим политику безопасности, которую он определяет.
@EnableWebSecurity
сообщает Spring Boot об удалении автоматически настроенной политики безопасности и использовании этой. Для быстрых демонстраций с автоматически настроенной безопасностью все в порядке. Но для чего-то реального, вы должны написать политику самостоятельно.@EnableGlobalMethodSecurity
включает защиту на уровне методов с помощью сложных аннотаций @Pre и @Post в Spring Security .- Это расширяет
WebSecsurityConfigurerAdapter
, удобный базовый класс для написания политики. - Он автоматически подключил
SpringDataJpaUserDetailsService
поле ввода, а затем подключил его черезconfigure(AuthenticationManagerBuilder)
метод.PASSWORD_ENCODER
ОтManager
также установки. - Основная политика безопасности написана на чистом Java с
configure(HttpSecurity)
.
Политика безопасности требует авторизовать все запросы, используя правила доступа, определенные ранее.
- Пути, перечисленные в,
antMatchers()
получают безусловный доступ, поскольку нет никаких причин блокировать статические веб-ресурсы. - Все, что не соответствует, что входит в
anyRequest().authenticated()
смысл, требует аутентификации. - После настройки этих правил доступа Spring Security приказывает использовать аутентификацию на основе форм, по умолчанию «/» в случае успеха, и предоставлять доступ к странице входа в систему.
- Базовый логин также настроен с отключенной CSRF. Это в основном для демонстраций и не рекомендуется для производственных систем без тщательного анализа.
- Выход из системы настроен на переход пользователя в «/».
Предупреждение | Базовая аутентификация удобна, когда вы экспериментируете с curl. Использование curl для доступа к системе на основе форм просто утомительно. Важно понимать, что аутентификация с использованием любого механизма через HTTP (не HTTPS) подвергает вас риску прослушивания учетных данных по проводам. CSRF — хороший протокол, чтобы оставить его без изменений. Он просто отключен, чтобы упростить взаимодействие с Бейсиком и скручиванием. В производстве лучше оставить его включенным. |
Автоматическое добавление сведений о безопасности
Хороший пользовательский опыт — это когда приложение может автоматически применять контекст. В этом примере, если вошедший в систему менеджер создает новую запись о сотруднике, имеет смысл для этого менеджера владеть ею. В обработчиках событий Spring Data REST пользователю не нужно явно связывать его. Это также гарантирует, что пользователь не случайно записывает неправильный менеджер.
@Component
@RepositoryEventHandler(Employee.class)
public class SpringDataRestEventHandler {
private final ManagerRepository managerRepository;
@Autowired
public SpringDataRestEventHandler(ManagerRepository managerRepository) {
this.managerRepository = managerRepository;
}
@HandleBeforeCreate
public void applyUserInformationUsingSecurityContext(Employee employee) {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
Manager manager = this.managerRepository.findByName(name);
if (manager == null) {
Manager newManager = new Manager();
newManager.setName(name);
newManager.setRoles(new String[]{"ROLE_MANAGER"});
manager = this.managerRepository.save(newManager);
}
employee.setManager(manager);
}
}
@RepositoryEventHandler(Employee.class)
помечает этот обработчик событий как применяемый только к Employee
объектам. @HandleBeforeCreate
Аннотаций дает вам шанс изменить поступающую Employee
запись , прежде чем она будет записана в базу данных.
В этом разделе вы просматриваете контекст безопасности текущего пользователя, чтобы получить имя пользователя. Затем найдите связанный менеджер с помощью findByName()
и примените его к менеджеру. Существует небольшой дополнительный код для создания нового менеджера, если он или она еще не существует в системе. Но это в основном для поддержки инициализации базы данных. В реальной производственной системе этот код должен быть удален, и вместо этого он должен зависеть от администраторов баз данных или службы безопасности, чтобы правильно поддерживать хранилище пользовательских данных.
Предварительная загрузка данных менеджера
Загрузка менеджеров и привязка сотрудников к этим менеджерам довольно проста:
@Component
public class DatabaseLoader implements CommandLineRunner {
private final EmployeeRepository employees;
private final ManagerRepository managers;
@Autowired
public DatabaseLoader(EmployeeRepository employeeRepository,
ManagerRepository managerRepository) {
this.employees = employeeRepository;
this.managers = managerRepository;
}
@Override
public void run(String... strings) throws Exception {
Manager greg = this.managers.save(new Manager("greg", "turnquist",
"ROLE_MANAGER"));
Manager oliver = this.managers.save(new Manager("oliver", "gierke",
"ROLE_MANAGER"));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("greg", "doesn't matter",
AuthorityUtils.createAuthorityList("ROLE_MANAGER")));
this.employees.save(new Employee("Frodo", "Baggins", "ring bearer", greg));
this.employees.save(new Employee("Bilbo", "Baggins", "burglar", greg));
this.employees.save(new Employee("Gandalf", "the Grey", "wizard", greg));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("oliver", "doesn't matter",
AuthorityUtils.createAuthorityList("ROLE_MANAGER")));
this.employees.save(new Employee("Samwise", "Gamgee", "gardener", oliver));
this.employees.save(new Employee("Merry", "Brandybuck", "pony rider", oliver));
this.employees.save(new Employee("Peregrin", "Took", "pipe smoker", oliver));
SecurityContextHolder.clearContext();
}
}
Единственный недостаток заключается в том, что Spring Security активен с правилами доступа в полную силу при запуске этого загрузчика. Таким образом, чтобы сохранить данные о сотрудниках, вы должны использовать setAuthentication()
API Spring Security для аутентификации этого загрузчика с правильным именем и ролью. В конце контекст безопасности очищается.
Туризм по вашей защищенной службе REST
Имея все эти моды на месте, вы можете запустить приложение ( ./mvnw spring-boot:run
) и проверить моды с помощью cURL.
$ curl -v -u greg:turnquist localhost:8080/api/employees/1
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'greg'
> GET /api/employees/1 HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Z3JlZzp0dXJucXVpc3Q=
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Set-Cookie: JSESSIONID=E27F929C1836CC5BABBEAB78A548DF8C; Path=/; HttpOnly
< ETag: "0"
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 25 Aug 2015 15:57:34 GMT
<
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"description" : "ring bearer",
"manager" : {
"name" : "greg",
"roles" : [ "ROLE_MANAGER" ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/employees/1"
}
}
}
Это показывает гораздо больше деталей, чем во время первой сессии. Прежде всего, Spring Security включает несколько протоколов HTTP для защиты от различных векторов атак (Pragma, Expires, X-Frame-Options и т. Д.). Вы также выдаете базовые учетные данные, с помощью -u greg:turnquist
которых отображается заголовок авторизации.
Среди всех заголовков вы можете увидеть заголовок ETag из вашего версионного ресурса.
Наконец, внутри самих данных вы можете увидеть новый атрибут: менеджер . Вы можете видеть, что он включает в себя имя и роли, но НЕ пароль. Это связано с использованием @JsonIgnore
в этом поле. Поскольку Spring Data REST не экспортировал этот репозиторий, его значения встроены в этот ресурс. Это будет полезно при обновлении пользовательского интерфейса в следующем разделе.
Отображение информации о менеджере в пользовательском интерфейсе
Со всеми этими модами в бэкэнде вы можете перейти к обновлению вещей в веб-интерфейсе. Прежде всего, покажите менеджера сотрудника внутри компонента `<Employee />` React:
var Employee = React.createClass({
handleDelete: function () {
this.props.onDelete(this.props.employee);
},
render: function () {
return (
<tr>
<td>{this.props.employee.entity.firstName}</td>
<td>{this.props.employee.entity.lastName}</td>
<td>{this.props.employee.entity.description}</td>
<td>{this.props.employee.entity.manager.name}</td>
<td>
<UpdateDialog employee={this.props.employee}
attributes={this.props.attributes}
onUpdate={this.props.onUpdate}/>
</td>
<td>
<button onClick={this.handleDelete}>Delete</button>
</td>
</tr>
)
}
});
Это просто добавляет столбец для this.props.employee.entity.manager.name
.
Фильтрация метаданных схемы JSON
Если в выводе данных отображается поле, можно предположить, что оно имеет запись в метаданных схемы JSON. Вы можете увидеть это в следующем отрывке:
{
...
"manager" : {
"readOnly" : false,
"$ref" : "#/descriptors/manager"
},
...
},
...
"$schema" : "http://json-schema.org/draft-04/schema#"
}
Поле менеджера — это не то, что вы хотите, чтобы люди редактировали напрямую. Поскольку он встроенный, его следует рассматривать как атрибут только для чтения. Чтобы отфильтровать встроенные записи из CreateDialog
и UpdatDialog
, просто удалите такие записи после извлечения метаданных схемы JSON.
/**
* Filter unneeded JSON Schema properties, like uri references and
* subtypes ($ref).
*/
Object.keys(schema.entity.properties).forEach(function (property) {
if (schema.entity.properties[property].hasOwnProperty('format') &&
schema.entity.properties[property].format === 'uri') {
delete schema.entity.properties[property];
}
if (schema.entity.properties[property].hasOwnProperty('$ref')) {
delete schema.entity.properties[property];
}
});
this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;
Этот код обрезает как отношения URI, так и записи $ ref.
Ловушка для несанкционированного доступа
С проверками безопасности, настроенными на бэкэнде, добавьте обработчик на случай, если кто-то попытается обновить запись без авторизации:
onUpdate: function (employee, updatedEmployee) {
client({
method: 'PUT',
path: employee.entity._links.self.href,
entity: updatedEmployee,
headers: {
'Content-Type': 'application/json',
'If-Match': employee.headers.Etag
}
}).done(response => {
/* Let the websocket handler update the state */
}, response => {
if (response.status.code === 403) {
alert('ACCESS DENIED: You are not authorized to update ' +
employee.entity._links.self.href);
}
if (response.status.code === 412) {
alert('DENIED: Unable to update ' + employee.entity._links.self.href +
'. Your copy is stale.');
}
});
},
У вас был код для отлова ошибки HTTP 412. Это перехватывает код состояния HTTP 403 и обеспечивает соответствующее предупреждение.
Сделайте то же самое для операций удаления:
onDelete: function (employee) {
client({method: 'DELETE', path: employee.entity._links.self.href}
).done(response => {/* let the websocket handle updating the UI */},
response => {
if (response.status.code === 403) {
alert('ACCESS DENIED: You are not authorized to delete ' +
employee.entity._links.self.href);
}
});
},
Это закодировано аналогично специальным сообщениям об ошибках.
Добавьте некоторые детали безопасности в пользовательский интерфейс
Последнее, что должно увенчать эту версию приложения, — показать, кто вошел в систему, а также кнопку выхода из системы.
<div>
Hello, <span th:text="${#authentication.name}">user</span>.
<form th:action="@{/logout}" method="post">
<input type="submit" value="Log Out"/>
</form>
</div>
Собираем все вместе
С этими изменениями во внешнем интерфейсе перезапустите приложение и перейдите по адресу http: // localhost: 8080 .
Вы сразу же будете перенаправлены на форму входа. Эта форма предоставляется Spring Security, хотя вы можете создать свою собственную, если хотите. Войдите как greg / turnquist.
Вы можете увидеть недавно добавленный столбец менеджера. Пройдите пару страниц, пока не найдете сотрудников, принадлежащих Оливеру .
Нажмите « Обновить» , внесите некоторые изменения и нажмите « Обновить» . Должно произойти сбой со следующим всплывающим окном:
Если вы попробуете удалить , это должно произойти сбой с аналогичным сообщением Создайте нового сотрудника, и он должен быть назначен вам.
Рассмотрение
В этой сессии:
- Вы определили модель менеджера и связали ее с сотрудником через отношение 1-ко-многим.
- Вы создали хранилище для менеджеров и сказали Spring Data REST не экспортировать.
- Вы написали набор правил доступа для репозитория empoyee, а также написали политику безопасности.
- Вы написали другой обработчик событий Spring Data REST, чтобы перехватить события создания до того, как они произойдут, чтобы их текущий пользователь мог быть назначен менеджером сотрудника.
- Вы обновили пользовательский интерфейс, чтобы показать менеджера сотрудника, а также отобразить всплывающие сообщения об ошибках при выполнении несанкционированных действий.
Вопросы?
Веб-страница стала довольно сложной. Но как насчет управления отношениями и встроенными данными? Диалоги создания / обновления не очень подходят для этого. Это может потребовать некоторых пользовательских письменных форм.
Менеджеры имеют доступ к данным сотрудников. Должны ли сотрудники иметь доступ? Если бы вы добавили больше деталей, таких как номера телефонов и адреса, как бы вы это смоделировали? Как бы вы предоставили сотрудникам доступ к системе, чтобы они могли обновлять эти конкретные поля? Есть ли еще элементы управления гипермедиа, которые было бы удобно разместить на странице? Надеюсь, вам понравился этот сериал.