#24 simple sns: 포스트 작성 api 구현
This commit is contained in:
@@ -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())
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 + "\"" + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user