После того как мы в теории разобрали важные детали о 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/