Web 애플리케이션에서 관리되는 영역으로 Client로부터 오는 요청과 응답에 대해 최초/최종 단계의 위치이며, 요청과 응답의 정보를 변경하거나 부가적인 기능을 추가할 수 있다.
주로 범용적으로 처리해야하는 작업, 로깅 및 보안 처리에 활용한다.
또한 인증, 인가와 관련된 로직을 처리할 수 있고 Filter를 사용하면 인증, 인가와 관련된 로직을 비즈니스 로직과 분리하여 관리할 수 있는 장점이 있다.
이러한 필터는 하나만 존재하는 것이 아니라 여러 개가 Chain 형식으로 묶어서 처리할 수 있다.
@Slf4j(topic = "LoggingFilter")
@Component
@Order(1) // 필터의 순서를 지정한다.
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws
IOException,
ServletException {
// 전처리
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String url = httpServletRequest.getRequestURI();
log.info(url);
// 다음 Filter로 이동
filterChain.doFilter(servletRequest,servletResponse);
// 이 doFilter를 통해 다음 필터로 이동한 뒤, DispatcherServlet을 실행
// 수행된 응답이 DispatcherServlet을 통해 이쪽으로 날라옴
// 후처리
log.info("비즈니스 로직 완료");
}
}
@Order 애노테이션을 사용하여 필터의 순서를 지정할 수 있다.
Filter 인터페이스를 구현하여 doFilter를 오버라이딩을 통해 재정의한다.
이 filterChain.doFilter는 다음 Filter로 이동하는 기능을 수행한다.
즉, 현재 이 LoggingFilter에서는 현재의 Request의 URL 값을 log를 통해 찍어주고 FilterChain 클래스의 doFilter를 통해 다음 필터로 이동하는 메서드 기능을 하게 된다.
필터로 넘어간 뒤 DispatcherServlet을 통해 요청에 대한 값을 Controller, Service, Repository, View 를 통해 로직이 실행된 후에, 다시 필터단으로 넘어와서 클라이언트로 값이 넘어가게 된다.
@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
@RequiredArgsConstructor
public class AuthFilter implements Filter {
// 인증 및 인가 처리 필터이다.
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws
IOException,
ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
String url = httpServletRequest.getRequestURI();
if(StringUtils.hasText(url) && (url.startsWith("api/user") || url.startsWith("/css") || url.startsWith("/js"))) {
// 회원가입, 로그인 관련 API는 인증 필요없이 요청 진행
filterChain.doFilter(servletRequest,servletResponse); // 다음 Filter로 이동
} else {
// 나머지 API 요청은 인증 처리 진행한다.
// 토큰 확인
String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest); // 직접 쿠키를 뽑아와서 토큰을 확인한다.
if(StringUtils.hasText(tokenValue)) {
// 토큰이 존재하면 검증을 시작한다.
String token = jwtUtil.substringToken(tokenValue);
if(!jwtUtil.validateToken(token)) {
throw new IllegalArgumentException("Token Error");
}
Claims info = jwtUtil.getUserInfoFromToken(token);
User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
new NullPointerException("Not Found User")
);
servletRequest.setAttribute("user", user);
filterChain.doFilter(servletRequest, servletResponse); // 다음 Filter로 이동
} else {
throw new IllegalArgumentException("Not Found Token");
}
}
}
}
Logging 클래스에서 doFilter로 다음 필터에 넘어오게 되면 해당 클래스에서 인증, 인가에 대한 처리를 시작하게 만들어주었다.
일단 if문을 통해 url이 존재하고, 해당 url의 시작값이 "api/user"인지 ,"/css"인지, "/js"인지 판단하여 해당 url이라면 인증 필요 없이 다음 필터로 이동하게 만들어주고
만약 거기에 해당하지 않는다면 인증 처리를 진행하는 코드로 작성하게 하였다.
jwtUtil.getTokenFromRequest 메서드는 해당 요청 Request값에 쿠키를 직접 뽑아서 토큰이 있는지 확인하는 작업이다.
// HttpServletRequest에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest httpServletRequest) {
Cookie[] cookies = httpServletRequest.getCookies();
// httpServletRequest 에서 Cookie 목록으로 가져와 JWT가 저장된 Cookie를 찾는다.
if(cookies == null)
return null;
// tokenValue가 존재하면 토큰 파싱, 검증을 진행하고 사용자 정보가 여기로 가져오게 된다.
for(Cookie cookie : cookies) {
// 가져온 Cookie의 값들 중에 "Authorization" 이 있는지 확인한다.
// 가져온 사용자 username을 사용해서 DB에 사용자가 존재하는지 확인하고 존재하면 인증이 완료된 것
if(cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
// Encode 되어 넘어간 Value값을 다시 Decode 해서 리턴한다.
return URLDecoder.decode(cookie.getValue(),"UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
return null;
}
일단 getTokenFromRequest 메서드 안의 코드는 HttpServletRequest 안의 쿠키들의 값들을 전부 Cookie[] 배열을 통해 넣어준다.
쿠키가 존재하면 토큰을 파싱하여, 검증을 진행하고 사용자 정보를 확인하게 한다.
처음에 토큰을 생성하였을 때, AUTHORIZATION_HEADER
이 값을 쿠키의 이름으로 설정해주었다.
쿠키 이름이 이것인지 확인하고 인코딩을 통한 값을 다시 디코딩하여 토큰을 반환해주는 메서드이다.
@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
@RequiredArgsConstructor
public class AuthFilter implements Filter {
// 인증 및 인가 처리 필터이다.
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws
IOException,
ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
String url = httpServletRequest.getRequestURI();
if(StringUtils.hasText(url) && (url.startsWith("api/user") || url.startsWith("/css") || url.startsWith("/js"))) {
// 회원가입, 로그인 관련 API는 인증 필요없이 요청 진행
filterChain.doFilter(servletRequest,servletResponse); // 다음 Filter로 이동
} else {
// 나머지 API 요청은 인증 처리 진행한다.
// 토큰 확인
String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest); // 직접 쿠키를 뽑아와서 토큰을 확인한다.
if(StringUtils.hasText(tokenValue)) {
// 토큰이 존재하면 검증을 시작한다.
String token = jwtUtil.substringToken(tokenValue);
if(!jwtUtil.validateToken(token)) {
throw new IllegalArgumentException("Token Error");
}
Claims info = jwtUtil.getUserInfoFromToken(token);
User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
new NullPointerException("Not Found User")
);
servletRequest.setAttribute("user", user);
filterChain.doFilter(servletRequest, servletResponse); // 다음 Filter로 이동
} else {
throw new IllegalArgumentException("Not Found Token");
}
}
}
}
그렇게 토큰을 반환하면 String tokenValue 값에 해당 토큰의 값이 넣어지게 될 것이며, 여기서부터 토큰을 검증한다.
jwtUtil.substringToken() 메서드를 통해서 해당 토큰의 값에 BEARER_PREFIX 값을 뺀 나머지 값을 반환하면 순수 토큰 값을 통해 검증을 하고 검증이 통과가 되면 DB에 해당 유저의 이름이 존재하는지 확인하는 과정이다.
해당 유저가 확인이 되면 doFilter를 통해 다음 필터로 넘어가게 된다.
이 전에 작성한 코드와 함께 보고 싶으면 [Spring] JWT 다루기 여기에서 작성한 JWT 토큰 생성
에 대해 코드를 보면 된다.
이 코드에서는 검증 이후에 유저의 DB를 확인하는 작업이지만, 다른 DB가 될 수도 있고 다른 검증 방법도 가능하다.