В этой статье будет рассмотрено создание простого веб приложения с использованием 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>
<?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.
@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.
@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.
@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").
@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() нет ничего нового, он получает данные всех пользователей и добавляет их на страницу.
@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() передаем пути, для которых хотим установить ограничение. Затем указываем, пользователям с какой ролью будет доступна эта страница/страницы.
@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 варианта — для гостя и для авторизованного пользователя.
<%@ 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 Страница регистрации.
<%@ 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 Страница авторизации.
<%@ 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 Страница администратора.
<%@ 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 Страница новостей статическая. Используется только для демонстрации прав пользователей, поэтому содержание на ваш выбор.
<%@ 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:
INSERT INTO public.t_role(id, name)
VALUES (1, 'ROLE_USER'), (2, 'ROLE_ADMIN');
Теперь можно попробовать зарегистрироваться. В приложение не предусмотрено метода для регистрации пользователя-администратора, но он нужен для демонстрации. Поэтому после регистрации нового пользователя, добавьте в таблицу пользователь-роли запись, дающую эту роль:
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.
Скачать готовый проект.
Используемые источники
Комментарии (8)
mmMike
31.12.2019 05:52Обязательное требование для всех сущностей: приватные поля, геттеры и сеттеры для всех полей и пустой конструктор (в примерах не представлены). Их не нужно писать вручную, нажмите Alt+Insert и Idea сделает это за вас.
А еще лучше включить lombok и использовать анотации. Ей ей код более читаем чем это портянка с гетерами и сеттерами.
И… ИМХО уж слишком подробно расписано. С трудом продрался через пошаговое описание, хотя понимаю что это и зачем, и неоднократно пользовался.
Инструкция вида "потрясти бубном 3 раза, прыгнуть через костер 5 раз — пойдет дождь".
Ну и не считая, что это 100-я статья этого вида.
Почему все авторы описывающие Spring описывают его на этом уровне. На уровне "магии" В сущности в основе Spring то простые принципы лежат. И никакой магии.
Я имею в виду, что в статье написано "как сделать", но не "почему так сделать".
Без обид…
rmuhamedgaliev
31.12.2019 08:19Без обид, но это еще одна статья про Spring. При том, что еще и JSP еще использовалось в туториале, а не связка токенов. Я могу ошибаться конечно, но из современных решений, JSP уже не носят и давно.
Лучше бы про micronaut статей бы больше.
not_bad
31.12.2019 10:26-1Хорошая статья для начинающих. Я бы предложил сделать стек более современным и удобным для такого проекта:
— in-memory БД (например, H2)
— Thymeleaf или что-то подобное вместо jsp. Можно пойти дальше и сделать сервис чисто REST API для отдельного фронта
— YAML вместо .properties для конфигурации
— как уже упоминалось, Lombok для геттеров, сеттеров и т. п.
— думаю, было бы интереснее реализовать подобный пример, используя WebFlux вместо Spring MVC
shapovalex
01.01.2020 12:54Так давно никто не пишет.
Зачем создавать проект через maven и вручную подключать зависимости если есть spring initializr?
JSP? Эту стюардессу давно закопали. Лучше покажите как проблему доступа решать в ангуляре.
Maven? Почему? От него начинает отказываться даже суровый Энтерпрайз.
В общем статья была бы ничего лет так 7 назад, но сейчас от неё нафталином попахиваетUrgen
01.01.2020 15:18От мавена в ближайшее время никто не откажется. Строгая структура, стабильный апи, миллион плагинов, которые развиваются и поддерживаются — этих доводов достаточно чтобы как минимум не переводить свои проекты на градл. Да и новые не всегда есть смысл на нём начинать.
shapovalex
01.01.2020 16:02Что у мавена реально крутое — это release plugin. Градловые поделки мне меньше нравятся.
Но, в целом, за последние 3 года я не нашёл ни одной причины чтобы не начинать новый проект на gradle. Старые ладно, их реально иногда затратно перевести.
ukt
Хороший пример.
Хинт. Геттеры и сеттеры можно сократить с помощью lombok.