#24 simple sns: 포스트 작성 api 구현

This commit is contained in:
haerong22
2022-11-04 23:34:08 +09:00
parent 1dd9957897
commit b8b6e507a8
12 changed files with 214 additions and 10 deletions

View File

@@ -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())
;
}
}

View File

@@ -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);
}
}

View File

@@ -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<Void> create(@RequestBody PostCreateRequest request,
Authentication authentication) {
postService.create(request.getTitle(), request.getBody(), authentication.getName());
return Response.success();
}
}

View File

@@ -14,7 +14,23 @@ public class Response<T> {
return new Response<>("SUCCESS", result);
}
public static Response<Void> success() {
return new Response<>("SUCCESS", null);
}
public static Response<Void> error(String errorCode) {
return new Response<>(errorCode, null);
}
public String toStream() {
if (result == null) {
return "{" +
"\"resultCode\":" + "\"" + resultCode + "\"," +
"\"result\":" + null +"}";
}
return "{" +
"\"resultCode\":" + "\"" + resultCode + "\"," +
"\"result\":" + "\"" + result + "\"" + "}";
}
}

View File

@@ -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());
}
}

View File

@@ -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.")
;

View File

@@ -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<? extends GrantedAuthority> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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 {