Spring + Java EE + Persistence, без XML. Часть 2

от автора

И снова день добрый.
Пост в продолжение 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 замапленный на запись в базе:

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 и еще несколько интересных вещей придется оставить на следующую статью, если, конечно, будет интерес к теме.

Удачи!

Статья

Никто ещё не голосовал. Воздержался 1 человек.

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

ссылка на оригинал статьи http://habrahabr.ru/post/262361/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *