Spring Secutiry: начало работы и первые шаги на практике

от автора

После того как мы в теории разобрали важные детали о Spring Security нужно учиться работать с ним на практике.

Создание приложения с Spring Secutity

Создавать наше приложение будем по аналогии с соц. сетями, где можно создать/редактировать/удалить свою страницу, а чужие страницы можно только смотреть. Только вместо фронтенда будем создавать Rest соединение с выдуманным фронтом.
Будем использовать Spring Boot в ходе разработки. Можно воспользоваться сайтом http://start.spring.io/ либо же в Intellij Idea Ultimate (важно чтобы была именно Ultimate версия, потому что она поддерживает Spring Boot).
В ходе создания приложения будем использовать данные параметры внутри Intellij Idea.
Важное примечание: код будет не Production Ready, потому что это обучающий материал.

Параметры создания нового проекта

Параметры создания нового проекта

А из основных зависимостей у нас будет: web, lombok, security, spring data jpa и postgresql driver

Но это не все зависимости, потому что для подключения JWT в недолгом будущем, нужны будут 3 определённые зависимости которые мы скоро подключим.

После создания проекта у нас должен появиться pom.xml файл с такими зависимостями (зависимости для JWT уже добавлены в самом начале). Я поясню что дают зависимости для JWT и стартер spring-boot-starter-security, остальные думаю не стоит разбирать поскольку они самые основные для любого Spring Boot проекта, ну, а lombok это вкусовщина и он может доставить проблем, но не в моём случае, потому что я его не буду гонять на сложные вещи.

 <dependencies>        <!-- JWT -->        <dependency>            <groupId>io.jsonwebtoken</groupId>            <artifactId>jjwt-api</artifactId>            <version>0.12.3</version>        </dependency>        <dependency>            <groupId>io.jsonwebtoken</groupId>            <artifactId>jjwt-impl</artifactId>            <version>0.12.3</version>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>io.jsonwebtoken</groupId>            <artifactId>jjwt-jackson</artifactId>            <version>0.12.3</version>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-jpa</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-security</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-webmvc</artifactId>        </dependency>        <dependency>            <groupId>org.postgresql</groupId>            <artifactId>postgresql</artifactId>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <optional>true</optional>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-jpa-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-security-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-webmvc-test</artifactId>            <scope>test</scope>        </dependency>    </dependencies>

Далее чтобы наш проект работал, нужно в application.properties и Docker-compose.yml прописать настройки для постгреса, вместо докера базу данных можно запустить например через DBeaver. Я буду писать их в докере и будет это выглядеть следующим образом:

services:  db:    #Образ из которого будет создаваться контейнер    image: postgres:latest        #Переменные окружения для базы данных    environment:      #Пароль      POSTGRES_PASSWORD: postgres      #Пользователь      POSTGRES_USER: postgres      #Название      POSTGRES_DB: security    ports:      #Порт на котором будет работать база данных      - "5432:5432"

Отлично, зависимости и Docker-compose.yml у нас есть, осталось только прописать настройки в application.properties

spring.application.name=SecurityCourse#Порт на котором будет работать приложениеserver.port=8080spring.datasource.driver-class-name=org.postgresql.Driver#Тут идёт подстановка параметров из Docker-compose.yml в application.propertiesspring.datasource.password=${POSTGRES_PASSWORD:postgres}spring.datasource.username=${POSTGRES_USER:postgres}spring.datasource.url=jdbc:postgresql://localhost:5432/${POSTGRES_DB:security}#ddl-auto=create-drop нужно чтобы при каждом запуске все таблицы пересоздавались зановоspring.jpa.hibernate.ddl-auto=create-drop

После того как мы прописали все необходимые параметры, можем со спокойной душой запускать Docker-compose.yml и затем и наше Spring Boot приложение!

Успешный запуск приложения

Успешный запуск приложения

Как мы можем видеть, приложение запустилось и в логах мы видим сгенерированый пароль. Важно отметить, что этот пароль временный и принадлежит пользователю «user», которого в ближайшем будущем мы переопределим.
Поскольку наше приложение, можем посмотреть что будет если попробовать зайти на наш сайт http://localhost:8080/
При первом заходе нас сразу редиректит по пути http://localhost:8080/login

Страница авторизации

Страница авторизации

