CMC 개발 컨퍼런스에서 Filter와 Interceptor를 주제로 발표를 했습니다. 첫 기술 관련 발표여서 많이 떨렸지만, 준비하는 과정 및 다른 분들의 발표를 듣는 과정에서 많이 배운 것 같습니다.
웹 어플리케이션을 만들 때 필요한 인터페이스
원래 웹 서버는 클라이언트 요청에 대해 정적인 페이지로만 응답할 수 있었다.
→ 사용자들은 동적인 페이지를 원한다!
→ 동적 데이터를 처리하기 위한 인터페이스 CGI(Common Gateway Interface)가 등장했다.
→ But, CGI는 쓰레드가 다르면 별도의 구현체를 생성한다.
→ CGI 구현체를 싱글톤 패턴으로 바꾼 것이 서블릿이다!
→ 웹 서버에서 요청이 들어오면 WAS(Web Application Server) 내부의 Web Container가 Thread를 생성하고 Servlet 인터페이스에 정의되어 있는 메서드를 호출한다.
→ 간단히 말하면 서블릿은 동적인 페이지를 만들기 위한 인터페이스이다!
스프링이 사용하는 서블릿
javax.servlet.Filter
인터페이스를 구현하여 사용할 수 있다.org.springframework.web.servlet.HandlerInterceptor
인터페이스를 구현하여 사용할 수 있다.chain.doFilter(request, response);
→ 필터가 존재한다면 다음 필터를, 필터가 없다면 서블릿을 호출한다. <filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/*</url-pattern>
</filer-mapping>
<filter-mapping>
<filter-name>secondFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
addPathPatterns
excludePathPatterns
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final String[] SIGNIN_PATH_TO_EXCLUDE = {"/sign-in/**", "/users/**", "/oauth/**", "/h2-console/**"};
private final SignInInterceptor signInInterceptor;
(...)
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signInInterceptor)
.order(1) // 인터셉터 호출 순서 지정
.addPathPatterns("/**") // 인터셉터 적용할 패턴 지정
.excludePathPatterns(SIGNIN_PATH_TO_EXCLUDE); // 인터셉터에서 제외할 패턴 지정
}
}
Path pattern 공식 문서: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
@Retention(RUNTIME)
@Target(METHOD)
public @interface LoginRequired {
}
@LoginRequired
@PatchMapping
public void changeStatus(@RequestBody IssueNumbersRequestDTO issueNumbersRequestDTO,
@RequestParam String status) {
logger.debug("이슈 닫기 or 열기");
issueCommandService.changeIssueStatus(issueNumbersRequestDTO, status);
}
@LoginRequired
@PatchMapping("/{issueId}/assignees")
public void updateAssignees(@PathVariable Long issueId,
@RequestBody AssigneesToUpdateRequestDTO updateAssigneesRequestDTO) {
logger.debug("이슈의 담당자 편집");
issueCommandService.updateAssignees(issueId, updateAssigneesRequestDTO);
}
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@RequiredArgsConstructor
public class JwtAuthInterceptor implements HandlerInterceptor {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer";
private final JwtService jwtService;
private final UserRepository userRepository;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
if (loginRequired(handler)) {
verifyJwt(request);
verifyUser(request);
}
return true;
}
private void verifyUser(HttpServletRequest request) {
Object userId = request.getAttribute(USER_ID);
userRepository.findById((Long) userId).orElseThrow(IllegalUserAccessException::new);
}
private boolean loginRequired(Object handler) {
return handler instanceof HandlerMethod
&& ((HandlerMethod) handler).hasMethodAnnotation(LoginRequired.class);
}
private void verifyJwt(HttpServletRequest request) {
String header = request.getHeader(AUTHORIZATION);
verifyHeader(header);
String jwt = header.substring(BEARER.length()).trim();
DecodedJWT decodedJWT = jwtService.verifyToken(jwt);
request.setAttribute(USER_ID, jwtService.getUserId(decodedJWT));
}
private void verifyHeader(String header) {
if (header == null || !header.startsWith(BEARER)) {
throw new HttpHeaderFormatException();
}
}
}
@Controller
또는 @RequestMapping
을 활용하여 매핑할 경우 핸들러 정보로 HandlerMethod
가 전달된다. @GetMapping("/form")
public String form(HttpSession session) {
if (!isLoginUser(session)) {
throw new IllegalUserAccessException("로그인이 필요합니다.");
}
return "/qna/form";
}
@PostMapping
public String create(String title, String contents, HttpSession session) {
if (!isLoginUser(session)) {
throw new IllegalUserAccessException("로그인이 필요합니다.");
}
User sessionUser = getSessionUser(session);
Question question = new Question(sessionUser, title, contents);
questionService.update(question);
Source
정성스럽게 잘 정리해주셔서 감사합니다! 역시 대단~ㅋㅋ