From 5f13305e9593e56a77ffc0cf8c5854dcd3a4c912 Mon Sep 17 00:00:00 2001 From: Anton Romanov Date: Wed, 9 Mar 2022 18:04:09 +0400 Subject: [PATCH] #3 -- add user classes, configs and pages --- build.gradle | 4 +- data/db.mv.db | Bin 32768 -> 49152 bytes .../ru/ulstu/configuration/Constants.java | 12 ++ .../ulstu/configuration/MvcConfiguration.java | 1 + .../PasswordEncoderConfiguration.java | 13 +++ .../configuration/SecurityConfiguration.java | 86 +++++++++++++++ src/main/java/ru/ulstu/model/User.java | 65 +++++++++++ src/main/java/ru/ulstu/model/UserRole.java | 50 +++++++++ .../ru/ulstu/model/UserRoleConstants.java | 6 + src/main/java/ru/ulstu/model/UserSession.java | 103 ++++++++++++++++++ .../java/ru/ulstu/user/IpAddressResolver.java | 23 ++++ .../ru/ulstu/user/UserNotFoundException.java | 7 ++ .../java/ru/ulstu/user/UserRepository.java | 15 +++ .../ru/ulstu/user/UserRoleRepository.java | 7 ++ src/main/java/ru/ulstu/user/UserService.java | 42 +++++++ .../ulstu/user/UserSessionLoginHandler.java | 44 ++++++++ .../ulstu/user/UserSessionLogoutHandler.java | 48 ++++++++ .../ru/ulstu/user/UserSessionRepository.java | 13 +++ .../ru/ulstu/user/UserSessionService.java | 40 +++++++ src/main/java/ru/ulstu/user/UserUtils.java | 24 ++++ src/main/resources/templates/admin.html | 6 +- src/main/resources/templates/error/403.html | 13 +++ src/main/resources/templates/error/404.html | 13 +++ src/main/resources/templates/error/500.html | 13 +++ src/main/resources/templates/login.html | 45 ++++++++ 25 files changed, 687 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ru/ulstu/configuration/Constants.java create mode 100644 src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java create mode 100644 src/main/java/ru/ulstu/configuration/SecurityConfiguration.java create mode 100644 src/main/java/ru/ulstu/model/User.java create mode 100644 src/main/java/ru/ulstu/model/UserRole.java create mode 100644 src/main/java/ru/ulstu/model/UserRoleConstants.java create mode 100644 src/main/java/ru/ulstu/model/UserSession.java create mode 100644 src/main/java/ru/ulstu/user/IpAddressResolver.java create mode 100644 src/main/java/ru/ulstu/user/UserNotFoundException.java create mode 100644 src/main/java/ru/ulstu/user/UserRepository.java create mode 100644 src/main/java/ru/ulstu/user/UserRoleRepository.java create mode 100644 src/main/java/ru/ulstu/user/UserService.java create mode 100644 src/main/java/ru/ulstu/user/UserSessionLoginHandler.java create mode 100644 src/main/java/ru/ulstu/user/UserSessionLogoutHandler.java create mode 100644 src/main/java/ru/ulstu/user/UserSessionRepository.java create mode 100644 src/main/java/ru/ulstu/user/UserSessionService.java create mode 100644 src/main/java/ru/ulstu/user/UserUtils.java create mode 100644 src/main/resources/templates/error/403.html create mode 100644 src/main/resources/templates/error/404.html create mode 100644 src/main/resources/templates/error/500.html create mode 100644 src/main/resources/templates/login.html diff --git a/build.gradle b/build.gradle index e339ff5..11f98dc 100644 --- a/build.gradle +++ b/build.gradle @@ -37,10 +37,10 @@ dependencies { 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-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: '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: 'javax.xml.bind', name:'jaxb-api' implementation group: 'org.javassist', name:'javassist' diff --git a/data/db.mv.db b/data/db.mv.db index 8fe3c2fececf3c4da42c6f2c33870fa4d36b9582..185885d226a55b3495ff230c9d3cfce300bdcf45 100644 GIT binary patch literal 49152 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 delta 3713 zcmdT{&1)M+6yKF>;mED+CfL+*NfadxI4tq(?u@j%mqbnJcY^KKP9Q1C%xcyaRwQFf zA+%5%N^hkf8VaSCQVKm=ac({JRw$Gte?bfFIpi2{ptG}IP>giDFvVJ)jp(-4gk>Hg?3}nLD1FQI>ik~|LVla5kK_mwB;_) zWv6UZElL2%ckwraouPd3sozn^T}F_E6&YHc^0eA@yyPH*f82B+ud6|Cws^Hmc%H8C z-Q{Mp<><(8dCdSAo(;8D?K*YLs^I~Qv>SAJrPgflMhL^}SI4p+tSZY++p_1+}YJ{)#!Nauv$}8%WJOPpyzq1I?{m% zrbMA-xecB0J<*39_F=_+@D>3;po&Uc`)D`XE3{f^&H4Qt6b;!@b2fW0bEqFd9F~Jv zbLQAWcxb_5DIYQ3ekV{Je3d z@>hN`y5M9SrBO;ifDMAMfjrioyucn!9=O6sDm4<)0PpAKle9LU;LC^dijw_0N3wm2 zZkQ02%DsIXo8XgdPIP!C=uq3O!{2y^LxY)-hirOk_~ns5`c>tn%sW-;;dZ^H^VcW} z9@%6cPkk`nb82`_c_o2MDTYfqezNusoYC)M;Dltsq3pxlV3p1o3FlRuQULH67`m9$ zG%z^YgVEe&QY~pxOKe)eq#Rz6<--I^FiFvh9@C19k5tw;4ACj@QI@ineQ;(FWLIyU ziZB^dnYoS11SHc`7AM(br`B_bNdTY^%_ccNfS39so|vKDlX3@$uLW{O01=5I)&VX3~C9F2@Q}cF_E6o0N1gY zinr_7J_tFVX7+ZmWv3{3l}fN4EjU?vb*O{@LC1rcqj);QN}H(Cym1${Me zM9);wWkJsc!E*@UV&TpYcYeD2+1)QTI+aeMk;$k_W14^i6Rwfw7I3&B0f+3rvpcfV z?#L9>m=*ydyF7Mn{Yog2^N*28Cy>aA-6ZnYP$D-2iTra%iG1iwHmj7CWuZrK>D-j OV$K0pzyaApA^$f$GcTh6 diff --git a/src/main/java/ru/ulstu/configuration/Constants.java b/src/main/java/ru/ulstu/configuration/Constants.java new file mode 100644 index 0000000..1cd4983 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/Constants.java @@ -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; +} diff --git a/src/main/java/ru/ulstu/configuration/MvcConfiguration.java b/src/main/java/ru/ulstu/configuration/MvcConfiguration.java index 2599a53..799c414 100644 --- a/src/main/java/ru/ulstu/configuration/MvcConfiguration.java +++ b/src/main/java/ru/ulstu/configuration/MvcConfiguration.java @@ -20,6 +20,7 @@ import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; public class MvcConfiguration implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/login"); registry.addViewController("/index"); registry.addViewController("/admin"); registry.addViewController("/editNews"); diff --git a/src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..19370da --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/PasswordEncoderConfiguration.java @@ -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(); + } +} diff --git a/src/main/java/ru/ulstu/configuration/SecurityConfiguration.java b/src/main/java/ru/ulstu/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..f81bc87 --- /dev/null +++ b/src/main/java/ru/ulstu/configuration/SecurityConfiguration.java @@ -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); + } + } +} diff --git a/src/main/java/ru/ulstu/model/User.java b/src/main/java/ru/ulstu/model/User.java new file mode 100644 index 0000000..7345316 --- /dev/null +++ b/src/main/java/ru/ulstu/model/User.java @@ -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 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 getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } +} diff --git a/src/main/java/ru/ulstu/model/UserRole.java b/src/main/java/ru/ulstu/model/UserRole.java new file mode 100644 index 0000000..6e0a45d --- /dev/null +++ b/src/main/java/ru/ulstu/model/UserRole.java @@ -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; + } +} diff --git a/src/main/java/ru/ulstu/model/UserRoleConstants.java b/src/main/java/ru/ulstu/model/UserRoleConstants.java new file mode 100644 index 0000000..cf78f0c --- /dev/null +++ b/src/main/java/ru/ulstu/model/UserRoleConstants.java @@ -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"; +} diff --git a/src/main/java/ru/ulstu/model/UserSession.java b/src/main/java/ru/ulstu/model/UserSession.java new file mode 100644 index 0000000..42e9838 --- /dev/null +++ b/src/main/java/ru/ulstu/model/UserSession.java @@ -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(); + } +} diff --git a/src/main/java/ru/ulstu/user/IpAddressResolver.java b/src/main/java/ru/ulstu/user/IpAddressResolver.java new file mode 100644 index 0000000..0ed8a60 --- /dev/null +++ b/src/main/java/ru/ulstu/user/IpAddressResolver.java @@ -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(); + } + +} diff --git a/src/main/java/ru/ulstu/user/UserNotFoundException.java b/src/main/java/ru/ulstu/user/UserNotFoundException.java new file mode 100644 index 0000000..73d70a5 --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.user; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/ulstu/user/UserRepository.java b/src/main/java/ru/ulstu/user/UserRepository.java new file mode 100644 index 0000000..bc44854 --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserRepository.java @@ -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 findOneByLoginIgnoreCase(String login); + + @EntityGraph(attributePaths = "roles") + User findOneWithRolesById(int id); + + @EntityGraph(attributePaths = "roles") + User findOneWithRolesByLogin(String login); +} diff --git a/src/main/java/ru/ulstu/user/UserRoleRepository.java b/src/main/java/ru/ulstu/user/UserRoleRepository.java new file mode 100644 index 0000000..79c3aa4 --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserRoleRepository.java @@ -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 { +} diff --git a/src/main/java/ru/ulstu/user/UserService.java b/src/main/java/ru/ulstu/user/UserService.java new file mode 100644 index 0000000..3bcb7bb --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserService.java @@ -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())); + } +} diff --git a/src/main/java/ru/ulstu/user/UserSessionLoginHandler.java b/src/main/java/ru/ulstu/user/UserSessionLoginHandler.java new file mode 100644 index 0000000..dd62ef9 --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserSessionLoginHandler.java @@ -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); + } + } +} diff --git a/src/main/java/ru/ulstu/user/UserSessionLogoutHandler.java b/src/main/java/ru/ulstu/user/UserSessionLogoutHandler.java new file mode 100644 index 0000000..87979d3 --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserSessionLogoutHandler.java @@ -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); + } +} diff --git a/src/main/java/ru/ulstu/user/UserSessionRepository.java b/src/main/java/ru/ulstu/user/UserSessionRepository.java new file mode 100644 index 0000000..54e063a --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserSessionRepository.java @@ -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 findOneBySessionId(String sessionId); + + List findAllByLogoutTimeIsNullAndLoginTimeBefore(Date date); +} diff --git a/src/main/java/ru/ulstu/user/UserSessionService.java b/src/main/java/ru/ulstu/user/UserSessionService.java new file mode 100644 index 0000000..dd06ffa --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserSessionService.java @@ -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); + } +} diff --git a/src/main/java/ru/ulstu/user/UserUtils.java b/src/main/java/ru/ulstu/user/UserUtils.java new file mode 100644 index 0000000..27a453f --- /dev/null +++ b/src/main/java/ru/ulstu/user/UserUtils.java @@ -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; + } +} diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index f1bcb60..f37ada4 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -5,12 +5,10 @@ --> - + diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html new file mode 100644 index 0000000..9eb069c --- /dev/null +++ b/src/main/resources/templates/error/403.html @@ -0,0 +1,13 @@ + + + + + +
+
Доступ запрещён
+
Вернуться на главную
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html new file mode 100644 index 0000000..d599650 --- /dev/null +++ b/src/main/resources/templates/error/404.html @@ -0,0 +1,13 @@ + + + + + +
+
Страница не найдена
+
Вернуться на главную
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/error/500.html b/src/main/resources/templates/error/500.html new file mode 100644 index 0000000..b77a6cf --- /dev/null +++ b/src/main/resources/templates/error/500.html @@ -0,0 +1,13 @@ + + + + + +
+
Ошибка сервера
+
Вернуться на главную
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..52f3558 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,45 @@ + + + + + + +
+ +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+
+
+ + \ No newline at end of file