Далее можем ввести в поле username — user, а в поле password — <то что мы увидели в логах консоли выше>

После успешного входа нас перекинет на страничку с ошибкой, в целом это правильное поведение, потому что у нас нет никаких контроллеров, сервисов и тд. В некоторых случаях в коде ошибки можно увидеть код 999, он указывает на блокировку со стороны встроенной системы безопасности, которая заменяет коды ошибок: 401, 403 и 404.

Страница с ошибкой

Страница с ошибкой

Теперь из интереса можно перейти на страницу http://localhost:8080/logout

Страница выхода из профиля

Страница выхода из профиля

После того как мы нажмём на кнопку Log Out , нас перекинет на страницу авторизации и в параметрах url-запроса мы сможем увидеть параметр login?logout. Знаю, много тафталогии, но что поделать?

Страничка авторизации с успешным выходом из профиля

Страничка авторизации с успешным выходом из профиля

Если в поля логина и пароля ввести какую-нибудь белеберду, то наше приложение покажет нам ошибку, что мы введи неправильные данные, и в параметре запроса можно увидеть: ?error

Ошибка ввода данных в поля логина и пароля

Ошибка ввода данных в поля логина и пароля

UserDetails, UserDetailsService и всё с ними связанное

Теперь когда мы поигрались с базовыми возможностями Spring Security можно поговорить про UserDetails и UserDetailsService. Они является своего рода фундаментом который определяет кто такой пользователь в системе. Всё остальное строится поверх этого фундамента. Очень важно понимать их до мелочей, поскольку это та же сущность, которую мы будем защищать в дальнейшем.

  • UserDetails — это интерфейс Spring Security, который описывает «как выглядит пользователь с точки зрения фреймворка». Потому что сам фреймворк ничего от слова совсем не знает про таблицу users (которая будет ниже) — он умеет работать только с этим интерфейсом. Поэтому нужно будет создать сущность User которая будет реализовывать данный интерфейс.

  • UserDetailsService — это сервис который Spring Security вызывает во время аутентификации, чтобы загрузить пользователя по имени. Он является единственной точкой входа, через которую фреймворк узнаёт о существовании наших пользователей. Без этой точки входа аутентификация не будет знать, откуда брать данные пользователя.

Теперь перед реализацией UserDetails и UserDetailsService нужно создать enum с ролями пользователей и UserRepository для хибера, чтобы мы могли его заинжектить в будущий UserDetailsServiceImpl. Этот enum я создам в главной папке где находится класс SecurityCourseApplication

