diff --git a/.gitignore b/.gitignore index f599cbe..c25374e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ hs_err_pid* build/ .gradle/ classes/ +out/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index a518043..fd67b53 100644 --- a/build.gradle +++ b/build.gradle @@ -46,8 +46,16 @@ configurations { dependencies { compile('org.springframework.boot:spring-boot-starter-thymeleaf') compile('org.springframework.boot:spring-boot-starter-web') + compile('net.sourceforge.nekohtml:nekohtml:1.9.21') compile('org.springframework.boot:spring-boot-starter-data-mongodb') compile("de.flapdoodle.embed:de.flapdoodle.embed.mongo") + compile group: 'org.springframework.boot', name: 'spring-boot-starter-security' + compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.7.0' + compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.6.1' + compile group: 'org.springframework', name: 'spring-aspects' + + compileOnly 'org.projectlombok:lombok:1.16.18' + compile group: 'com.rabbitmq', name: 'amqp-client', version: '5.0.0' runtime('com.h2database:h2') diff --git a/src/main/java/ro/ubb/istudent/aspects/Loggable.java b/src/main/java/ro/ubb/istudent/aspects/Loggable.java new file mode 100755 index 0000000..e7fcea9 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/aspects/Loggable.java @@ -0,0 +1,11 @@ +package ro.ubb.istudent.aspects; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +public @interface Loggable { +} diff --git a/src/main/java/ro/ubb/istudent/aspects/LogginAspect.java b/src/main/java/ro/ubb/istudent/aspects/LogginAspect.java new file mode 100755 index 0000000..4b142e9 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/aspects/LogginAspect.java @@ -0,0 +1,30 @@ +package ro.ubb.istudent.aspects; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Aspect +@Component +public class LogginAspect { + + private Logger logger = LoggerFactory.getLogger(LogginAspect.class); + + @Before("execution(public * *(..)) && @annotation(ro.ubb.istudent.aspects.Loggable)") + public void logBefore(JoinPoint joinPoint) throws Throwable { + String userName = SecurityContextHolder.getContext().getAuthentication().getName(); + + String logInfo = "user: " + userName + " called method: " + joinPoint.getSignature().getName() + + "with params: " + Arrays.toString(joinPoint.getArgs()); + + logger.info(logInfo); + + } + +} diff --git a/src/main/java/ro/ubb/istudent/config/CorsFilter.java b/src/main/java/ro/ubb/istudent/config/CorsFilter.java new file mode 100755 index 0000000..551e020 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/config/CorsFilter.java @@ -0,0 +1,57 @@ +package ro.ubb.istudent.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class CorsFilter implements Filter { + + private final SecurityTokenProperties tokenProperties; + + @Autowired + public CorsFilter(SecurityTokenProperties tokenProperties) { + this.tokenProperties = tokenProperties; + } + + public void init(FilterConfig filterConfig) { + } + + /** + * The doFilter method of the Filter is called by the container + * each time a request/response pair is passed through the chain due to a + * client request for a resource at the end of the chain. The FilterChain + * passed in to this method allows the Filter to pass on the request and + * response to the next entity in the chain. + *

+ * Directly set headers on the response after invocation of the next + * entity in the filter chain. + * + * @param req The request to process + * @param res The response associated with the request + * @param chain Provides access to the next filter in the chain for this + * filter to pass the request and response to for further + * processing + * @throws IOException if an I/O error occurs during this filter's + * processing of the request + * @throws ServletException if the processing fails for any other reason + */ + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) res; + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept , Cache-Control , " + + tokenProperties.getHeader()); + chain.doFilter(req, res); + } + + public void destroy() { + } + +} diff --git a/src/main/java/ro/ubb/istudent/config/SecurityTokenProperties.java b/src/main/java/ro/ubb/istudent/config/SecurityTokenProperties.java new file mode 100755 index 0000000..f548f83 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/config/SecurityTokenProperties.java @@ -0,0 +1,20 @@ +package ro.ubb.istudent.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Data +public class SecurityTokenProperties { + + @Value("${security.token.secretKey}") + private String secretKey; + + @Value("${security.token.header}") + private String header; + + @Value("${security.token.expiration}") + private Long expiration; + +} diff --git a/src/main/java/ro/ubb/istudent/config/SwaggerConfig.java b/src/main/java/ro/ubb/istudent/config/SwaggerConfig.java new file mode 100755 index 0000000..a4d29fa --- /dev/null +++ b/src/main/java/ro/ubb/istudent/config/SwaggerConfig.java @@ -0,0 +1,33 @@ +package ro.ubb.istudent.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("ro.ubb.istudent.controller")) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Rest info") + .description("Information about rest endpoints") + .version("1.0") + .build(); + } + +} diff --git a/src/main/java/ro/ubb/istudent/config/WebSecurityConfiguration.java b/src/main/java/ro/ubb/istudent/config/WebSecurityConfiguration.java new file mode 100755 index 0000000..6f5bdf5 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/config/WebSecurityConfiguration.java @@ -0,0 +1,122 @@ +package ro.ubb.istudent.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.data.mongodb.config.EnableMongoAuditing; +import org.springframework.http.HttpMethod; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.authentication.AuthenticationManager; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import ro.ubb.istudent.security.AuthenticationTokenFilter; +import ro.ubb.istudent.security.EntryPointUnauthorizedHandler; + +@EnableWebSecurity +@EnableScheduling +@Configuration +@Order(1) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableMongoAuditing +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final CorsFilter corsFilter; + private final UserDetailsService userDetailsService; + private final EntryPointUnauthorizedHandler unauthorizedHandler; + + @Autowired + public WebSecurityConfiguration(CorsFilter corsFilter, UserDetailsService userDetailsService, EntryPointUnauthorizedHandler unauthorizedHandler) { + this.corsFilter = corsFilter; + this.userDetailsService = userDetailsService; + this.unauthorizedHandler = unauthorizedHandler; + } + + public WebSecurityConfiguration(boolean disableDefaults, CorsFilter corsFilter, UserDetailsService userDetailsService, EntryPointUnauthorizedHandler unauthorizedHandler) { + super(disableDefaults); + this.corsFilter = corsFilter; + this.userDetailsService = userDetailsService; + this.unauthorizedHandler = unauthorizedHandler; + } + + + @Autowired + public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { + authenticationManagerBuilder + .userDetailsService(this.userDetailsService) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .addFilterBefore(corsFilter, ChannelProcessingFilter.class) + .csrf() + .disable() + .exceptionHandling() + .authenticationEntryPoint(this.unauthorizedHandler) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + // note: you can add as well js, css, png etc files if needed + // just follow the same pattern changing the file extension + // for static file serving check also the application.properties file + // you should indicate the path to resources -> static-location + .regexMatchers("/").permitAll() + .regexMatchers("/.*.html").permitAll() + .regexMatchers("/.*.js").permitAll() + .regexMatchers("/.*.css").permitAll() + .regexMatchers("/.*.png").permitAll() + .regexMatchers("/.*.jpg").permitAll() + .regexMatchers("/.*.jpeg").permitAll() + .antMatchers("/login").permitAll() + .antMatchers("/user/save").permitAll() + .antMatchers("/v2/api-docs").permitAll() + .antMatchers(HttpMethod.OPTIONS).permitAll() + .anyRequest().authenticated(); + httpSecurity + .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); + } + + @Bean + public AuthenticationTokenFilter authenticationTokenFilterBean() throws Exception { + AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter(); + authenticationTokenFilter.setAuthenticationManager(authenticationManager()); + return authenticationTokenFilter; + } + + @Configuration + public class CustomWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("forward: public/index.html"); + super.addViewControllers(registry); + } + } + +} diff --git a/src/main/java/ro/ubb/istudent/controller/AuthenticationController.java b/src/main/java/ro/ubb/istudent/controller/AuthenticationController.java new file mode 100755 index 0000000..caa8e71 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/controller/AuthenticationController.java @@ -0,0 +1,95 @@ +package ro.ubb.istudent.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import ro.ubb.istudent.aspects.Loggable; +import ro.ubb.istudent.domain.ValidToken; +import ro.ubb.istudent.dto.AuthenticationRequest; +import ro.ubb.istudent.dto.AuthenticationResponse; +import ro.ubb.istudent.repository.ValidTokenRepository; +import ro.ubb.istudent.security.TokenUtils; +import ro.ubb.istudent.util.HttpHeaders; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; + +@RestController +public class AuthenticationController { + + private final AuthenticationManager authenticationManager; + private final TokenUtils tokenUtils; + private final UserDetailsService userDetailsService; + private final ValidTokenRepository validTokenRepository; + + @Autowired + public AuthenticationController(TokenUtils tokenUtils, AuthenticationManager authenticationManager, UserDetailsService userDetailsService, ValidTokenRepository validTokenRepository) { + this.tokenUtils = tokenUtils; + this.authenticationManager = authenticationManager; + this.userDetailsService = userDetailsService; + this.validTokenRepository = validTokenRepository; + } + + @Loggable + @RequestMapping(path = "/login", method = RequestMethod.POST) + public ResponseEntity authenticationRequest(@RequestBody AuthenticationRequest authenticationRequest) + throws AuthenticationException { + + // Perform the authentication + String userNameOrEmail = authenticationRequest.getEmail() != null ? authenticationRequest.getEmail() : authenticationRequest.getUserName(); + + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + userNameOrEmail, + authenticationRequest.getPassword() + )); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + // Reload password post-authentication so we can generate token + UserDetails userDetails = userDetailsService.loadUserByUsername(userNameOrEmail); + String token = tokenUtils.generateToken(userDetails); + Date expirationDate = tokenUtils.getExpirationDateFromToken(token); + + validTokenRepository.save(ValidToken.builder() + .token(token) + .expirationDate(expirationDate) + .build()); + + return ResponseEntity.ok(new AuthenticationResponse(token)); + } + + @RequestMapping(path = "/logoutMe", method = RequestMethod.GET) + @Transactional + public ResponseEntity logout(HttpServletRequest httpRequest, HttpServletResponse response) { + String authToken = httpRequest.getHeader(HttpHeaders.AUTH_TOKEN); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + UserDetails userDetails = + (UserDetails) authentication.getPrincipal(); + + if (tokenUtils.validateToken(authToken, userDetails)) { + new SecurityContextLogoutHandler().logout(httpRequest, response, authentication); + validTokenRepository.deleteByToken(authToken); + return ResponseEntity.ok("done"); + } else { + return ResponseEntity.ok("bad"); + } + + } + +} diff --git a/src/main/java/ro/ubb/istudent/controller/UserController.java b/src/main/java/ro/ubb/istudent/controller/UserController.java new file mode 100755 index 0000000..930dda7 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/controller/UserController.java @@ -0,0 +1,52 @@ +package ro.ubb.istudent.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ro.ubb.istudent.dto.UserDTO; +import ro.ubb.istudent.service.UserService; + +@RestController +@RequestMapping("/user") +public class UserController { + + private final UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @RequestMapping(value = "/save", method = RequestMethod.POST) + public ResponseEntity saveUser(@RequestBody UserDTO userDTO) { + boolean saved = false; + try { + saved = userService.saveUser(userDTO); + } catch (Throwable e) { + e.printStackTrace(); + } + return ResponseEntity.ok(saved); + } + + @RequestMapping(value = "/update", method = RequestMethod.POST) + public ResponseEntity updateUser(@RequestBody UserDTO userDTO) { + boolean saved = false; + try { + saved = userService.saveUser(userDTO); + } catch (Throwable e) { + e.printStackTrace(); + } + return ResponseEntity.ok(saved); + } + + @RequestMapping(value = "/findByEmail/{email:.+}", method = RequestMethod.GET) + public ResponseEntity getUserByEmail(@PathVariable String email){ + return ResponseEntity.ok(userService.findByEmail(email)); + } + + @RequestMapping(value = "/findByUserName/{username}", method = RequestMethod.GET) + public ResponseEntity getUserByUserName(@PathVariable String username) { + return ResponseEntity.ok(userService.findByUserName(username)); + } + +} diff --git a/src/main/java/ro/ubb/istudent/domain/Gender.java b/src/main/java/ro/ubb/istudent/domain/Gender.java new file mode 100644 index 0000000..2f5fa6b --- /dev/null +++ b/src/main/java/ro/ubb/istudent/domain/Gender.java @@ -0,0 +1,6 @@ +package ro.ubb.istudent.domain; + +public enum Gender { + M, + F +} diff --git a/src/main/java/ro/ubb/istudent/domain/User.java b/src/main/java/ro/ubb/istudent/domain/User.java new file mode 100755 index 0000000..2b09c31 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/domain/User.java @@ -0,0 +1,45 @@ +package ro.ubb.istudent.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "users") +public class User { + + @Id + private ObjectId id; + + private String email; + + private String userName; + + private String password; + + private String address; + + private String phoneNumber; + + private Integer age; + + private Gender gender; + + private List roles; + + public GrantedAuthority getAuthority() { + return new SimpleGrantedAuthority(roles.get(0).name()); + } + +} diff --git a/src/main/java/ro/ubb/istudent/domain/UserRole.java b/src/main/java/ro/ubb/istudent/domain/UserRole.java new file mode 100755 index 0000000..4aa0f11 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/domain/UserRole.java @@ -0,0 +1,7 @@ +package ro.ubb.istudent.domain; + +public enum UserRole { + ADMIN, + TEACHER, + STUDENT +} diff --git a/src/main/java/ro/ubb/istudent/domain/ValidToken.java b/src/main/java/ro/ubb/istudent/domain/ValidToken.java new file mode 100755 index 0000000..f4173a0 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/domain/ValidToken.java @@ -0,0 +1,26 @@ +package ro.ubb.istudent.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +@Document(collection = "valid_tokens") +public class ValidToken { + + @Id + private ObjectId id; + + private String token; + + private Date expirationDate; +} diff --git a/src/main/java/ro/ubb/istudent/dto/AuthenticationRequest.java b/src/main/java/ro/ubb/istudent/dto/AuthenticationRequest.java new file mode 100755 index 0000000..0b1f184 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/dto/AuthenticationRequest.java @@ -0,0 +1,18 @@ +package ro.ubb.istudent.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationRequest { + + private String email; + private String userName; + private String password; + +} diff --git a/src/main/java/ro/ubb/istudent/dto/AuthenticationResponse.java b/src/main/java/ro/ubb/istudent/dto/AuthenticationResponse.java new file mode 100755 index 0000000..aa82baf --- /dev/null +++ b/src/main/java/ro/ubb/istudent/dto/AuthenticationResponse.java @@ -0,0 +1,14 @@ +package ro.ubb.istudent.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationResponse { + + private String token; + +} diff --git a/src/main/java/ro/ubb/istudent/dto/SecurityUser.java b/src/main/java/ro/ubb/istudent/dto/SecurityUser.java new file mode 100755 index 0000000..965285b --- /dev/null +++ b/src/main/java/ro/ubb/istudent/dto/SecurityUser.java @@ -0,0 +1,62 @@ +package ro.ubb.istudent.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import ro.ubb.istudent.domain.UserRole; + +import java.util.Collection; +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SecurityUser implements UserDetails { + + private String id; + private String email; + private String username; + private String password; + private List roles; + private Collection authorities; + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + +} diff --git a/src/main/java/ro/ubb/istudent/dto/UserDTO.java b/src/main/java/ro/ubb/istudent/dto/UserDTO.java new file mode 100755 index 0000000..183aa4e --- /dev/null +++ b/src/main/java/ro/ubb/istudent/dto/UserDTO.java @@ -0,0 +1,29 @@ +package ro.ubb.istudent.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ro.ubb.istudent.domain.Gender; +import ro.ubb.istudent.domain.UserRole; + +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserDTO implements Serializable { + + private String id; + private String userName; + private String email; + private String password; + private String address; + private String phoneNumber; + private Integer age; + private Gender gender; + private List roles; + +} diff --git a/src/main/java/ro/ubb/istudent/mappers/DTOToEntityMapper.java b/src/main/java/ro/ubb/istudent/mappers/DTOToEntityMapper.java new file mode 100755 index 0000000..6a81a56 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/mappers/DTOToEntityMapper.java @@ -0,0 +1,29 @@ +package ro.ubb.istudent.mappers; + +import org.bson.types.ObjectId; +import org.springframework.stereotype.Component; +import ro.ubb.istudent.domain.User; +import ro.ubb.istudent.dto.UserDTO; + +@Component +public class DTOToEntityMapper { + + // perform mapping logic + public User toUser(UserDTO userDTO) { + User user = User.builder() + .email(userDTO.getEmail()) + .password(userDTO.getPassword()) + .userName(userDTO.getUserName()) + .roles(userDTO.getRoles()) + .address(userDTO.getAddress()) + .age(userDTO.getAge()) + .phoneNumber(userDTO.getPhoneNumber()) + .gender(userDTO.getGender()) + .build(); + if(userDTO.getId() != null){ + user.setId(new ObjectId(userDTO.getId())); + } + return user; + } + +} diff --git a/src/main/java/ro/ubb/istudent/mappers/EntityDTOMapper.java b/src/main/java/ro/ubb/istudent/mappers/EntityDTOMapper.java new file mode 100755 index 0000000..9b38148 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/mappers/EntityDTOMapper.java @@ -0,0 +1,24 @@ +package ro.ubb.istudent.mappers; + +import org.springframework.stereotype.Component; +import ro.ubb.istudent.domain.User; +import ro.ubb.istudent.dto.UserDTO; + +@Component +public class EntityDTOMapper { + + public UserDTO toUserDTO(User user){ + if (user == null) return null; + return UserDTO.builder() + .id(user.getId().toHexString()) + .email(user.getEmail()) + .userName(user.getUserName()) + .roles(user.getRoles()) + .address(user.getAddress()) + .age(user.getAge()) + .phoneNumber(user.getPhoneNumber()) + .gender(user.getGender()) + .build(); + } + +} diff --git a/src/main/java/ro/ubb/istudent/repository/UserRepository.java b/src/main/java/ro/ubb/istudent/repository/UserRepository.java new file mode 100755 index 0000000..b3ea9ae --- /dev/null +++ b/src/main/java/ro/ubb/istudent/repository/UserRepository.java @@ -0,0 +1,19 @@ +package ro.ubb.istudent.repository; + +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; +import ro.ubb.istudent.domain.User; + +import java.util.Optional; + +@Repository +public interface UserRepository extends MongoRepository { + + Optional findUserById(String id); + + Optional findUserByEmail(String email); + + Optional findUserByUserName(String userName); + +} diff --git a/src/main/java/ro/ubb/istudent/repository/ValidTokenRepository.java b/src/main/java/ro/ubb/istudent/repository/ValidTokenRepository.java new file mode 100755 index 0000000..cae2335 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/repository/ValidTokenRepository.java @@ -0,0 +1,17 @@ +package ro.ubb.istudent.repository; + +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.repository.MongoRepository; +import ro.ubb.istudent.domain.ValidToken; + +import java.util.Date; + +public interface ValidTokenRepository extends MongoRepository { + + void deleteByToken(String token); + + void deleteByExpirationDateBefore(Date expirationDate); + + ValidToken findByTokenAndExpirationDateAfter(String token, Date expirationDate); + +} diff --git a/src/main/java/ro/ubb/istudent/security/AuthenticationTokenFilter.java b/src/main/java/ro/ubb/istudent/security/AuthenticationTokenFilter.java new file mode 100755 index 0000000..1048264 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/security/AuthenticationTokenFilter.java @@ -0,0 +1,50 @@ +package ro.ubb.istudent.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import ro.ubb.istudent.config.SecurityTokenProperties; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +public class AuthenticationTokenFilter extends UsernamePasswordAuthenticationFilter { + + @Autowired + private SecurityTokenProperties tokenProperties; + @Autowired + private TokenUtils tokenUtils; + @Autowired + private UserDetailsService userDetailsService; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + String authToken = httpRequest.getHeader(this.tokenProperties.getHeader()); + String username = this.tokenUtils.getUsernameFromToken(authToken); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + if (this.tokenUtils.validateToken(authToken, userDetails)) { + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/ro/ubb/istudent/security/EntryPointUnauthorizedHandler.java b/src/main/java/ro/ubb/istudent/security/EntryPointUnauthorizedHandler.java new file mode 100755 index 0000000..86b62ac --- /dev/null +++ b/src/main/java/ro/ubb/istudent/security/EntryPointUnauthorizedHandler.java @@ -0,0 +1,24 @@ +package ro.ubb.istudent.security; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static ro.ubb.istudent.util.StringUtils.ACCESS_DENIED; + +@Component +public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + AuthenticationException e) throws IOException, ServletException { + httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, ACCESS_DENIED); + } + +} diff --git a/src/main/java/ro/ubb/istudent/security/TokenUtils.java b/src/main/java/ro/ubb/istudent/security/TokenUtils.java new file mode 100755 index 0000000..0e57de6 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/security/TokenUtils.java @@ -0,0 +1,195 @@ +package ro.ubb.istudent.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import ro.ubb.istudent.config.SecurityTokenProperties; +import ro.ubb.istudent.dto.SecurityUser; +import ro.ubb.istudent.repository.ValidTokenRepository; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static ro.ubb.istudent.util.StringUtils.CREATED; +import static ro.ubb.istudent.util.StringUtils.SUB; + +@Component +public class TokenUtils { + + private final Logger logger = Logger.getLogger(this.getClass()); + + private final SecurityTokenProperties tokenProperties; + private ValidTokenRepository validTokenRepository; + + @Autowired + public TokenUtils(SecurityTokenProperties tokenProperties, ValidTokenRepository validTokenRepository) { + this.tokenProperties = tokenProperties; + this.validTokenRepository = validTokenRepository; + } + + /** + * Generate a new token for the given userDetails + * + * @param userDetails the user details to generate the token for + * @return the token generated + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(SUB, userDetails.getUsername()); + claims.put(CREATED, this.generateCurrentDate()); + return this.generateToken(claims); + } + + /** + * Verify if the given token is a valid token of the user details given + * + * @param token the token to be verified + * @param userDetails the user details + * @return true if the token is valid, false otherwise + */ + public boolean validateToken(String token, UserDetails userDetails) { + SecurityUser user = (SecurityUser) userDetails; + final String username = this.getUsernameFromToken(token); + final Date created = this.getCreatedDateFromToken(token); + boolean tokenIsValid = validTokenRepository.findByTokenAndExpirationDateAfter( + token, new Date()) != null; + + return (username.equals(user.getUsername()) + && !(this.isTokenExpired(token)) && tokenIsValid); + } + + + /** + * Read the username form the given token + * + * @param token the token to read the username from + * @return the username read + */ + public String getUsernameFromToken(String token) { + String username; + try { + final Claims claims = this.getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * Read created date property from a given token + * + * @param token to read from + * @return the created date + */ + public Date getCreatedDateFromToken(String token) { + Date created; + try { + final Claims claims = this.getClaimsFromToken(token); + created = new Date((Long) claims.get("created")); + } catch (Exception e) { + logger.error(e.toString()); + created = null; + } + return created; + } + + /** + * Read expiration date property from a given token + * + * @param token to read from + * @return the expiration date + */ + public Date getExpirationDateFromToken(String token) { + Date expiration; + try { + final Claims claims = this.getClaimsFromToken(token); + expiration = claims.getExpiration(); + } catch (Exception e) { + logger.error(e.toString()); + expiration = null; + } + return expiration; + } + + /** + * Read the claims from a token + * + * @param token to read form + * @return the Claims object read + */ + private Claims getClaimsFromToken(String token) { + Claims claims; + try { + claims = Jwts.parser() + .setSigningKey(tokenProperties.getSecretKey()) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + logger.error(e.toString()); + claims = null; + } + return claims; + } + + /** + * Generate the current date + * + * @return the date generated + */ + private Date generateCurrentDate() { + return new Date(System.currentTimeMillis()); + } + + /** + * Generate the expiration date relative to the current date + * + * @return the date generated + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + tokenProperties.getExpiration() * 1000); + } + + /** + * Verify if the token given is expired + * + * @param token the token to be verified + * @return true if the token is expired, false otherwise + */ + private Boolean isTokenExpired(String token) { + final Date expiration = this.getExpirationDateFromToken(token); + return expiration.before(this.generateCurrentDate()); + } + + + /** + * Verify if the creation date is before the given lastPasswordReset date + * + * @param created the token creation date + * @param lastPasswordReset the last date of password reset for a user + * @return true if the token is created before the last password reset, false otherwise + */ + private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) { + return (lastPasswordReset != null && created.before(lastPasswordReset)); + } + + /** + * Generate a new token for the claims given + * + * @param claims the claims to generate the token for + * @return the token generated + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(this.generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecretKey()) + .compact(); + } + +} diff --git a/src/main/java/ro/ubb/istudent/service/ScheduledJobs.java b/src/main/java/ro/ubb/istudent/service/ScheduledJobs.java new file mode 100755 index 0000000..0c5aa6a --- /dev/null +++ b/src/main/java/ro/ubb/istudent/service/ScheduledJobs.java @@ -0,0 +1,27 @@ +package ro.ubb.istudent.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import ro.ubb.istudent.repository.ValidTokenRepository; + +import java.util.Date; + +@Component +public class ScheduledJobs { + + private final ValidTokenRepository validTokenRepository; + + @Autowired + public ScheduledJobs(ValidTokenRepository validTokenRepository) { + this.validTokenRepository = validTokenRepository; + } + + @Scheduled(cron = "0 */30 * * * ?") + @Transactional + public void cleanUpTokenDB() { + validTokenRepository.deleteByExpirationDateBefore(new Date()); + } + +} diff --git a/src/main/java/ro/ubb/istudent/service/UserDetailsService.java b/src/main/java/ro/ubb/istudent/service/UserDetailsService.java new file mode 100755 index 0000000..72ee849 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/service/UserDetailsService.java @@ -0,0 +1,52 @@ +package ro.ubb.istudent.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import ro.ubb.istudent.domain.User; +import ro.ubb.istudent.dto.SecurityUser; +import ro.ubb.istudent.repository.UserRepository; + +import java.util.Optional; + +import static ro.ubb.istudent.util.StringUtils.EMPTY_SPACE; +import static ro.ubb.istudent.util.StringUtils.NOT_FOUND; + +@Service +public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { + + private final UserRepository userRepository; + + @Autowired + public UserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException { + Optional userByEmail = userRepository.findUserByEmail(username); + Optional userByUserName = userRepository.findUserByUserName(username); + + if (userByEmail.isPresent()){ + User user = userByEmail.get(); + return SecurityUser.builder() + .id(user.getId().toHexString()) + .roles(user.getRoles()) + .email(user.getEmail()) + .password(user.getPassword()) + .build(); + } + + if (userByUserName.isPresent()){ + User user = userByUserName.get(); + return SecurityUser.builder() + .id(user.getId().toHexString()) + .roles(user.getRoles()) + .username(user.getUserName()) + .password(user.getPassword()) + .build(); + } + + throw new UsernameNotFoundException(username + EMPTY_SPACE + NOT_FOUND); + } +} diff --git a/src/main/java/ro/ubb/istudent/service/UserService.java b/src/main/java/ro/ubb/istudent/service/UserService.java new file mode 100755 index 0000000..1d56724 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/service/UserService.java @@ -0,0 +1,13 @@ +package ro.ubb.istudent.service; + +import ro.ubb.istudent.dto.UserDTO; + +public interface UserService { + + boolean saveUser(UserDTO user) throws Throwable; + + UserDTO findByEmail(String email); + + UserDTO findByUserName(String userName); + +} diff --git a/src/main/java/ro/ubb/istudent/service/UserServiceImpl.java b/src/main/java/ro/ubb/istudent/service/UserServiceImpl.java new file mode 100755 index 0000000..93e856c --- /dev/null +++ b/src/main/java/ro/ubb/istudent/service/UserServiceImpl.java @@ -0,0 +1,56 @@ +package ro.ubb.istudent.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import ro.ubb.istudent.domain.User; +import ro.ubb.istudent.dto.UserDTO; +import ro.ubb.istudent.mappers.DTOToEntityMapper; +import ro.ubb.istudent.mappers.EntityDTOMapper; +import ro.ubb.istudent.repository.UserRepository; + +import java.util.Optional; + +@Service +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + private final DTOToEntityMapper dtoToEntityMapper; + private final EntityDTOMapper entityDTOMapper; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + @Autowired + public UserServiceImpl(UserRepository userRepository, DTOToEntityMapper dtoToEntityMapper, + EntityDTOMapper entityDTOMapper, BCryptPasswordEncoder bCryptPasswordEncoder) { + this.userRepository = userRepository; + this.dtoToEntityMapper = dtoToEntityMapper; + this.entityDTOMapper = entityDTOMapper; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + } + + @Override + public boolean saveUser(UserDTO userDTO) throws Throwable { + User user = dtoToEntityMapper.toUser(userDTO); + String password = bCryptPasswordEncoder.encode(user.getPassword()); + user.setPassword(password); + return userRepository.save(user) != null; + } + + @Override + public UserDTO findByEmail(String email) { + Optional userOptional = userRepository.findUserByEmail(email); + User user = userOptional.orElseThrow(() -> new IllegalArgumentException("Wrong email")); + return entityDTOMapper.toUserDTO(user); + } + + @Override + public UserDTO findByUserName(String userName) { + Optional userOptional = userRepository.findUserByUserName(userName); + User user = userOptional.orElseThrow(() -> new IllegalArgumentException("Wrong userName")); + return entityDTOMapper.toUserDTO(user); + } + +} + + + diff --git a/src/main/java/ro/ubb/istudent/util/HttpHeaders.java b/src/main/java/ro/ubb/istudent/util/HttpHeaders.java new file mode 100755 index 0000000..714415e --- /dev/null +++ b/src/main/java/ro/ubb/istudent/util/HttpHeaders.java @@ -0,0 +1,7 @@ +package ro.ubb.istudent.util; + +public class HttpHeaders { + + public static final String AUTH_TOKEN = "X-Auth-Token"; + +} diff --git a/src/main/java/ro/ubb/istudent/util/StringUtils.java b/src/main/java/ro/ubb/istudent/util/StringUtils.java new file mode 100755 index 0000000..2063b48 --- /dev/null +++ b/src/main/java/ro/ubb/istudent/util/StringUtils.java @@ -0,0 +1,11 @@ +package ro.ubb.istudent.util; + +public class StringUtils { + + public static final String NOT_FOUND = "not found"; + public static final String SUB = "sub"; + public static final String CREATED = "created"; + public static final String EMPTY_SPACE = " "; + public static final String ACCESS_DENIED = "Access Denied"; + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f5d4db0..6a585d4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,2 +1,14 @@ application: - base-url: "http://localhost:8080/api" \ No newline at end of file + base-url: "http://localhost:8080/api" + +security: + token: + secretKey: "superSecretKey" + header: "X-Auth-Token" + expiration: 3600 + + +spring: + thymeleaf: + mode: LEGACYHTML5 + cache: false \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index cc09721..c0d2d22 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,50 +1,109 @@ - - + - Spring Boot - POST-GET AJAX Example - - - + + Sign-Up/Login Form + + + + + + +

+ + + +
+
+

iStudent

+ + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + -
-
-
- -
- -
- - - -