응답의 본문은 둘 중 하나만 사용해야한다
둘다 사용하면 IllegalStateException 발생

Filter에서 처리하자
검증을 매번하는 건 당연히 무리다
그러니 우리는 Filter 에서 체크하여
코드의 중복도 줄이고 컨트롤러 접근 자체를 막아보자
다만 필터는 스프링 MVC 접근 전이라
일반적으로는 JSON 에러 처리를 직접 하던
조용히 false 처리하여 처리하던 (→ 스프링 시큐리티와 함께 엔드포인트에서 처리)
따로 설정하여 스프링의 예외처리단에서 예외처리를 하는
크게 4가지 방법이 있고, 우선 지금은 직접 JSON 으로 예외처리를 하고
나머지는 추후 이 포스팅을 수정하며 알아보도록 할 것이다
Servlet API
서블릿 API에서도 필요한 것들은 우선 정리했다
Request
getRequestURI, URL() + 그외 *
URI, URL 정보를 얻는다
// URL, URI 정보를 얻기
String uri = request.getRequestURI();
String url = request.getRequestURL().toString();
// 메서드와 쿼리스트링 정보를 얻기
String method = request.getMethod();
String queryString = request.getQueryString();getParameter(), getParameterMap() *
파라미터의 정보를 가져온다
String id = request.getParameter("id");
// 요청 파라미터 전체를 가져옴
Map<String, String[]> params = request.getParameterMap();Attribute Setter *
Filter에서 데이터 전달에 사용하기 위해 속성을 설정한다
request.setAttribute("userId", 123L);
Long userId = (Long) request.getAttribute("userId");Response
setContentType(), setCharacterEncoding() *
응답 Content-Type을 설정한다
response.setContentType("application/json"); // JSON
response.setContentType("text/html"); // HTML
response.setContentType("text/plain"); // 일반 텍스트
// 인코딩은 따로 setCharacterEncoding() 으로 지정하거나
// 타입 지정시에 타입; charset=UTF-8; 같이 지정해줘도 된다getWriter() *
문자열 응답에 사용한다 (JSON, HTML, XML 등)
PrintWriter writer = response.getWriter();
writer.write("Hello");
writer.write("{\"message\": \"success\"}");
// DTO를 Json 형태의 변환이 필요하면
//ObjectMapper 를 활용해서 writeValueAsString 하는 등의 방법이 더 좋긴하다getOutputStream() *
바이너리 응답에 사용한다 (파일, 이미지 등)
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(bytes);💡
sendRedirect(URI) *
// 오류가 발생하여 로그인 페이지로 되돌릴 경우
private void redirectToLogin(HttpServletResponse response) throws IOException {
response.sendRedirect("/login");
// 또는
response.setStatus(HttpStatus.FOUND.value()); // 302
response.setHeader("Location", "/login");
}Filter 에서 JWT 검증
@Slf4j
@Order(3)
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private final AdminDetailService adminDetailService;
// 인증이 필요 없는 URL 패턴
private static final List<String> PERMIT_ALL_PATH_ANT_STYLE = List.of(
"/api/auth/**",
"/api/admins/signUp",
"/api/admins/login"
);
// URL 패턴 검증용 (Ant 스타일)
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청
log.info("JwtAuthFilter IN");
// 올바른 토큰이 있는지?
// Authorization 토큰
String authorization = request.getHeader("Authorization");
if(authorization == null || !authorization.startsWith("Bearer ")) {
MessageResponse errorResponse = MessageResponse.fail(HttpStatus.UNAUTHORIZED.name(), "인증 정보가 없습니다");
String json = objectMapper.writeValueAsString(errorResponse);
response.setContentType("application/json; charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(json);
return;
}
// Bearer 검증 잘라내기
String token = authorization.substring("Bearer ".length());
if(!jwtUtil.validateToken(token)) {
MessageResponse errorResponse = MessageResponse.fail(HttpStatus.UNAUTHORIZED.name(), "인증 정보에 대한 확인이 필요합니다");
String json = objectMapper.writeValueAsString(errorResponse);
response.setContentType("application/json; charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(json);
return;
} else {
// 토큰 검증 완료 되었으니 정보를 담아서 활용
// 토큰에서 이메일 추출
String email = jwtUtil.getEmail(token);
// UserDetailService로 정보 조회
UserDetails adminDetail = adminDetailService.loadUserByUsername(email);
// 인증 객체 생성
UsernamePasswordAuthenticationToken authentication
= new UsernamePasswordAuthenticationToken(
adminDetail // principal (누구인지)
, null // credentials
, adminDetail.getAuthorities()); // 권한 정보
// SecurityContext에 인증 객체 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
// 응답
log.info("Test Filter2 OUT");
}
// URI가 토큰 체크를 해야하는 URI 인지?
@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
log.info("JwtFilter IN - request URI : {}", request.getRequestURI());
return PERMIT_ALL_PATH_ANT_STYLE.stream().anyMatch(antStyleUri -> antPathMatcher.match(antStyleUri, request.getRequestURI()));
}
}커스텀으로 처리한 DTO가 있어서 응답 양식을 맞춰주었다 (코드 중복은 우선 생략)
이제 Authorization 헤더에 토큰을 담아보면.. 인증이 통과한다
그리고 앞으로 요청에는 로그인한 계정의 이메일이 담길 것이다
이를 기반으로 쿠키에 저장한 토큰 값을 꺼내 검증하는 것을 추가하면
RefreshToken 처리와 실제 접근 AccessToken 으로 구분하여 보안을 올릴 수 있다
Share article