Добрый день!
В этой статье будет рассмотрено создание простого веб приложения с использованием Spring Boot и Spring Security. В приложении будет реализована регистрация новых пользователей и авторизация, ограничение доступа к страницам сайта в зависимости от роли пользователя.
Главная цель статьи показать как можно ограничить доступ к различным страницам сайта для пользователей с разными ролями.
Что будет представлять из себя приложение
Сайт со следующими страницам:
- страницы доступные всем пользователям: главная, регистрации и логина;
- страница доступная для зарегистрированных пользователей: новости;
- страница доступная для администратора.
Что будем использовать
- JDK 8+;
- Intellij Idea;
- Spring (Spring Boot, Spring MVC, Spring Security);
- Hibernate;
- JSP;
- PostgreSQL.
Содержание
- Описание основных используемых аннотаций.
- Создание нового проекта в IDE.
- Создание структуры проекта (пакетов).
- Добавление сущностей, контроллеров, сервисов, репозиториев и представлений.
- Запуск приложения.
1. Описание основных используемых аннотаций
Controller – специальный тип класса, применяемый в MVC приложениях. Похож на обычный сервлет HttpServlet, работающий с объектами HttpServletRequest и HttpServletResponse, но с расширенными возможностями от Spring Framework.
Repository – указывает, что класс используется для задания перечня
необходимых работ по поиску, получению и сохранению данных. Аннотация может использоваться для реализации шаблона DAO.
Service – указывает, что класс является сервисом для реализации бизнес логики.
Configuration – эта аннотация используется для классов, которые определяют bean-компоненты.
Autowired – аннотация позволяет автоматически установить значение поля. Функциональность этой аннотации заключается в том, что нам не нужно заботиться о том, как лучше всего Bean’у передать экземпляр другого Bean’a. Spring сам найдет нужный Bean и подставит его значение в свойство, которое отмечено аннотацией.
Немного информации о Spring Security
Самым фундаментальным объектом является SecurityContextHolder. В нем хранится информация о текущем контексте безопасности приложения, который включает в себя подробную информацию о пользователе (принципале), работающим с приложением. Spring Security использует объект Authentication, пользователя авторизованной сессии.
«Пользователь» – это просто Object. В большинстве случаев он может быть
приведен к классу UserDetails. UserDetails можно представить, как адаптер между БД пользователей и тем что требуется Spring Security внутри SecurityContextHolder.
Для создания UserDetails используется интерфейс UserDetailsService, с единственным методом:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
2. Создание нового проекта в IDE
Мы будем использовать систему сборки Maven.
Под GroupId подразумевается уникальный идентификатор компании (или ваше личное доменное имя), которая выпускает проект. ArtefactId – это просто название нашего проекта.
После завершения создания проекта отроется файл pom.xml, Idea предложит включить автоимпорт – не отказывайтесь. В этом файле будут содержаться все зависимости (библиотеки), используемые в проекте.
3. Создание структуры проекта (пакетов)
Сразу перейдем к созданию пакетов. Структура проекта, которая должна получиться показана ниже.
Теперь коротко о том, что будет храниться в каждом пакете:
- src\main\java\com\boots\config — классы с конфигурациями для MVC (MvcConfig) и безопасности (WebSecurityConfig);
- src\main\java\com\boots\controller — классы с контроллерами;
- src\main\java\com\boots\entity — классы с моделями;
- src\main\java\com\boots\repository — интерфейсы репозиториев;
- src\main\java\com\boots\service — классы c сервисами для моделей;
- src\main\webapp\resources — статические объекты: js, css, img;
- src\main\webapp\WEB-INF\jsp — представления в виде файлов .jsp.
Рассмотрим файл pom.xml. В этом файле нужно указать ссылку на родительский файл с помощью тега parent, т.о. все свойства и зависимости родителя будут добавлены в этот дочерний файл.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.9.RELEASE</version> </parent>
Далее добавляем зависимости для работы модулей Spring, драйвер БД PostgreSQL, сервера Tomcat, JSTL.
<properties> <java.version>1.8</java.version> </properties>
По умолчанию maven будет использовать старую версию java 1.6, чтобы это исправить указываем версию явно.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.8</version> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>9.0.27</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.2.0.RELEASE</version> </dependency> </dependencies>
Также добавляем плагин, позволяющий упаковывать архивы jar или war и запускать их «на месте»:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Полный pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ark</groupId> <artifactId>spring</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>9.0.27</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Заполним файл application.properties. Первые 3 строки содержат данные для подключения к БД (имя БД – «spring», логин и пароль). Последний 2 строки указывают путь к .jsp файлам:
spring.datasource.url=jdbc:postgresql://localhost/spring spring.datasource.username=postgres spring.datasource.password=password spring.jpa.show-sql=true spring.jpa.generate-ddl=false spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.mvc.view.prefix = /WEB-INF/jsp/ spring.mvc.view.suffix = .jsp
Свойство spring.jpa.show-sql выводит тела запросов к БД в консоль.
spring.jpa.hibernate.ddl-auto позволяет задать стратегию формирования БД на основе наших моделей, имеет разные значения (none, create, update и др.). update в данном случае значит, что таблицы БД и поля будут созданы на основе наших моделей и буду изменяться вместе с ними.
Забегая вперед можно отметить, что нам нужно будет только создать БД с именем spring, а таблицы пользователей, ролей и их связующая таблица вместе с внешними ключами будут сформированы автоматически на основе моделей (пакет entity), к созданию которых мы сейчас перейдем.
4. Добавление сущностей, контроллеров, сервисов, репозиториев и представлений
4.1. Добавление сущностей (моделей)
Обязательное требование для всех сущностей: приватные поля, геттеры и сеттеры для всех полей и пустой конструктор (в примерах не представлены). Их не нужно писать вручную, нажмите Alt+Insert и Idea сделает это за вас.
Для импорта необходимых классов и библиотек используем комбинацию клавиш Alt+Enter.
User
@Entity @Table(name = "t_user") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Size(min=2, message = "Не меньше 5 знаков") private String username; @Size(min=2, message = "Не меньше 5 знаков") private String password; @Transient private String passwordConfirm; @ManyToMany(fetch = FetchType.EAGER) private Set<Role> roles; public User() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } @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 true; } public void setUsername(String username) { this.username = username; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return getRoles(); } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getPasswordConfirm() { return passwordConfirm; } public void setPasswordConfirm(String passwordConfirm) { this.passwordConfirm = passwordConfirm; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } }
User. В начале об аннотациях: Entity говорит о том, что поля класса имеют отображение в БД, Table(name = «t_user») указывает с какой именно таблицей.
GenerationType.IDENTITY параметр IDENTITY значит, что генерацией id будет заниматься БД. Существует другие стратегии. SEQUENCE – использует встроенный в базы данных, такие как PostgreSQL или Oracle, механизм генерации последовательных значений (sequence). TABLE – используется отдельная таблица с проинициализированным значениями ключей. Еще один вариант – AUTO, hibernate сам выберет из одну вышеописанных стратегий, но рекомендуется указывать стратегию явно.
Поле, находящееся под аннотацией Transient, не имеет отображения в БД. Список ролей связан с пользователем отношением многие ко многим (один пользователь может иметь несколько ролей с одной стороны и у одной роли может быть несколько пользователей с другой);FetchType.EAGER – «жадная» загрузка, т.е. список ролей загружается вместе с пользователем сразу (не ждет пока к нему обратятся).
Для того, чтобы в дальнейшим использовать класс Userв Spring Security, он должен реализовывать интерфейс UserDetails. Для этого нужно переопределить все его методы. Но в нашем примере мы будем использовать только метод getAuthorities(), он возвращает список ролей пользователя. Поэтому для остальных методов измените возвращаемое значение на true.
Role
@Entity @Table(name = "t_role") public class Role implements GrantedAuthority { @Id private Long id; private String name; @Transient @ManyToMany(mappedBy = "roles") private Set<User> users; public Role() { } public Role(Long id) { this.id = id; } public Role(Long id, String name) { this.id = id; this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<User> getUsers() { return users; } public void setUsers(Set<User> users) { this.users = users; } @Override public String getAuthority() { return getName(); } }
Role. Этот класс должен реализовывать интерфейс GrantedAuthority, в котором необходимо переопределить только один метод getAuthority() (возвращает имя роли). Имя роли должно соответствовать шаблону: «ROLE_ИМЯ», например, ROLE_USER. Кроме конструктора по умолчанию необходимо добавить еще пару публичных конструкторов: первый принимает только id, второй id и name.
Здесь же можно добавить ограничения на поля. Size(min=2) – значит, что минимальная длина поля 2, в случае если ограничение нарушено будет выведено сообщение.
4.2. Реализация слоя доступа к данным и сервисного слоя
Spring Data предоставляет набор готовых реализаций для создания слоя, обеспечивающего доступ к БД. Интерфейс JpaRepository предоставляет набор стандартных методов (findBy, save, deleteById и др.) для работы с БД.
UserRepository. Создаем интерфейс для пользователя в пакете repository и наследуем JpaRepository<User, Long>, указываем класс User и тип его id — Long.
public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }
Т.о. просто создав интерфейс и унаследовав JpaRepository можно выполнять стандартные запросы к БД. Если понадобиться специфичный метод просто добавляем его в интерфейс, опираясь на подсказки Idea. Например, нам нужен метод поиска пользователя в БД по имени. Пишем тип возвращаемого объекта, а затем IDE предлагает возможные варианты. Т.е. в данном случае имя метода определяет, тело запроса.
При необходимости можно использовать аннотацию Query над методом и писать запросы на HQL или SQL (нужно добавить nativeQuery = true).
@Query(value = "SELECT nextval(pg_get_serial_sequence('t_user', 'id'))", nativeQuery = true) Long getNextId();
RoleRepository. Создаем аналогично, собственные методы тут не понадобятся.
public interface RoleRepository extends JpaRepository<Role, Long> { }
UserService. Содержит методы для бизнес-логики приложения. Этот класс реализует интерфейс UserDetailsService (необходим для Spring Security), в котором нужно переопределить один метод loadUserByUsername().
В этом классе можно увидеть еще один способ выполнения SQL запроса — с помощью EntityManager.
UserService
@Service public class UserService implements UserDetailsService { @PersistenceContext private EntityManager em; @Autowired UserRepository userRepository; @Autowired RoleRepository roleRepository; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found"); } return user; } public User findUserById(Long userId) { Optional<User> userFromDb = userRepository.findById(userId); return userFromDb.orElse(new User()); } public List<User> allUsers() { return userRepository.findAll(); } public boolean saveUser(User user) { User userFromDB = userRepository.findByUsername(user.getUsername()); if (userFromDB != null) { return false; } user.setRoles(Collections.singleton(new Role(1L, "ROLE_USER"))); user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); userRepository.save(user); return true; } public boolean deleteUser(Long userId) { if (userRepository.findById(userId).isPresent()) { userRepository.deleteById(userId); return true; } return false; } public List<User> usergtList(Long idMin) { return em.createQuery("SELECT u FROM User u WHERE u.id > :paramId", User.class) .setParameter("paramId", idMin).getResultList(); } }
Рассмотрим метод saveUser(User user).
public boolean saveUser(User user) { User userFromDB = userRepository.findByUsername(user.getUsername()); if (userFromDB != null) { return false; } user.setRoles(Collections.singleton(new Role(1L, "ROLE_USER"))); user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); userRepository.save(user); return true; }
Сначала происходит поиск в БД по имени пользователя, если пользователь с таким именем уже существует метод заканчивает работу. Если имя пользователя не занято, добавляется роль ROLE_USER. Чтобы не хранить пароль в «сыром» виде он предварительно хэшируется с помощью bCryptPasswordEncoder. Затем новый пользователь сохраняется в БД.
4.3. Добавление контроллеров
Для страниц, которые никак не обрабатываются сервером, а просто возвращают страницу, маппинг можно настроить в конфигурации. Страница login обрабатывается Spring Security контроллером по умолчанию, поэтому для неё отдельный контроллер не требуется.
@Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); registry.addViewController("/news").setViewName("news"); } }
RegistrationController. Отдельный контроллер нужен для страницы регистрации. Для обработки GET запроса используется аннотация @GetMapping("/registration"), для POST – @PostMapping("/registration").
RegistrationController
@Controller public class RegistrationController { @Autowired private UserService userService; @GetMapping("/registration") public String registration(Model model) { model.addAttribute("userForm", new User()); return "registration"; } @PostMapping("/registration") public String addUser(@ModelAttribute("userForm") @Valid User userForm, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "registration"; } if (!userForm.getPassword().equals(userForm.getPasswordConfirm())){ model.addAttribute("passwordError", "Пароли не совпадают"); return "registration"; } if (!userService.saveUser(userForm)){ model.addAttribute("usernameError", "Пользователь с таким именем уже существует"); return "registration"; } return "redirect:/"; } }
Чтобы что-то добавить или получить со страницы мы обращаемся к model. В GET запросе на страницу добавляется новый пустой объект класса User. Это сделано для того, чтобы при POST запросе не доставать данные из формы регистрации по одному (username, password, passwordComfirm), а сразу получить заполненный объект userForm.
Метод addUser() в качестве параметров ожидает объект пользователя (userForm), который был добавлен при GET запросе. Аннотация Valid проверяет выполняются ли ограничения, установленные на поля, в данном случае длина не меньше 2 символов. Если ограничения не были выполнены, то bindingResult будет содержать ошибки.
Если пароль и его подтверждение не совпадают добавляем сообщение на страницу и возвращаем её. В конце пробуем сохранить добавить пользователя в БД.
Метод saveUser() возвращает false, если пользователь с таким именем уже существует и true, если пользователь сохранен в БД. При неудачной попытке сохранения – добавляем сообщение об ошибке и возвращаем страницу. При удачном сохранении пользователя – переходим на главную страницу.
AdminController. Доступ к странице admin имеют только пользователи с ролью администратора. В методе userList() нет ничего нового, он получает данные всех пользователей и добавляет их на страницу.
AdminController
@Controller public class AdminController { @Autowired private UserService userService; @GetMapping("/admin") public String userList(Model model) { model.addAttribute("allUsers", userService.allUsers()); return "admin"; } @PostMapping("/admin") public String deleteUser(@RequestParam(required = true, defaultValue = "" ) Long userId, @RequestParam(required = true, defaultValue = "" ) String action, Model model) { if (action.equals("delete")){ userService.deleteUser(userId); } return "redirect:/admin"; } @GetMapping("/admin/gt/{userId}") public String gtUser(@PathVariable("userId") Long userId, Model model) { model.addAttribute("allUsers", userService.usergtList(userId)); return "admin"; } }
Метод deleteUser() использует аннотацию RequestParam т.е. в представлении будет форма, которая должная передать два параметра – userId и action. Ссылка будет иметь вид http://localhost:8080/admin?userId=24&action=delete при выполнении такого запроса будет удален пользователь с id=24.
Еще один вариант передать параметры в URL – использовать PathVariable. С помощью этой аннотации мы получаем отдельные части URL, для метод getUser() URL будет выглядеть следующим образом: http://localhost:8080/admin/gt/24, после перехода выведется список всех пользователей с id>24.
Настройки безопасности
WebSecurityConfig. Содержит 2 бина BCryptPasswordEncoder и AuthenticationManager, которые, уже встречались ранее в классе userService.
Кроме этого в методе configure() настраивается доступ к различным ресурсам сайта. В качестве параметров метода antMatchers() передаем пути, для которых хотим установить ограничение. Затем указываем, пользователям с какой ролью будет доступна эта страница/страницы.
WebSecurityConfig
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf() .disable() .authorizeRequests() //Доступ только для не зарегистрированных пользователей .antMatchers("/registration").not().fullyAuthenticated() //Доступ только для пользователей с ролью Администратор .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/news").hasRole("USER") //Доступ разрешен всем пользователей .antMatchers("/", "/resources/**").permitAll() //Все остальные страницы требуют аутентификации .anyRequest().authenticated() .and() //Настройка для входа в систему .formLogin() .loginPage("/login") //Перенарпавление на главную страницу после успешного входа .defaultSuccessUrl("/") .permitAll() .and() .logout() .permitAll() .logoutSuccessUrl("/"); } @Autowired protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder()); } }
4.4. Добавление представлений
index.jsp Главная страница, ниже представлены 2 варианта — для гостя и для авторизованного пользователя.
index.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE HTML> <html> <head> <title>Главная</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link rel="stylesheet" type="text/css" href="${contextPath}/resources/css/style.css"> </head> <body> <div> <h3>${pageContext.request.userPrincipal.name}</h3> <sec:authorize access="!isAuthenticated()"> <h4><a href="/login">Войти</a></h4> <h4><a href="/registration">Зарегистрироваться</a></h4> </sec:authorize> <sec:authorize access="isAuthenticated()"> <h4><a href="/logout">Выйти</a></h4> </sec:authorize> <h4><a href="/news">Новости (только пользователь)</a></h4> <h4><a href="/admin">Пользователи (только админ)</a></h4> </div> </body> </html>
Для скрытия части контента на странице для авторизованных пользователей (ссылка на страницу регистрации и авторизации) можно использовать тег authorize из библиотеки тегов Spring Security. Параметр access принимает несколько выражений, можно, например, установить ограничение в зависимости от роли пользователя hasRole(‘ADMIN’).
registration.jsp Страница регистрации.
registration.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Регистрация</title> </head> <body> <div> <form:form method="POST" modelAttribute="userForm"> <h2>Регистрация</h2> <div> <form:input type="text" path="username" placeholder="Username" autofocus="true"></form:input> <form:errors path="username"></form:errors> ${usernameError} </div> <div> <form:input type="password" path="password" placeholder="Password"></form:input> </div> <div> <form:input type="password" path="passwordConfirm" placeholder="Confirm your password"></form:input> <form:errors path="password"></form:errors> ${passwordError} </div> <button type="submit">Зарегистрироваться</button> </form:form> <a href="/">Главная</a> </div> </body> </html>
На этой странице используется тег form из библиотеки тегов, с помощью него осуществляется связка атрибута модели userForm (мы добавили его на страницу при GET запросе в контроллере) и формы:
<form:form method="POST" modelAttribute="userForm">
Также необходимо указать путь для привязки свойств userForm:
<form:input type="text" path="username" placeholder="Username"></form:input>
login.jsp Страница авторизации.
login.jsp
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Log in with your account</title> </head> <body> <sec:authorize access="isAuthenticated()"> <% response.sendRedirect("/"); %> </sec:authorize> <div> <form method="POST" action="/login"> <h2>Вход в систему</h2> <div> <input name="username" type="text" placeholder="Username" autofocus="true"/> <input name="password" type="password" placeholder="Password"/> <button type="submit">Log In</button> <h4><a href="/registration">Зарегистрироваться</a></h4> </div> </form> </div> </body> </html>
Эту страницу, как уже говорилось, обрабатывает контроллер Spring’а по умолчанию. Важно указать действие: action="/login" и name инпутов.
admin.jsp Страница администратора.
admin.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Log in with your account</title> <link rel="stylesheet" type="text/css" href="${contextPath}/resources/css/style.css"> </head> <body> <div> <table> <thead> <th>ID</th> <th>UserName</th> <th>Password</th> <th>Roles</th> </thead> <c:forEach items="${allUsers}" var="user"> <tr> <td>${user.id}</td> <td>${user.username}</td> <td>${user.password}</td> <td> <c:forEach items="${user.roles}" var="role">${role.name}; </c:forEach> </td> <td> <form action="${pageContext.request.contextPath}/admin" method="post"> <input type="hidden" name="userId" value="${user.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit">Delete</button> </form> </td> </tr> </c:forEach> </table> <a href="/">Главная</a> </div> </body> </html>
news.jsp Страница новостей статическая. Используется только для демонстрации прав пользователей, поэтому содержание на ваш выбор.
news.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Новости</title> </head> <body> <div> <h2>Новости <br> Только для залогинившихся пользователей.</h2> <a href="/">Главная</a> </div> </body> </html>
5. Запуск приложения
В main класс Application нужно добавить следующее:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Перед тем, как переходить к следующему шагу, убедитесь в том, что структура вашего проекта соответствует представленной в начале.
Пришло время создать пустую БД с именем spring, это нужно сделать перед первым запуском приложения и только один раз.
Можете запустить приложение и посмотреть, как измениться БД – в ней создадутся 3 пустые таблицы. Нужно добавить роли пользователей в таблицу t_role:
SQL запрос добавление ролей
INSERT INTO public.t_role(id, name) VALUES (1, 'ROLE_USER'), (2, 'ROLE_ADMIN');
Теперь можно попробовать зарегистрироваться. В приложение не предусмотрено метода для регистрации пользователя-администратора, но он нужен для демонстрации. Поэтому после регистрации нового пользователя, добавьте в таблицу пользователь-роли запись, дающую эту роль:
SQL запрос добавление роли админа
INSERT INTO public.t_user_roles(user_id, roles_id) VALUES (1, 2);
Если после добавления прав администратора вы не можете зайти на страницу администратора (ошибка 403) – перезайдите на сайт.
К странице http://localhost:8080/admin доступ имеет только пользователь с ролью администратора. http://localhost:8080/news увидит любой зарегистрированный пользователь. Также можно попробовать перейти на страницы регистрации и логина, будучи авторизированным на сайте.
Заключение
В итоге было создано веб приложение, в котором мы можем управлять доступом пользователей к страницам сайта применяя роли. Можно назначать несколько ролей одному пользователю. Например, при регистрации нового пользователя добавляем базовую для всех роль User, а затем при запросе дополнительных привилегий пользователем можем дать ему допустим роль Writer, которая позволит ему добавлять новые новости на сайт.
Файлы css и js были созданы, но их содержимое не было представлено. При желании можно добавить дизайн, например, используя Bootstrap и интерактивности с помощью js.
Скачать готовый проект.
Используемые источники
- Registration and Login with Spring Boot, Spring Security, Spring Data JPA, Hibernate, MySQL, JSP, Bootstrap and Docker Compose
- Обратная сторона Spring
- Spring Security/Технический обзор Spring Security
- Официальная документация Spring
- Учимся готовить: Spring 3 MVC + Spring Security + Hibernate