Spring без XML. Часть 2

  • Tutorial
И снова день добрый. Пост в продолжение публикации «Spring + Java EE + Persistence, без XML. Часть 1».


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

Удачи!

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

Статья

  • +8
  • 15,9k
  • 6
Поделиться публикацией

Комментарии 6

    +3
    Хорошо. А где собственно здесь JavaEE? Это обычное Spring-приложение, которое даже не деплоится на JEE контейнер. На сегодняшний день JEE уже умеет многое из того, что предлагает Spring, поэтому использование обеих технологий в одном приложении — вопрос целесообразности.
      0
      Частично оно там используется, но ок, убрал
      0
      Было бы гораздо лучше если бы в этом цикле статей показать как сварить спринг приложение самому с нуля, а не поднимать spring boot… У новичков все равно останутся вопросы «как оно работает». Да и бут в продакшн кидать не лучшее решение…
        0
        Было бы гораздо лучше если бы в этом цикле статей показать как сварить спринг приложение самому с нуля, а не поднимать spring boot

        Ну так вперед, пишите!
          0
          Добрый день — а можно подробнее про бут в продакшене? В Австралии для одного из довольно популярных сервисов AusPost REST движок сделан на нем (отслеживание отправлений почтой Австралии) и вроде нормально работает на амазоне.
          0
          Будет здорово, если во все статьи этого цикла в начале каждой статьи добавить оглавление.

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

          Самое читаемое