[01.18] 내일배움캠프[Spring] TIL-56
1. Spring Project
- 금일 Spring Security 를 적용해서 기본 기능을 구현했다.
- 기본 기능
1) 유저 회원가입 -> 역할 분배 ( Customer
.Seller
,Admin
)
2) 역할에 따른 작업 -> Seller
등록, 등록에 따른 물품판매 등록 등..
오늘의 문제점1
- Spring Security 의
401
,403
Error Handling
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeHttpRequests()
.requestMatchers("/users/sign").permitAll()
.requestMatchers("/users/login").permitAll()
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/sellers/**").hasAnyRole("SELLER")
.requestMatchers("/products/list").permitAll()
.requestMatchers("/products/list/seller/**").permitAll()
.anyRequest().authenticated()
.and().addFilterBefore(new JwtAuthFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
//403
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
return http.build();
}
- 역할에 따른
403
오류를 터트려 주기 위해서 hasAnyRole
을 걸어놨는데, Enum
타입으로 그냥 넣었더니 인식을 못했다 -> String Type
으로 바꿔주니 인식했다.
CustomAuthenticationEntryPoint
package com.sparta.morningworkout.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.morningworkout.dto.StatusResponseDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.OutputStream;
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final StatusResponseDto statusResponseDto =
new StatusResponseDto(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, statusResponseDto);
os.flush();
}
}
}
customAccessDeniedHandler
package com.sparta.morningworkout.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.OutputStream;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.FORBIDDEN.value(), "접근이 거부되었습니다. 권한이 없습니다");
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException accessDeniedException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}
오늘의 문제점2
- 페이징 처리를 하는 중,
TotalPage
의 카운트가 제대로 도출되지 않았다.
해결1
- 반환 타입이
return new PageImpl<>()
형태로 반환해줬기 때문에, PageImple
코드를 까보면 pageable
,totalPage
를 받아야 하는 형태로 구성되어 있었고, 변수로 주니 해결이 됐다.
return new PageImpl<>(products.stream().map(ProductResponseDto::new).collect(Collectors.toList()),pageable,products.getTotalPages());
해결2
PageImpl
로 굳이 감싸서 반환해야할까? -> 어차피 페이징 처리해서 Page<>
형태로 가져오는데 왜?
Page<Product> products = productRepository.findAllByUserId(user.getId(),pageable);
return products.map(ProductResponseDto::new);
- 그냥 어차피 페이징 처리되서 그 안에 total로 count()한 값을 갖고있기 때문에, 이걸 반환해도 된다.
2. 테스트 코드 맛보기
- 아직 Mock 객체 사용에 서툴기 때문에, 특정 URL요청에 따른
Status.OK
가 잘 나오는지 확인했다.
@Autowired
MockMvc mockMvc;
@Test
@DisplayName("requestBuyProducts")
void requestBuyProducts() throws Exception {
mockMvc.perform(post("/customers/products/1")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.with(csrf()))
.andExpect(status().isOk());
}
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("커스터머 불러오기")
void showCustomerList() throws Exception {
MultiValueMap<String,String> requestParam = new LinkedMultiValueMap<>();
requestParam.set("page","1");
mockMvc.perform(MockMvcRequestBuilders.get("/sellers/customers").params(requestParam)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.with(csrf()))
.andExpect(status().isOk());
}