#3 -- add user classes, configs and pages

This commit is contained in:
Anton Romanov 2022-03-09 18:04:09 +04:00
parent f8ff29e989
commit 5f13305e95
25 changed files with 687 additions and 6 deletions

View File

@ -37,10 +37,10 @@ dependencies {
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
implementation group: 'org.springframework.boot', name:'spring-boot-starter-data-jpa' implementation group: 'org.springframework.boot', name:'spring-boot-starter-data-jpa'
//implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security'
implementation group: 'org.slf4j', name: 'slf4j-api', version: versionSLF4J implementation group: 'org.slf4j', name: 'slf4j-api', version: versionSLF4J
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect' implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect'
//implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5' implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5'
implementation group: 'com.h2database', name:'h2' implementation group: 'com.h2database', name:'h2'
implementation group: 'javax.xml.bind', name:'jaxb-api' implementation group: 'javax.xml.bind', name:'jaxb-api'
implementation group: 'org.javassist', name:'javassist' implementation group: 'org.javassist', name:'javassist'

Binary file not shown.

View File

@ -0,0 +1,12 @@
package ru.ulstu.configuration;
public class Constants {
public static final int MIN_PASSWORD_LENGTH = 6;
public static final String LOGIN_REGEX = "^[_'.@A-Za-z0-9-]*$";
public static final String COOKIES_NAME = "JSESSIONID";
public static final String LOGOUT_URL = "/login?logout";
public static final String SESSION_ID_ATTR = "sessionId";
public static final int SESSION_TIMEOUT_SECONDS = 30 * 60;
}

View File

@ -20,6 +20,7 @@ import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
public class MvcConfiguration implements WebMvcConfigurer { public class MvcConfiguration implements WebMvcConfigurer {
@Override @Override
public void addViewControllers(ViewControllerRegistry registry) { public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login");
registry.addViewController("/index"); registry.addViewController("/index");
registry.addViewController("/admin"); registry.addViewController("/admin");
registry.addViewController("/editNews"); registry.addViewController("/editNews");

View File

@ -0,0 +1,13 @@
package ru.ulstu.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,86 @@
package ru.ulstu.configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import ru.ulstu.model.UserRoleConstants;
import ru.ulstu.user.UserService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final AuthenticationSuccessHandler authenticationSuccessHandler;
private final LogoutSuccessHandler logoutSuccessHandler;
public SecurityConfiguration(UserService userService,
BCryptPasswordEncoder bCryptPasswordEncoder,
AuthenticationSuccessHandler authenticationSuccessHandler,
LogoutSuccessHandler logoutSuccessHandler) {
this.userService = userService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.authenticationSuccessHandler = authenticationSuccessHandler;
this.logoutSuccessHandler = logoutSuccessHandler;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable();
log.debug("Security enabled");
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/news/*").permitAll()
.antMatchers("/swagger-ui.html").hasAuthority(UserRoleConstants.ADMIN)
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.successHandler(authenticationSuccessHandler)
.permitAll()
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.logoutSuccessUrl(Constants.LOGOUT_URL)
.invalidateHttpSession(false)
.clearAuthentication(true)
.deleteCookies(Constants.COOKIES_NAME)
.permitAll();
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/img/**")
.antMatchers("/templates/**")
.antMatchers("/webjars/**");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
try {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
}

View File

@ -0,0 +1,65 @@
package ru.ulstu.model;
import ru.ulstu.configuration.Constants;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "is_users")
public class User extends BaseEntity {
@NotNull
@Pattern(regexp = Constants.LOGIN_REGEX)
@Size(min = 1, max = 50)
@Column(length = 50, unique = true, nullable = false)
private String login;
@NotNull
@Size(min = 60, max = 60)
@Column(name = "password_hash", length = 60, nullable = false)
private String password;
@ManyToMany
@JoinTable(
name = "is_user_role",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "user_role_name", referencedColumnName = "name")})
private Set<UserRole> roles;
public User() {
roles = new HashSet<>();
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login.toLowerCase();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<UserRole> getRoles() {
return roles;
}
public void setRoles(Set<UserRole> roles) {
this.roles = roles;
}
}

View File

@ -0,0 +1,50 @@
package ru.ulstu.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
@Table(name = "is_user_roles")
public class UserRole {
@Id
@NotNull
@Size(max = 50)
@Column(length = 50, nullable = false)
private String name;
public UserRole() {
}
public UserRole(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserRole role = (UserRole) o;
return !(name != null ? !name.equals(role.name) : role.name != null);
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}

View File

@ -0,0 +1,6 @@
package ru.ulstu.model;
public class UserRoleConstants {
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
}

View File

@ -0,0 +1,103 @@
package ru.ulstu.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotNull;
import java.util.Date;
@Entity
@Table(name = "is_user_sessions")
public class UserSession extends BaseEntity {
@NotNull
@Column(name = "session_id", nullable = false, unique = true)
private String sessionId;
@NotNull
@Column(name = "ip_address", nullable = false)
private String ipAddress;
@NotNull
@Column(nullable = false)
private String host;
@NotNull
@Column(name = "login_time", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date loginTime;
@Column(name = "logout_time")
@Temporal(TemporalType.TIMESTAMP)
private Date logoutTime;
@ManyToOne(optional = false)
@JoinColumn(name = "user_id")
private User user;
public UserSession() {
}
public UserSession(String sessionId, String ipAddress, String host, User user) {
this.sessionId = sessionId;
this.ipAddress = ipAddress;
this.host = host;
this.loginTime = new Date();
this.user = user;
}
public String getSessionId() {
return sessionId;
}
public String getIpAddress() {
return ipAddress;
}
public String getHost() {
return host;
}
public Date getLoginTime() {
return loginTime;
}
public Date getLogoutTime() {
return logoutTime;
}
public User getUser() {
return user;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public void setHost(String host) {
this.host = host;
}
public void setLoginTime(Date loginTime) {
this.loginTime = loginTime;
}
public void setLogoutTime(Date logoutTime) {
this.logoutTime = logoutTime;
}
public void setUser(User user) {
this.user = user;
}
public void close() {
this.logoutTime = new Date();
}
}

View File

@ -0,0 +1,23 @@
package ru.ulstu.user;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
public final class IpAddressResolver {
private static final String CLIENT_IP_HEADER = "Client-IP";
private static final String FORWARDED_FOR_HEADER = "X-Forwarded-For";
public static String getRemoteAddr(HttpServletRequest request) {
String headerClientIp = request.getHeader("");
String headerXForwardedFor = request.getHeader(HttpServletRequest.FORM_AUTH);
if (StringUtils.isEmpty(request.getRemoteAddr()) && !StringUtils.isEmpty(headerClientIp)) {
return headerClientIp;
}
if (!StringUtils.isEmpty(headerXForwardedFor)) {
return headerXForwardedFor;
}
return request.getRemoteAddr();
}
}

View File

@ -0,0 +1,7 @@
package ru.ulstu.user;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,15 @@
package ru.ulstu.user;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.model.User;
public interface UserRepository extends JpaRepository<User, Integer> {
User findOneByLoginIgnoreCase(String login);
@EntityGraph(attributePaths = "roles")
User findOneWithRolesById(int id);
@EntityGraph(attributePaths = "roles")
User findOneWithRolesByLogin(String login);
}

View File

@ -0,0 +1,7 @@
package ru.ulstu.user;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.model.UserRole;
public interface UserRoleRepository extends JpaRepository<UserRole, String> {
}

View File

@ -0,0 +1,42 @@
package ru.ulstu.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.ulstu.model.User;
import java.util.Collections;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional
public class UserService implements UserDetailsService {
private final Logger log = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserByLogin(String login) {
return userRepository.findOneByLoginIgnoreCase(login);
}
@Override
public UserDetails loadUserByUsername(String username) {
final User user = userRepository.findOneByLoginIgnoreCase(username);
if (user == null) {
throw new UserNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(user.getLogin(),
user.getPassword(),
Optional.ofNullable(user.getRoles()).orElse(Collections.emptySet()).stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList()));
}
}

View File

@ -0,0 +1,44 @@
package ru.ulstu.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import ru.ulstu.configuration.Constants;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Component
public class UserSessionLoginHandler extends SavedRequestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final Logger log = LoggerFactory.getLogger(UserSessionLoginHandler.class);
private final UserSessionService userSessionService;
public UserSessionLoginHandler(UserSessionService userSessionService) {
super();
this.userSessionService = userSessionService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
final String login = authentication.getName();
final String ipAddress = IpAddressResolver.getRemoteAddr(request);
final String host = request.getRemoteHost();
log.debug("Authentication Success for {}@{} ({})", login, ipAddress, host);
HttpSession session = request.getSession(false);
if (session != null) {
final String sessionId = session.getId();
userSessionService.createUserSession(sessionId, login, ipAddress, host);
session.setAttribute(Constants.SESSION_ID_ATTR, sessionId);
session.setMaxInactiveInterval(Constants.SESSION_TIMEOUT_SECONDS);
}
}
}

View File

@ -0,0 +1,48 @@
package ru.ulstu.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import ru.ulstu.configuration.Constants;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Component
public class UserSessionLogoutHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler {
private final Logger log = LoggerFactory.getLogger(UserSessionLogoutHandler.class);
private final UserSessionService userSessionService;
public UserSessionLogoutHandler(UserSessionService userSessionService) {
this.userSessionService = userSessionService;
setDefaultTargetUrl(Constants.LOGOUT_URL);
}
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (authentication == null) {
super.onLogoutSuccess(request, response, authentication);
return;
}
final String login = authentication.getName();
final String ipAddress = IpAddressResolver.getRemoteAddr(request);
final String host = request.getRemoteHost();
log.debug("Logout Success for {}@{} ({})", login, ipAddress, host);
HttpSession session = request.getSession(false);
if (session != null) {
final String sessionId = session.getAttribute(Constants.SESSION_ID_ATTR).toString();
userSessionService.closeUserSession(sessionId);
session.removeAttribute(Constants.SESSION_ID_ATTR);
session.invalidate();
}
super.onLogoutSuccess(request, response, authentication);
}
}

View File

@ -0,0 +1,13 @@
package ru.ulstu.user;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.model.UserSession;
import java.util.Date;
import java.util.List;
public interface UserSessionRepository extends JpaRepository<UserSession, Integer> {
UserSession findOneBySessionId(String sessionId);
List<UserSession> findAllByLogoutTimeIsNullAndLoginTimeBefore(Date date);
}

View File

@ -0,0 +1,40 @@
package ru.ulstu.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.ulstu.model.User;
import ru.ulstu.model.UserSession;
@Service
@Transactional
public class UserSessionService {
private final Logger log = LoggerFactory.getLogger(UserSessionService.class);
private final UserSessionRepository userSessionRepository;
private final UserService userService;
public UserSessionService(UserSessionRepository userSessionRepository, UserService userService) {
this.userSessionRepository = userSessionRepository;
this.userService = userService;
}
public void createUserSession(String sessionId, String login, String ipAddress, String host) {
final User user = userService.getUserByLogin(login);
if (user == null) {
throw new UserNotFoundException(login);
}
userSessionRepository.save(new UserSession(sessionId, ipAddress, host, user));
log.debug("User session {} created for user {}@{} ({})", sessionId, login, ipAddress, host);
}
public void closeUserSession(String sessionId) {
final UserSession userSession = userSessionRepository.findOneBySessionId(sessionId);
if (userSession == null) {
throw new IllegalArgumentException(String.format("User session %s not found", sessionId));
}
userSession.close();
userSessionRepository.save(userSession);
log.debug("User session {} closed", sessionId);
}
}

View File

@ -0,0 +1,24 @@
package ru.ulstu.user;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
public class UserUtils {
public static String getCurrentUserLogin() {
final SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext == null) {
return null;
}
final Authentication authentication = securityContext.getAuthentication();
if (authentication.getPrincipal() instanceof UserDetails) {
final UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
return springSecurityUser.getUsername();
}
if (authentication.getPrincipal() instanceof String) {
return (String) authentication.getPrincipal();
}
return null;
}
}

View File

@ -5,12 +5,10 @@
--> -->
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd"> <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html <html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{default}">
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<div class="container" layout:fragment="content"> <div class="container" layout:fragment="content">
<a href="/editNews/0" class="btn btn-outline-dark"> <a href="/editNews/0" class="btn btn-outline-dark">
<i class="fa fa-plus-square" aria-hidden="true"> Добавить новость</i> <i class="fa fa-plus-square" aria-hidden="true">Добавить новость</i>
</a> </a>
</div> </div>
</html> </html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<div class="container" layout:fragment="content">
<h5>Доступ запрещён</h5>
<a href="/"><h6>Вернуться на главную</h6></a>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<div class="container" layout:fragment="content">
<h5>Страница не найдена</h5>
<a href="/"><h6>Вернуться на главную</h6></a>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<div class="container" layout:fragment="content">
<h5>Ошибка сервера</h5>
<a href="/"><h6>Вернуться на главную</h6></a>
</div>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<nav layout:fragment="navbar">
<div class="navbar-header">
<a class="navbar-brand" href="/"><span class="ui-menuitem-text"><i
class="fa fa-plane fa-4" aria-hidden="true"></i> Balance</span></a>
</div>
</nav>
<div class="container" layout:fragment="content">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#signin">Вход в систему</a>
</li>
</ul>
<div class="tab-content">
<div id="signin" class="tab-pane active">
<form th:action="@{/login}" method="post" class="margined-top-10">
<fieldset>
<div class="form-group">
<input type="text" name="username" id="username" class="form-control"
placeholder="Логин" required="true" autofocus="true"/>
</div>
<div class="form-group">
<input type="password" name="password" id="password" class="form-control"
placeholder="Пароль" required="true"/>
</div>
<button type="submit" class="btn btn-success btn-block">Войти</button>
<div class="form-group">
<small class="form-text text-muted">
<a href="/resetRequest">Забыли пароль?</a>
</small>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</body>
</html>