diff --git a/build.gradle b/build.gradle index 04b39ec..e8a7298 100644 --- a/build.gradle +++ b/build.gradle @@ -14,15 +14,15 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.projectlombok:lombok:1.18.24' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' - // render - implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0' annotationProcessor 'org.projectlombok:lombok' // https://mvnrepository.com/artifact/org.postgresql/postgresql diff --git a/src/main/java/com/io/realworld/api/MainController.java b/src/main/java/com/io/realworld/api/MainController.java deleted file mode 100644 index 5d318bc..0000000 --- a/src/main/java/com/io/realworld/api/MainController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.io.realworld.api; - -import com.io.realworld.DTO.UserSignupRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.ModelAndView; - -@Slf4j -@Controller -public class MainController { - - @GetMapping("") - public String mainHome(){ - return "index"; - } - - @GetMapping("/register") - public String signupView(){ - return "/users/signup"; - } - -} diff --git a/src/main/java/com/io/realworld/api/users/UserController.java b/src/main/java/com/io/realworld/api/users/UserController.java index a3da444..bf701cb 100644 --- a/src/main/java/com/io/realworld/api/users/UserController.java +++ b/src/main/java/com/io/realworld/api/users/UserController.java @@ -3,13 +3,10 @@ package com.io.realworld.api.users; import com.io.realworld.DTO.UserSignupRequest; import com.io.realworld.DTO.UserResponse; import com.io.realworld.repository.User; -import com.io.realworld.service.UserService; +import com.io.realworld.service.JwtService; import com.io.realworld.service.UserServiceImpl; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.ModelAndView; import javax.validation.Valid; @@ -19,14 +16,19 @@ import javax.validation.Valid; public class UserController { - private UserServiceImpl userService; + private final UserServiceImpl userService; - public UserController(UserServiceImpl userService) { + private final JwtService jwtService; + + + public UserController(UserServiceImpl userService, JwtService jwtService) { this.userService = userService; + this.jwtService = jwtService; } + @PostMapping(value = "/users") - public UserResponse signup(@Valid @RequestBody UserSignupRequest userSignupRequest) { + public UserResponse signup(@Valid @RequestBody UserSignupRequest userSignupRequest) { User user = userService.signup(userSignupRequest); log.info("register"); @@ -34,6 +36,7 @@ public class UserController { .email(user.getEmail()) .bio(user.getBio()) .image(user.getImage()) + .token(jwtService.createToken(user.getEmail())) .build(); } } diff --git a/src/main/java/com/io/realworld/config/WebConfig.java b/src/main/java/com/io/realworld/config/WebConfig.java index 4f1a6a2..33edd89 100644 --- a/src/main/java/com/io/realworld/config/WebConfig.java +++ b/src/main/java/com/io/realworld/config/WebConfig.java @@ -1,22 +1,31 @@ package com.io.realworld.config; + + +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.authentication.AuthenticationManagerFactoryBean; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; - +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@RequiredArgsConstructor @EnableWebSecurity public class WebConfig { + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -35,6 +44,7 @@ public class WebConfig { .disable() .exceptionHandling() .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); + return http.build(); } diff --git a/src/main/java/com/io/realworld/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/io/realworld/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..d4006f4 --- /dev/null +++ b/src/main/java/com/io/realworld/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,66 @@ +package com.io.realworld.config.jwt; + +import com.io.realworld.repository.User; +import com.io.realworld.service.JwtService; +import com.io.realworld.service.UserServiceImpl; +import lombok.AllArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +@Component +@AllArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String HEADER = "Authorization"; + + private final JwtService jwtService; + + private final UserServiceImpl userService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Optional token = getToken(request.getHeader(HEADER)); + String email = null; + String jwt = null; + if(token.isPresent()){ + jwt = String.valueOf(token); + email = jwtService.getEmail(jwt); + } + if(email != null){ + User findUser = userService.findByEmail(email); + + if(jwtService.validateToken(jwt,findUser)){ + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(findUser,null, AuthorityUtils.NO_AUTHORITIES); + usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + + filterChain.doFilter(request,response); + + } + + private Optional getToken(String header) { + if(header == null){ + return Optional.empty(); + }else{ + String[] s = header.split(" "); + if(s.length < 2){ + return Optional.empty(); + }else{ + return Optional.ofNullable(s[1]); + } + } + } +} diff --git a/src/main/java/com/io/realworld/config/jwt/JwtConfig.java b/src/main/java/com/io/realworld/config/jwt/JwtConfig.java new file mode 100644 index 0000000..324756f --- /dev/null +++ b/src/main/java/com/io/realworld/config/jwt/JwtConfig.java @@ -0,0 +1,18 @@ +package com.io.realworld.config.jwt; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +@Component +@Getter +@PropertySource("classpath:application.properties") +public class JwtConfig { + + @Value("${real-world.token.expiry}") + private Long expiry; + + @Value("${real-world.token.key}") + private String key; +} diff --git a/src/main/java/com/io/realworld/repository/User.java b/src/main/java/com/io/realworld/repository/User.java index 767a142..02cbaea 100644 --- a/src/main/java/com/io/realworld/repository/User.java +++ b/src/main/java/com/io/realworld/repository/User.java @@ -2,13 +2,16 @@ package com.io.realworld.repository; import lombok.Builder; import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; +import java.util.Collection; @Table(name = "users") @Entity @Getter -public class User { +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,11 +33,38 @@ public class User { this.image = image; } + protected User(){} public static User of(String username, String email, String password) { return new User(username, email, password, "", ""); } - protected User() { + @Override + public String getUsername(){ + return this.email; } + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } } diff --git a/src/main/java/com/io/realworld/repository/UserRepository.java b/src/main/java/com/io/realworld/repository/UserRepository.java index a0c79b6..89bf72a 100644 --- a/src/main/java/com/io/realworld/repository/UserRepository.java +++ b/src/main/java/com/io/realworld/repository/UserRepository.java @@ -2,7 +2,9 @@ package com.io.realworld.repository; import org.springframework.data.repository.CrudRepository; + public interface UserRepository extends CrudRepository { User save(User user); User findByEmail(String email); + } diff --git a/src/main/java/com/io/realworld/service/JwtService.java b/src/main/java/com/io/realworld/service/JwtService.java new file mode 100644 index 0000000..ac57396 --- /dev/null +++ b/src/main/java/com/io/realworld/service/JwtService.java @@ -0,0 +1,64 @@ +package com.io.realworld.service; + +import com.io.realworld.config.jwt.JwtConfig; +import com.io.realworld.repository.User; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.Optional; + +@Service +@Getter +@AllArgsConstructor +public class JwtService { + + private final JwtConfig jwtConfig; + + private Key getSignKey(String secretKey){ + byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } + + public Claims extractAllClaims(String token) throws ExpiredJwtException{ + return Jwts.parserBuilder() + .setSigningKey(getSignKey(jwtConfig.getKey())) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String getEmail(String token){ + return extractAllClaims(token).get("email",String.class); + } + + public Boolean isTokenExpired(String token){ + final Date expiration = extractAllClaims(token).getExpiration(); + return expiration.before(new Date()); + } + + public String createToken(String email){ + Claims claims = Jwts.claims(); + claims.put("email",email); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getExpiry() * 1000)) + .signWith(getSignKey(jwtConfig.getKey())) + .compact(); + } + + public Boolean validateToken(String token, User user){ + final String email = getEmail(token); + return email.equals(user.getEmail()) && !isTokenExpired(token); + } + +} diff --git a/src/main/java/com/io/realworld/service/UserServiceImpl.java b/src/main/java/com/io/realworld/service/UserServiceImpl.java index 48336d9..71873ae 100644 --- a/src/main/java/com/io/realworld/service/UserServiceImpl.java +++ b/src/main/java/com/io/realworld/service/UserServiceImpl.java @@ -7,11 +7,12 @@ import com.io.realworld.repository.User; import com.io.realworld.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -37,4 +38,9 @@ public class UserServiceImpl implements UserService { return passwordEncoder.encode(password); } + @Transactional + public User findByEmail(String email){ + return userRepository.findByEmail(email); + } + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 79f1502..14fdc45 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,4 +11,9 @@ spring.jpa.hibernate.ddl-auto=create spring.jpa.properties.hibernate.format_sql=true spring.jpq.show-sql=true +#secret + +real-world.token.expiry=3000000 +real-world.token.key=realworldPostGreSQL0132474654564564213131d31vfxvjfijkjdks + diff --git a/src/main/resources/static/js/userAjax.js b/src/main/resources/static/js/userAxios.js similarity index 53% rename from src/main/resources/static/js/userAjax.js rename to src/main/resources/static/js/userAxios.js index 1a420f4..279ad13 100644 --- a/src/main/resources/static/js/userAjax.js +++ b/src/main/resources/static/js/userAxios.js @@ -10,16 +10,12 @@ function register() { } }) - $.ajax({ + axios({ url: "/api/users", data: userDTO, - contentType: "application/json", - async: false, - type: "POST", - }).success(function (data) { - // Todo redirect home login status - console.log(JSON.stringify(data)); - window.location.href= "/index.html"; - console.log(window.location.href) - }); + headers:{ "Content-Type": "application/json"}, + method: "post", + }).then(function(res){ + alert(res); + }) } \ No newline at end of file diff --git a/src/main/resources/templates/framents/header.html b/src/main/resources/templates/framents/header.html index 6def7ff..05e6ca6 100644 --- a/src/main/resources/templates/framents/header.html +++ b/src/main/resources/templates/framents/header.html @@ -4,6 +4,7 @@ + Conduit diff --git a/src/main/resources/templates/users/signup.html b/src/main/resources/templates/users/signup.html index 6f0fb8e..5298268 100644 --- a/src/main/resources/templates/users/signup.html +++ b/src/main/resources/templates/users/signup.html @@ -1,6 +1,6 @@ - +
@@ -12,19 +12,19 @@ Have an account?

-
    -
  • That email is already taken
  • +
      +
    -
    +
    - +
    - +
    - +