From b8b6e507a893415b931ba0bccc6a4c77f059f8c0 Mon Sep 17 00:00:00 2001 From: haerong22 Date: Fri, 4 Nov 2022 23:34:08 +0900 Subject: [PATCH] =?UTF-8?q?#24=20simple=20sns:=20=ED=8F=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sns/config/AuthenticationConfig.java | 18 ++++- .../sns/config/filter/JwtTokenFilter.java | 68 +++++++++++++++++++ .../sns/controller/PostController.java | 28 ++++++++ .../sns/controller/response/Response.java | 16 +++++ .../CustomAuthenticationEntryPoint.java | 19 ++++++ .../com/example/sns/exception/ErrorCode.java | 1 + .../main/java/com/example/sns/model/User.java | 32 ++++++++- .../example/sns/model/entity/PostEntity.java | 8 +++ .../com/example/sns/service/PostService.java | 7 +- .../com/example/sns/service/UserService.java | 7 ++ .../com/example/sns/util/JwtTokenUtils.java | 15 ++++ .../sns/controller/PostControllerTest.java | 5 ++ 12 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 simple_sns/src/main/java/com/example/sns/config/filter/JwtTokenFilter.java create mode 100644 simple_sns/src/main/java/com/example/sns/controller/PostController.java create mode 100644 simple_sns/src/main/java/com/example/sns/exception/CustomAuthenticationEntryPoint.java diff --git a/simple_sns/src/main/java/com/example/sns/config/AuthenticationConfig.java b/simple_sns/src/main/java/com/example/sns/config/AuthenticationConfig.java index 4d56be14..dacc7df3 100644 --- a/simple_sns/src/main/java/com/example/sns/config/AuthenticationConfig.java +++ b/simple_sns/src/main/java/com/example/sns/config/AuthenticationConfig.java @@ -1,15 +1,27 @@ package com.example.sns.config; +import com.example.sns.config.filter.JwtTokenFilter; +import com.example.sns.exception.CustomAuthenticationEntryPoint; +import com.example.sns.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; 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.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class AuthenticationConfig extends WebSecurityConfigurerAdapter { + private final UserService userService; + + @Value("${jwt.secret-key}") + private String key; + @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() @@ -20,9 +32,9 @@ public class AuthenticationConfig extends WebSecurityConfigurerAdapter { .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() - // TODO -// .exceptionHandling() -// .authenticationEntryPoint() + .addFilterBefore(new JwtTokenFilter(key, userService), UsernamePasswordAuthenticationFilter.class) + .exceptionHandling() + .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) ; } } diff --git a/simple_sns/src/main/java/com/example/sns/config/filter/JwtTokenFilter.java b/simple_sns/src/main/java/com/example/sns/config/filter/JwtTokenFilter.java new file mode 100644 index 00000000..f4b212bb --- /dev/null +++ b/simple_sns/src/main/java/com/example/sns/config/filter/JwtTokenFilter.java @@ -0,0 +1,68 @@ +package com.example.sns.config.filter; + +import com.example.sns.model.User; +import com.example.sns.service.UserService; +import com.example.sns.util.JwtTokenUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +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; + +@Slf4j +@RequiredArgsConstructor +public class JwtTokenFilter extends OncePerRequestFilter { + + private final String key; + private final UserService userService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + final String header = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (header == null || !header.startsWith("Bearer ")) { + log.error("Error occurs while getting header. header is null or invalid"); + filterChain.doFilter(request, response); + return; + } + + try { + final String token = header.split(" ")[1].trim(); + + if (JwtTokenUtils.isExpired(token, key)) { + log.error("Token is expired"); + filterChain.doFilter(request, response); + return; + } + + String username = JwtTokenUtils.getUsername(token, key); + + User user = userService.loadUserByUsername(username); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + user, null, user.getAuthorities() + ); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (RuntimeException e) { + log.error("Error occurs while validating. {}", e.toString()); + filterChain.doFilter(request, response); + return; + } + + filterChain.doFilter(request, response); + + } +} diff --git a/simple_sns/src/main/java/com/example/sns/controller/PostController.java b/simple_sns/src/main/java/com/example/sns/controller/PostController.java new file mode 100644 index 00000000..7be113dd --- /dev/null +++ b/simple_sns/src/main/java/com/example/sns/controller/PostController.java @@ -0,0 +1,28 @@ +package com.example.sns.controller; + +import com.example.sns.controller.request.PostCreateRequest; +import com.example.sns.controller.response.Response; +import com.example.sns.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + + @PostMapping + public Response create(@RequestBody PostCreateRequest request, + Authentication authentication) { + + postService.create(request.getTitle(), request.getBody(), authentication.getName()); + + return Response.success(); + } +} diff --git a/simple_sns/src/main/java/com/example/sns/controller/response/Response.java b/simple_sns/src/main/java/com/example/sns/controller/response/Response.java index 2255496a..d0e1e48b 100644 --- a/simple_sns/src/main/java/com/example/sns/controller/response/Response.java +++ b/simple_sns/src/main/java/com/example/sns/controller/response/Response.java @@ -14,7 +14,23 @@ public class Response { return new Response<>("SUCCESS", result); } + public static Response success() { + return new Response<>("SUCCESS", null); + } + public static Response error(String errorCode) { return new Response<>(errorCode, null); } + + public String toStream() { + if (result == null) { + return "{" + + "\"resultCode\":" + "\"" + resultCode + "\"," + + "\"result\":" + null +"}"; + } + + return "{" + + "\"resultCode\":" + "\"" + resultCode + "\"," + + "\"result\":" + "\"" + result + "\"" + "}"; + } } diff --git a/simple_sns/src/main/java/com/example/sns/exception/CustomAuthenticationEntryPoint.java b/simple_sns/src/main/java/com/example/sns/exception/CustomAuthenticationEntryPoint.java new file mode 100644 index 00000000..8f9b4616 --- /dev/null +++ b/simple_sns/src/main/java/com/example/sns/exception/CustomAuthenticationEntryPoint.java @@ -0,0 +1,19 @@ +package com.example.sns.exception; + +import com.example.sns.controller.response.Response; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + response.setContentType("application/json"); + response.setStatus(ErrorCode.INVALID_TOKEN.getStatus().value()); + response.getWriter().write(Response.error(ErrorCode.INVALID_TOKEN.name()).toStream()); + } +} diff --git a/simple_sns/src/main/java/com/example/sns/exception/ErrorCode.java b/simple_sns/src/main/java/com/example/sns/exception/ErrorCode.java index 1b3a5678..68a3f690 100644 --- a/simple_sns/src/main/java/com/example/sns/exception/ErrorCode.java +++ b/simple_sns/src/main/java/com/example/sns/exception/ErrorCode.java @@ -11,6 +11,7 @@ public enum ErrorCode { DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "Username is duplicated."), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "User not founded."), INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "Password is invalid."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Token is invalid."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error.") ; diff --git a/simple_sns/src/main/java/com/example/sns/model/User.java b/simple_sns/src/main/java/com/example/sns/model/User.java index 384b5bf5..018868f0 100644 --- a/simple_sns/src/main/java/com/example/sns/model/User.java +++ b/simple_sns/src/main/java/com/example/sns/model/User.java @@ -3,12 +3,17 @@ package com.example.sns.model; import com.example.sns.model.entity.UserEntity; import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import java.sql.Timestamp; +import java.util.Collection; +import java.util.List; @Getter @AllArgsConstructor -public class User { +public class User implements UserDetails { private Integer id; private String username; @@ -29,4 +34,29 @@ public class User { entity.getDeletedAt() ); } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(this.getUserRole().toString())); + } + + @Override + public boolean isAccountNonExpired() { + return this.deletedAt == null; + } + + @Override + public boolean isAccountNonLocked() { + return this.deletedAt == null; + } + + @Override + public boolean isCredentialsNonExpired() { + return this.deletedAt == null; + } + + @Override + public boolean isEnabled() { + return this.deletedAt == null; + } } diff --git a/simple_sns/src/main/java/com/example/sns/model/entity/PostEntity.java b/simple_sns/src/main/java/com/example/sns/model/entity/PostEntity.java index 4bcca58c..2ed6c9f7 100644 --- a/simple_sns/src/main/java/com/example/sns/model/entity/PostEntity.java +++ b/simple_sns/src/main/java/com/example/sns/model/entity/PostEntity.java @@ -45,4 +45,12 @@ public class PostEntity { void updatedAt() { this.updatedAt = Timestamp.from(Instant.now()); } + + public static PostEntity of(String title, String body, UserEntity userEntity) { + PostEntity entity = new PostEntity(); + entity.setTitle(title); + entity.setBody(body); + entity.setUser(userEntity); + return entity; + } } diff --git a/simple_sns/src/main/java/com/example/sns/service/PostService.java b/simple_sns/src/main/java/com/example/sns/service/PostService.java index e82d8ef4..defe33c3 100644 --- a/simple_sns/src/main/java/com/example/sns/service/PostService.java +++ b/simple_sns/src/main/java/com/example/sns/service/PostService.java @@ -19,8 +19,6 @@ public class PostService { @Transactional public void create(String title, String body, String username) { - - // TODO user find UserEntity userEntity = userEntityRepository.findByUsername(username) .orElseThrow( () -> new SnsApplicationException( @@ -29,10 +27,7 @@ public class PostService { ) ); - // TODO post save - postEntityRepository.save(new PostEntity()); - - // TODO return + postEntityRepository.save(PostEntity.of(title, body, userEntity)); } } diff --git a/simple_sns/src/main/java/com/example/sns/service/UserService.java b/simple_sns/src/main/java/com/example/sns/service/UserService.java index db913ab2..191f3a1d 100644 --- a/simple_sns/src/main/java/com/example/sns/service/UserService.java +++ b/simple_sns/src/main/java/com/example/sns/service/UserService.java @@ -27,6 +27,13 @@ public class UserService { @Value("${jwt.token.expired-time-ms}") private Long expiredTimeMs; + public User loadUserByUsername(String username) { + return userEntityRepository.findByUsername(username).map(User::fromEntity) + .orElseThrow( + () -> new SnsApplicationException(USER_NOT_FOUND, String.format("%s not founded", username)) + ); + } + @Transactional public User join(String username, String password) { diff --git a/simple_sns/src/main/java/com/example/sns/util/JwtTokenUtils.java b/simple_sns/src/main/java/com/example/sns/util/JwtTokenUtils.java index 6e8754f9..9cf69919 100644 --- a/simple_sns/src/main/java/com/example/sns/util/JwtTokenUtils.java +++ b/simple_sns/src/main/java/com/example/sns/util/JwtTokenUtils.java @@ -11,6 +11,21 @@ import java.util.Date; public class JwtTokenUtils { + public static String getUsername(String token, String key) { + return extractClaims(token, key).get("username", String.class); + } + + public static boolean isExpired(String token, String key) { + Date expiredDate = extractClaims(token, key).getExpiration(); + + return expiredDate.before(new Date()); + } + + private static Claims extractClaims(String token, String key) { + return Jwts.parserBuilder().setSigningKey(getKey(key)) + .build().parseClaimsJws(token).getBody(); + } + public static String generateToken(String username, String key, long expiredTimeMs) { Claims claims = Jwts.claims(); claims.put("username", username); diff --git a/simple_sns/src/test/java/com/example/sns/controller/PostControllerTest.java b/simple_sns/src/test/java/com/example/sns/controller/PostControllerTest.java index bd6a7526..050b3a26 100644 --- a/simple_sns/src/test/java/com/example/sns/controller/PostControllerTest.java +++ b/simple_sns/src/test/java/com/example/sns/controller/PostControllerTest.java @@ -1,11 +1,13 @@ package com.example.sns.controller; import com.example.sns.controller.request.PostCreateRequest; +import com.example.sns.service.PostService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; @@ -25,6 +27,9 @@ public class PostControllerTest { @Autowired private ObjectMapper objectMapper; + @MockBean + private PostService postService; + @Test @WithMockUser void 포스트작성() throws Exception {