И снова день добрый. Пост в продолжение публикации «Spring + Java EE + Persistence, без XML. Часть 1».
Если вы хотите с этой части начать, либо не осталось проекта сделанного в предыдущей части, можете скачать его с github.
Схема простая:
В этой части мы рассмотрим как хранятся отношения многие-ко-многим на уровне объектов сущностей;
доделаем распределения прав пользователям;
сделаем простейший REST-controller;
сделаем регистрацию новых пользователей (только для админа);
и все это без XML.
Как кто-то наверняка заметил, на данный момент у нас вместо получения прав пользователей из базы приделана жутковатого вида заглушка, совершенно не гибкая причем.
Что же нужно чтобы эта исправить? Ввести такую сущность как «роль». У одного юзера может быть несколько ролей, причем множество пользователей может иметь одну и ту же роль. Т.е. классическое Many-To-Many.
Для начала заведем табличку roles (id, role), не забыв указать что значения в role должны быть уникальными. Также создадим вспомогательную таблицу users_roles(user_id, role_id). И сразу же создадим базовые роли ADMIN, USER, GUEST. Ну и сразу создадим для связывающей страницы внешние ключи user_id -> user.id, role_id -> role.id. Все это можно сделать сразу, выполнив вот такой скрипт:
Сначала зайдем Application.class и уточним расположение Jpa репозиториев:
Теперь создадим в entities/ класс Role замапленный на запись в базе:
Ок, а как теперь свзать их с классом User? Для этого нужно всего лишь добавить в User вот такой код:
Здесь мы указываем таблицу связи двух сущностей, какая колонка в этой таблице соответствует нашей сущности( joinColumns = { @JoinColumn(name = «user_id»)}), а какая — связываемой сущности: inverseJoinColumns = @JoinColumn(name = «role_id»).
В классе Role все проще:
Чтобы Spring устроила наша роль как Authorithy, надо в классе Role реализовать интерфейс GrantedAuthority:
Готово! Теперь мы можем переписать MySQLUserDetailsService:
Теперь мы грузим authorities через user.getRoles(), а не мусорный класс, так что пользователь получит только те роли, которые присвоены ему в базе.
На данный момент мы это не используем, но чуть позже вы увидите, как можно разграничивать доступ в зависимости от роли пользователя.
Итак, создадим простенький rest controller для работы с пользователями доступный только для пользователей с правами админа.
Для начала создадим папку controllers, а в ней — UsersContoller:
Итак, во первых мы отметили что это контроллер rest, что значит что возвращать он будет не полноценные html странички, а сырые данные, по умолчанию в формате Json. @RequestMapping("/users") — это означает, что срабатывать он будет на зпрос от пользователя вида «yoursite/users». Но мы тут собираемся пользователями рулить, в то время как открыть этот контроллер может любой авторизованный пользоватль! Так что добавляем волшебную строчку:
Внутри контроллера проверка прав теперь вообще не нужна, туда попасть сможет только тот у кого уже роль ADMIN имеется.
Теперь добавим вывод всех пользователей:
Попробуйте перейти по адресу http://localhost:8080/users если все сделали правильно, будет ошибка 403. А теперь добавьте пользователю админские права, для этого надо добавить в users_roles запись (1,1), если у вас такие же id юзера и роли ADMIN как у меня. После добавления нового значения в таблицу идем в http://localhost:8080/secret и жмем там logout чтобы перезайти заново(для подгрузки новых прав). Теперь пробуем открыть http://localhost:8080/users. Должно вывести что-то такое:
В чем же дело, у на ведь всего один пользователь? Тут все просто — в автоматическом выводе объекта как json выводятся и все его поля, в итоге если в одном из них есть он же, это превращается в бесконечный цикл. Чтобы поправить досадную оплошность, добавим полю users в классе Role аннотацию JsonIgnore:
Перезапускаем приложение, перезаходим в http://localhost:8080/users и видим нормальный вывод:
Ок, теперь добавим еще парочку методов и «user-friendly» интерфейс для создания новых пользователей.
Для этого сначала реализуем метод который по POST запросу добавит новую сущность:
Уже неплохо, из стороннего api теперь можно пост запросом юзеров добавлять, а как в самом нашем приложении это делать?
Для этого создадим вложенный маршрут /add, который будет отрабатывать по запросу GET /users/add:
И в resources/templates/ добавим add.html:
Поля формы будут напрямую конвертироваться в параметры функции создания пользователя с таким же названием, крайне удобно, по-моему.
Напрямую вывести шаблон в @RestController мы не можем, так что используем для этого вспомогательный класс ModelAndView в который достаточно передать название view (без.html).
Готово, теперь можно напрямую на сайте создавать новых пользователей с помощью rest либо с помощью формы в /users/add.
Осталось добавить два простейших метода которые выдают/удаляют конкретного пользователя(запрос типа GET/DELETE /users/2):
Данные методы по-моему самодокументируемы. Аннотация PathVariable(«значение») вытаскивает из запроса то, что будет в нем вместо шаблона {значение}(в нашем случае — цифра).
Ветка https://github.com/MaxPovver/ForHabrahabr/tree/withcontroller содержит все нужное, только сначала надо будет запустить import_me.sql в вашей БД.(после скачивания/клонирования не забудьте сделать checkout)
Хотелось уместить в этой статье сильно больше, на она по-моему и так уже слегка перегружена, а я еще даже до половины не дошел. Так что тестирование, OneToMany и еще несколько интересных вещей придется оставить на следующую статью, если, конечно, будет интерес к теме.
Удачи!
Содержание
1. Введение
1.1 Подгружаем проект
1.2 Что мы будем делать в этой части?
2. Фиксим распределение ролей между пользователями
2.1 Работа с базой
2.2 Пишем код
3. Создаем контроллер UsersController
3.1 Реализуем создание нового пользователя
3.2 Добавляем работу с конкретным пользователем
4. Для желающих запустить готовый проект
5 Заключение
1. Введение
1.1 Подгружаем проект
Если вы хотите с этой части начать, либо не осталось проекта сделанного в предыдущей части, можете скачать его с github.
Схема простая:
- Заходите из консоли в папку с проектами IDEA
- git clone github.com/MaxPovver/ForHabrahabr.git
- cd ForHabrahabr/
- git checkout withauth
- Готово, теперь можете грузить проект в студию так же как описано в первой части.
1.2 Что мы будем делать в этой части?
В этой части мы рассмотрим как хранятся отношения многие-ко-многим на уровне объектов сущностей;
доделаем распределения прав пользователям;
сделаем простейший REST-controller;
сделаем регистрацию новых пользователей (только для админа);
и все это без XML.
2 Фиксим распределение ролей между пользователями
Как кто-то наверняка заметил, на данный момент у нас вместо получения прав пользователей из базы приделана жутковатого вида заглушка, совершенно не гибкая причем.
Что же нужно чтобы эта исправить? Ввести такую сущность как «роль». У одного юзера может быть несколько ролей, причем множество пользователей может иметь одну и ту же роль. Т.е. классическое Many-To-Many.
2.1 Работа с базой
Для начала заведем табличку roles (id, role), не забыв указать что значения в role должны быть уникальными. Также создадим вспомогательную таблицу users_roles(user_id, role_id). И сразу же создадим базовые роли ADMIN, USER, GUEST. Ну и сразу создадим для связывающей страницы внешние ключи user_id -> user.id, role_id -> role.id. Все это можно сделать сразу, выполнив вот такой скрипт:
Просто запустить
# Дамп таблицы roles
# ------------------------------------------------------------
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`role` varchar(250) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
UNIQUE KEY `role` (`role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `roles` (`id`, `role`)
VALUES
(1,'ADMIN'),
(3,'GUEST'),
(2,'USER');
# Дамп таблицы users
# ------------------------------------------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(250) DEFAULT NULL,
`password` varchar(250) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `users` (`id`, `username`, `password`)
VALUES
(1,'user','user');
# Дамп таблицы users_roles
# ------------------------------------------------------------
DROP TABLE IF EXISTS `users_roles`;
CREATE TABLE `users_roles` (
`user_id` bigint(20) unsigned DEFAULT NULL,
`role_id` bigint(20) unsigned DEFAULT NULL,
KEY `hasuser` (`user_id`),
KEY `hasrole` (`role_id`),
CONSTRAINT `hasrole` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `hasuser` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 Пишем код
Сначала зайдем Application.class и уточним расположение Jpa репозиториев:
@EnableJpaRepositories(basePackages = {"habraspring.repositories"})
Теперь создадим в entities/ класс Role замапленный на запись в базе:
Role.java
@Entity
@Table(name="roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String role;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
protected Role(){}
public Role(String name)
{
role = name;
}
}
Ок, а как теперь свзать их с классом User? Для этого нужно всего лишь добавить в User вот такой код:
@ManyToMany
@JoinTable(name = "users_roles",
joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
Здесь мы указываем таблицу связи двух сущностей, какая колонка в этой таблице соответствует нашей сущности( joinColumns = { @JoinColumn(name = «user_id»)}), а какая — связываемой сущности: inverseJoinColumns = @JoinColumn(name = «role_id»).
В классе Role все проще:
@ManyToMany(mappedBy = "roles")
Set<User> users;
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
Чтобы Spring устроила наша роль как Authorithy, надо в классе Role реализовать интерфейс GrantedAuthority:
public class Role implements GrantedAuthority {
...
@Override
public String getAuthority() {
return getRole();
}
}
Готово! Теперь мы можем переписать MySQLUserDetailsService:
@Service
public class MySQLUserDetailsService implements UserDetailsService {
@Autowired
UsersRepository users;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails loadedUser;
try {
User client = users.findByUsername(username);
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
client.getRoles());
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
return loadedUser;
}
}
Теперь мы грузим authorities через user.getRoles(), а не мусорный класс, так что пользователь получит только те роли, которые присвоены ему в базе.
На данный момент мы это не используем, но чуть позже вы увидите, как можно разграничивать доступ в зависимости от роли пользователя.
Итак, создадим простенький rest controller для работы с пользователями доступный только для пользователей с правами админа.
3. Создаем контроллер UsersController
Для начала создадим папку controllers, а в ней — UsersContoller:
package habraspring.controllers;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class UsersController {
}
Итак, во первых мы отметили что это контроллер rest, что значит что возвращать он будет не полноценные html странички, а сырые данные, по умолчанию в формате Json. @RequestMapping("/users") — это означает, что срабатывать он будет на зпрос от пользователя вида «yoursite/users». Но мы тут собираемся пользователями рулить, в то время как открыть этот контроллер может любой авторизованный пользоватль! Так что добавляем волшебную строчку:
@PreAuthorize("hasRole('ADMIN')")
Внутри контроллера проверка прав теперь вообще не нужна, туда попасть сможет только тот у кого уже роль ADMIN имеется.
Теперь добавим вывод всех пользователей:
@Autowired
UsersRepository users;
@RequestMapping(method = RequestMethod.GET)
public List<User> getUsers()
{
List<User> result = new ArrayList<>();
users.findAll().forEach(result::add);
return result;
}
Попробуйте перейти по адресу http://localhost:8080/users если все сделали правильно, будет ошибка 403. А теперь добавьте пользователю админские права, для этого надо добавить в users_roles запись (1,1), если у вас такие же id юзера и роли ADMIN как у меня. После добавления нового значения в таблицу идем в http://localhost:8080/secret и жмем там logout чтобы перезайти заново(для подгрузки новых прав). Теперь пробуем открыть http://localhost:8080/users. Должно вывести что-то такое:
Много букв
[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:[{«id»:1,«username»:«user»,«password»:«user»,«roles»:[{«id»:1,«role»:«ADMIN»,«users»:
В чем же дело, у на ведь всего один пользователь? Тут все просто — в автоматическом выводе объекта как json выводятся и все его поля, в итоге если в одном из них есть он же, это превращается в бесконечный цикл. Чтобы поправить досадную оплошность, добавим полю users в классе Role аннотацию JsonIgnore:
import com.fasterxml.jackson.annotation.JsonIgnore;
...
@JsonIgnore
@ManyToMany(mappedBy = "roles")
Set<User> users;
Перезапускаем приложение, перезаходим в http://localhost:8080/users и видим нормальный вывод:
[{"id":1,"username":"user","password":"user","roles":[{"id":1,"role":"ADMIN","authority":"ADMIN"}]}]
Ок, теперь добавим еще парочку методов и «user-friendly» интерфейс для создания новых пользователей.
3.1 Реализуем создание нового пользователя
Для этого сначала реализуем метод который по POST запросу добавит новую сущность:
@RequestMapping(method = RequestMethod.POST)
public User addUser(String username, String password, String password_confirm)
{
//no empty fields allowed
if (username.isEmpty() || password.isEmpty() || password_confirm.isEmpty())
return null;
//passwords should match
if (!password.equals(password_confirm))
return null;
return users.save(new User(username, password));
}
Уже неплохо, из стороннего api теперь можно пост запросом юзеров добавлять, а как в самом нашем приложении это делать?
Для этого создадим вложенный маршрут /add, который будет отрабатывать по запросу GET /users/add:
@RequestMapping(value = "/add",method = RequestMethod.GET)
public ModelAndView getUserForm()
{
return new ModelAndView("add");
}
И в resources/templates/ добавим add.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Add user page</title>
</head>
<body>
<form th:action="@{/users}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><label> Password confirm: <input type="password" name="password_confirm"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
Поля формы будут напрямую конвертироваться в параметры функции создания пользователя с таким же названием, крайне удобно, по-моему.
Напрямую вывести шаблон в @RestController мы не можем, так что используем для этого вспомогательный класс ModelAndView в который достаточно передать название view (без.html).
Готово, теперь можно напрямую на сайте создавать новых пользователей с помощью rest либо с помощью формы в /users/add.
3.2 Добавляем работу с конкретным пользователем
Осталось добавить два простейших метода которые выдают/удаляют конкретного пользователя(запрос типа GET/DELETE /users/2):
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public void delete(@PathVariable("id") Long id)
{
users.delete(id);
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User getUser(@PathVariable("id") Long id)
{
return users.findOne(id);
}
Данные методы по-моему самодокументируемы. Аннотация PathVariable(«значение») вытаскивает из запроса то, что будет в нем вместо шаблона {значение}(в нашем случае — цифра).
4. Для желающих запустить готовый проект
Ветка https://github.com/MaxPovver/ForHabrahabr/tree/withcontroller содержит все нужное, только сначала надо будет запустить import_me.sql в вашей БД.(после скачивания/клонирования не забудьте сделать checkout)
5. Заключение
Хотелось уместить в этой статье сильно больше, на она по-моему и так уже слегка перегружена, а я еще даже до половины не дошел. Так что тестирование, OneToMany и еще несколько интересных вещей придется оставить на следующую статью, если, конечно, будет интерес к теме.
Удачи!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (5)
Apx
14.07.2015 10:57Было бы гораздо лучше если бы в этом цикле статей показать как сварить спринг приложение самому с нуля, а не поднимать spring boot… У новичков все равно останутся вопросы «как оно работает». Да и бут в продакшн кидать не лучшее решение…
MaximChistov Автор
14.07.2015 11:20Было бы гораздо лучше если бы в этом цикле статей показать как сварить спринг приложение самому с нуля, а не поднимать spring boot
Ну так вперед, пишите!
cyberorg
27.07.2015 19:16Будет здорово, если во все статьи этого цикла в начале каждой статьи добавить оглавление.
Throwable
Хорошо. А где собственно здесь JavaEE? Это обычное Spring-приложение, которое даже не деплоится на JEE контейнер. На сегодняшний день JEE уже умеет многое из того, что предлагает Spring, поэтому использование обеих технологий в одном приложении — вопрос целесообразности.
MaximChistov Автор
Частично оно там используется, но ок, убрал