태어나서 처음 회사에 지원했다. 서류를 여러 군데 넣었다. 벌써 서류 탈락만 3번째이다.
서류 탈락이 이런 기분이구나...ㅎㅎ
회사 기준에 부합하지 않는 내 실력과 결과물 탓이지 뭐.
더 열심히 다듬어야겠다.
서류를 다듬다가 인증과 인가 구현 중 이슈를 작성했던 부분이 눈에 띄었다.
인증과 인가... 음...
인가는 애노테이션을 이용해 인터셉터로 구현한 명확한 근거가 있었다.
인증은 좀 애매했다. JWT 토큰을 인증하는 부분은 굳이 인터셉터까지 도달할 필요가 없다.
그래서 인증 작업을 인터셉터에서 필터로 리팩토링을 진행했다.
필터(Filter)란?
HTTP 요청과 응답을 거른 뒤 정제할 수 있는 기능이다. Servlet Container 단에서 동작한다.
스프링 범위 밖에서 처리된다.
Dispathcer Servlet에 요청이 전달되기 전 / 후에 url 패턴에 맞는 모든 요청에 대해서 부가적인 작업과 기능을 추가하거나 처리할 수 있다.
필터를 사용해야 할 때
- 보안 관련 공통 작업을 진행
> 웹 컨테이너상에서 동작하므로 보안 검사를 통해 요청을 차단할 수 있어 안정성을 높인다.
- 로깅 작업
- 인코딩 작업
- request 커스터마이징
필터 메서드
- init 메소드
- 필터 객체를 초기화하고 서비스에 추가하기 위한 메서드이다. 웹 컨테이너가 1회 init 메소드를 호출하여 필터 객체를 초기화하면, 이후 요청들은 doFilter() 메소드를 통해 처리된다.
- doFilter 메소드
- url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되고, 디스패처 서블릿에서 클라이언트에게 HTTP 응답이 가기 전에 웹 컨테이너에 의해 실행되는 메소드이다.
doFilter()의 파라미터로는 FilterChain이 있는데, FilterChain의 doFilter() 를 통해 다음 대상으로 요청을 전달하게 된다. chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.
- url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되고, 디스패처 서블릿에서 클라이언트에게 HTTP 응답이 가기 전에 웹 컨테이너에 의해 실행되는 메소드이다.
- destory 메서드
- 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메서드이다. 이는 웹 컨테이너에 의해 1번 호출되며, 이후에는 이제 doFilter() 에 의해 처리되지 않는다.
필터의 구현
일반적으로 Filter 인터페이스를 상속받아 필터를 구현할 수 있지만,
일반적인 상황인 요청이 들어와 서블릿을 실행할 때 또 다른 요청이 들어오게 되면 동일한 필터를 다시 실행하게 된다.
이러할 경우 불필요한 리소스가 낭비된다.
그래서~~! 사용한 것은 OnePerRequestFilter
이 클래스를 상속받게 되면 요청당 필터로 동일 필터의 동작을 방지하며 한 번의 요청당 한 번의 필터만 실행되어 보안 인증을 구현하기에 매우 유용하다.
구현 필터 코드
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private static final String BEARER_TOKEN = "Bearer";
private static final String NULL_TOKEN = "DB에 토큰이 존재하지 않습니다. 로그인이 필요합니다.";
private static final String INVALID_TOKEN = "토큰이 일치하지 않습니다. 잘못된 접근입니다.";
private static final String INVALID_SIGNATURE = "토큰 형식이 잘못 되었습니다.";
//private static final String EMPTY_TOKEN = "헤더에 저장된 토큰이 필요합니다.";
private static final String CLASS_CAST_FAIL = "토큰 유효성 검사가 실패하였습니다. 확인 후 재요청 바랍니다.";
private static final String MALFORMED_TOKEN = "유효하지 않은 토큰입니다.";
private static final String EXPIRED_TOKEN = "토큰 유효기간이 만료되었습니다. 재로그인 바랍니다.";
private static final String UNSUPPORTED_TOKEN = "지원하지 않는 토큰입니다.";
private final AuthorizationExtractor authorizationExtractor;
private final JwtTokenProvider jwtTokenProvider;
private final RedisService redisService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
if (!requestURI.startsWith("/api")) {
filterChain.doFilter(request, response);
}
try {
String requestToken = authorizationExtractor.extract(request, BEARER_TOKEN);
jwtTokenProvider.validateToken(requestToken);
final String tokenUserId = jwtTokenProvider.getUserId(requestToken);
/* request에 토큰 유저 권한 및 아이디 추가 */
request.setAttribute("tokenUserRole", jwtTokenProvider.getUserGrade(requestToken));
request.setAttribute("tokenUserId", jwtTokenProvider.getUserId(requestToken));
request.setAttribute("token", requestToken);
/* Redis DB에 저장된 토큰 추출 */
final String redisToken = redisService.getData(tokenUserId);
/* DB에 토큰이 존재하지 않을 경우 */
if (redisToken == null) {
throw new RedisNullTokenException(NULL_TOKEN);
}
/* DB 토큰과 로그인 유저 토큰 정보가 일치하지 않을 경우 */
if (!redisToken.equals(requestToken)) {
throw new TokenMismatchException(INVALID_TOKEN);
}
filterChain.doFilter(request, response);
} /*catch (TokenEmptyException e) {
setErrorResponse(response, EMPTY_TOKEN, e);
}*/ catch (SignatureException e) {
setErrorResponse(response, INVALID_SIGNATURE, e);
} catch (RedisNullTokenException e) {
setErrorResponse(response, NULL_TOKEN, e);
} catch (TokenMismatchException e) {
setErrorResponse(response, INVALID_TOKEN, e);
} catch (ClassCastException e) {
setErrorResponse(response, CLASS_CAST_FAIL, e);
} catch (MalformedJwtException e) {
setErrorResponse(response, MALFORMED_TOKEN, e);
} catch (ExpiredJwtException e) {
setErrorResponse(response, EXPIRED_TOKEN, e);
} catch (UnsupportedJwtException e) {
setErrorResponse(response, UNSUPPORTED_TOKEN, e);
}
}
private void setErrorResponse(HttpServletResponse response, String message, Exception e) throws IOException {
log.error(e.getMessage());
response.setCharacterEncoding("utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(message);
}
}
@Component를 사용해 스프링 빈으로 등록할 수 있다.
필터를 등록하는 방식은 두 가지인데,
@Component를 이용
-> 요청 URL 매핑 불가, @Order로 순서 지정 가능
@ServletComponentScan(Application 클래스에 지정) + @WebFilter
-> URL 매핑 가능, 순서 지정 불가
나는 첫 번째 방식을 따르기로 했다. 필터가 하나이지만, 애노테이션을 하나만 붙이는 게 더 깔끔하다고 생각이 들었기 때문이다.
예외 처리는 스프링 시큐리티를 사용하지 않고 있기 때문에, 예외 처리 필터를 사용하지 못했다.
직접 Try - Catch 문으로 예외를 잡아 처리해 응답 객체로 응답했다.
동일하게 예외 처리되어 잘 응답이 되는 것을 확인 가능하다.
후기
처음에 인증을 구현할 때 확실히 서블릿 필터에 대한 개념이 부족했던 것 같다.
인터셉터만 사용해봤기 때문에 익숙한 인터셉터로 인증을 구현하지 않았나 싶다.
그 당시에는 낯선 개념에 막혀 안된다고 판단했었지만, 계속 공부하다 보니 익숙해져 한번 해볼 수 있겠다는 생각이 들었다. 막상 다시 해보니 개념이 확실히 정립된 상태여서 비교적 쉽게 구현이 됐다.
이게 발전인가..
그래도 안주하지 말고 흠 없게 하자!
'📕 Spring Framework > Spring Project' 카테고리의 다른 글
코드 리팩토링 [1] (0) | 2022.08.03 |
---|---|
리팩토링 계획 (0) | 2022.07.28 |
스프링 부트 소나큐브(SonarQube) 적용 + PostgreSql (0) | 2022.07.05 |
「파일 업로드/다운로드 및 테스트」 (0) | 2022.07.01 |
「컨트롤러 단위 테스트」 (0) | 2022.06.23 |