И снова день добрый.
Пост в продолжение Spring + Java EE + Persistence, без XML. Часть 1
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 замапленный на запись в базе:
@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. Должно вывести что-то такое:
В чем же дело, у на ведь всего один пользователь? Тут все просто — в автоматическом выводе объекта как 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 и еще несколько интересных вещей придется оставить на следующую статью, если, конечно, будет интерес к теме.
Удачи!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
ссылка на оригинал статьи http://habrahabr.ru/post/262361/
Добавить комментарий