public enum Role {  USRER, //обычный пользователь: видит и редактирует только свои данные  ADMIN  //администратор: имеет доступ ко всем данным независимо от владельца}

Далее создадим сущность User которая будет реализовывать интерфейс UserDetails, и поместим эту сущность в папку entity по пути ru.khalov.securitycourse.entity, ниже покажу как это должно выглядеть.

@Entity@Table(name = "users")@Getter@Setter@NoArgsConstructorpublic class User implements UserDetails {    @Id    @GeneratedValue(strategy = GenerationType.IDENTITY)    @Column(name = "id", nullable = false)    private Long id; // В продакшене нужно использовать UUID    @Column(name = "username", nullable = false, unique = true)    private String username;        @Column(name = "password", nullable = false)    private String password; // хранится в зашифрованном виде    // Это поле это признак того, что аккаунт активен    private boolean enabled = true;        @ElementCollection(fetch = FetchType.EAGER)    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))    @Enumerated(EnumType.STRING)    private Set<Role> roles = new HashSet<>();        @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        return roles.stream().map(role ->                 new SimpleGrantedAuthority("ROLE_"+role.name()))                .collect(Collectors.toSet());    }    @Override    public @Nullable String getPassword() {        return password;    }    @Override    public String getUsername() {        return username;    }    @Override    public boolean isAccountNonExpired() {        return true;    }    @Override    public boolean isAccountNonLocked() {        return true;    }    @Override    public boolean isCredentialsNonExpired() {        return true;    }    @Override    public boolean isEnabled() {        return enabled;    }}

Мы описали класс User, супер. Но теперь стоит пояснить за аннотации над полем roles:

  • @ElementCollection — она помечает коллекцию не-сущностей, то есть embedded-объектов или базовых типов, которую нужно хранить в отдельной таблице. Важно что в отличае от @OneToMany, здесь элементы коллекции — самостоятельные JPA-сущности, то есть у них нет @Entity. Параметр fetch = FetchType.EAGER — указывает на то, что подгрузить эту коллекцию сразу (по умному: не лениво) при загрузке основной сущности.

  • @CollectionTable — она задаёт параметры вспомогательной таблицы, в которой будут храниться элементы коллекции. Параметр: name = «user_roles» — это имя таблицы в базе данных, параметр: joinColumns — это колонка внешнего ключа, которая связывает доп таблицу с основной по определённому ключу. @JoinColunm(name = «user_id») — это имя FK — колонки в таблице «user_roles».

  • @Enumirated — она указывает, как сохранять значения enum в базе данных. Параметр: EnumType.STRING — сохраняет строковое имя константы enum, например «ADMIN».

Дальше необходимо пояснить за некоторые методы которые реализует наша сущность User:

  • public Collection<? extends GrantedAuthority> getAuthorities() — этот метод возвращает список прав (authorities) пользователя в формате, который понимает Spring Security. Каждая роль превращается в GrandedAuthirity (интерфейс который реализуется в классе SimpleGrandedAuthority) с префиксом «ROLE_». Это соглашение, на котором строится проверка hasRole() которую релизуем позже. А возвращает метод набор прав которые есть у пользователя например: {ROLE_USER} или {ROLE_USER, ROLE_ADMIN}

  • public boolean isAccountNonExpired() — этот метод показывает, что учётная запись не истекла по сроку действия. В основном не используется и по дефлоту возвращается true.

  • public boolean isAccountNonLocked() — этот метод показывает, что учётка не заблокирована к примеру после нескольких неудачных попыток входа, или после блокировки администратором. Если вернуть false — то Spring Security не даст пользователю пройти аутентификацию, несмотря на верность логина+пароля.

  • public boolean isCredentialsNonExpired() — этот метод показывает, что пароль не устарел, например если пользователь поменял пароль, и при входе пользователь ввёл старый пароль, приложение скажет что пароль устарел.

  • public boolean isEnabled() — этот метод показывает, что аккаунт активен, например пользователь временно заморозил свою страницу потому что перешёл на другую страницу или временно перешёл в другую соц. сеть, но через время вернётся и снова зайдёт на страницу.

Важно заметить, что у сущности User нет метода isOwner(), потому что этот метод относится к бизнес-логике и его нужно выносить в отдельный сервис.

Далее нужно написать UserRepository, это будет самый банальный репозиторий который экстендится от JpaRepository<>

@Repository //не обязательна поскольку внутри JpaRepository эта аннотация уже естьpublic interface UserRepository extends JpaRepository<User, Long> {    //Поиск пользователя по его уникальому имени, если пользователь не найден,    //вернётся пустота, но это обработается в будущем    Optional<User> findByUsername(String username);    //Проверяет занят логин или нет. Используется при регистрации,    //чтобы не создавать двух и более людей с одним именем    boolean existsByUsername(String username);}

Теперь можно создавать UserDetailsServiceImpl который будет реализовывать UserDetailsService:

@Service@RequiredArgsConstructorpublic class UserDetailsServiceImpl implements UserDetailsService {    private final UserRepository userRepository;    @Override    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{        return userRepository.findByUsername(username).orElseThrow(() ->                new UsernameNotFoundException("User not found: " + username)            );    }}

Spring Security вызывает этот сервис во время аутентификации, чтобы загрузить пользователя по имени. Это как раз та самая единственная точка входа, через которую ферймворк узнаёт о существовании таблицы «users». Повторюсь, но это правда достаточно важно, без этого сервиса, аутентификация не будет знать откуда брать данные пользователя!

Думаю на этом данную статью завершить, и продолжить уже в следующей, поскольку в этой статье уже есть пара вещей которые стоит запомнить как в теории, так и на практике. В дальнейших статьях будем реализовывать PasswordEncoder, Session+Cookie аутентификации через REST.

Спасибо каждому кто дочитал эту статью! Надеюсь она оказалась полезной и вы узнали что-то новое. А если ты более опытный человек, и нашёл у меня какие-либо ошибки, можешь написать комментарий, я буду только рад 🙂

ссылка на оригинал статьи https://habr.com/ru/articles/1055508/