인터셉터에서 addPathPattern 메서드로 URI 필터링을 제공하는데, Http Method로도 필터링하고 싶었습니다. 추가적인 필터링 진행하기 위해서 Spring Security의 AntRequestMatcher를 사용했는데, 다음과 같은 문제가 생겼습니다.
이러한 문제들을 해결하기 위해 변경할 방법을 모색하던 중 PathPatternParser의 존재를 확인했고, 이 방법을 학습했습니다.
컨트롤러 매핑과 비교할 수 있도록 하려면 URI를 디코딩해야 하는데, 경로 구조를 변경할 가능성이 있기 때문에 다시 바람직하지 않습니다.
boolean matches = "/users/register".equals("/users/register")
요청 URI를 바로 디코딩해 일치하는지 확인할 때 발생하는 문제
이러한 이유로 URL 디코딩을 위해 Spring 1.0에서 AntPathMatcher 클래스가 탄생고 Spring 6.0 이후 PathPatternParser로 변경되어 더 많은 기능들을 제공하고 있습니다.
AntPathMatcher는 Spring 1.0부터 제공된 경로 분석 유틸 클래스로 Ant style path pattern을 기반으로 URL을 일치 여부를 확인합니다.
@Test
void testAntPathMatcher() {
AntPathMatcher antPathMatcher = new AntPathMatcher();
antPathMatcher.match("/users/register", "users/register");
}
Ant Style
Ant 스타일 패턴은 특정 구조를 가진 문자열을 일치시키는 데 사용되며 패턴에는 입력 문자열의 일부와 일치할 수 있는 와일드카드 및 자리 표시자가 포함될 수 있습니다.
Ant 스타일 패턴에 사용되는 일반적인 와일드카드 문자는 별표(
*
)로 빈 문자열을 포함한 모든 문자열과 일치합니다.?
와 같은 단일 와일드카드 문자를 일치시키는 데 사용할 수도 있습니다.PATH : /users/** is equal to URI : /users/ URI : /users/123 URI : /users/123/profile URI : /users/123/profile/edit
Spring 5.3 이상에서 제공되는 경로 분석 유틸 클래스이며, PathPattern 문자열을 분석하고 URL 일치 여부를 확인할 수 있는 PathPattern
을 만들게 됩니다.
@Test
void testPathPatternParser() {
PathPatternParser pathPatternParser = new PathPatternParser();
PathContainer parsePath = PathContainer.parsePath("/users/register");
PathPattern pattern = pathPatternParser.parse("/users/register");
boolean matches = pattern.matches(parsePath);
assertThat(matches).isTrue();
}
PathElement
덕분에 wildcard 패턴과 caturing variant에 제약이 없고, PathContainer
덕분에 디코딩된 예약 문자가 경로 구조를 변경할 위험 없이 한 번에 하나의 경로 세그먼트를 일치시킬 수 있습니다.
org.springframework.web.util.pattern.PathPattern
설명AntPathMatcher
는 **
패턴을 제일 마지막에만 사용할 수 있다.AntPathMatcher
는 {*spring}
와 같은 capturing variant도 마지막에만 사용할 수 있다.AntPathMatcher
which needs either the lookup path decoded or the controller mapping encoded, a parsed PathPattern
matches to a parsed representation of the path called RequestPath
, one path segment at a time.즉,
PathPatternParser
로 동작한다면 경로 분석을 위해 요청 URI는PathContainer
에 의해 래핑됩니다.PathContainer
에서는 path parameter가 제거된 상태로 관리가 됩니다. 그 후,PathPattern과
비교하는데, seperator(/
)로 인해 PathElement 단위로 쪼개져 비교합니다.
위 내용을 이해하기 위해 동작을 정리했습니다.
PathPatternParser의 동작을 이해하기 위해 클래스 동작을 쉽게 정리했습니다.
요청 URI가 들어오게 된다면 경로를 PathContainer
로 래핑합니다. parsePath
메서드는 경로 파라미터를 제거한 상태에서 디코딩된 형태로 내용을 표시합니다.
PathContainer#parsePath
static PathContainer parsePath(String path) {
return DefaultPathContainer.createFromUrlPath(path, Options.HTTP_PATH);
}
PathPattern
구문 분석된 경로 패턴을 나타냅니다. 빠른 연산을 위해 PathElement
체이닝으로 비교해 계산된 결과를 누적합니다.
PathPattern#matches
public class PathPattern implements Comparable<PathPattern> {
private final PathElement head;
//...
public boolean matches(PathContainer pathContainer) {
// ...
MatchingContext matchingContext = new MatchingContext(pathContainer, false);
return this.head.matches(0, matchingContext);
}
}
PathElement
PathPattern
은 PathContainer
와 비교할 때, seperator를 기준으로 PathElement
로 나뉘어져 비교합니다. 나뉠 때, PathElement
에 맞게 비교됩니다.
아래는 Elemet 클래스의 종류입니다.
/user/{id}
으로 요청이 온다면/
은SeparatorPathElement
에서user
는LiteralPathElement
로{id}
는CaptureVariablePathElement
에서 분석됩니다.
PathPatternParser
PathPatternParser는 다량의 요청에 대해 지속적인 경로 비교를 효율적으로 도와주는 클래스입니다.
PathPatternParser#parse
경로 상 문자열을 한 번에 한 문자씩 처리해 seperator를 중심으로 PathElement
로 나누고 각 단계에서 구조를 확인합니다.
public PathPattern parse(String pathPattern) throws PatternParseException {
return new InternalPathPatternParser(this).parse(pathPattern);
}
이 때, InternalPathPatternParser를 호출해서 사용하게 되는데, 그 이유는 PathPatternParser가 thread-safe하지 않기 때문입니다.
InternalPathPatternParser#parse
경로 상 문자열을 한 번에 한 문자씩 처리해 seperator를 중심으로 PathElement
로 나누고 각 단계에서 구조를 확인합니다. 전체적인 동작 과정을 아래와 같습니다.
public PathPattern parse(String pathPattern) throws PatternParseException {
// 1. 초기화 과정
// 2. 문자열 파싱
while (this.pos < this.pathPatternLength) {
// 문자열 한 글자씩 검사 로직
this.pos++;
}
// ...
return new PathPattern(pathPattern, this.parser, this.headPE);
}
PathElement
로 나누는 과정은 2 번째 단계에서 진행합니다. sperator를 만나게 되면 방금까지 모은 문자열을 PathElement
로 만듭니다.
public PathPattern parse(String pathPattern) throws PatternParseException {
// 1. 초기화 과정
// 2. 문자열 파싱
while (this.pos < this.pathPatternLength) {
// ...
if (ch == separator) {
if (this.pathElementStart != -1) {
pushPathElement(createPathElement());
}
if (peekDoubleWildcard()) {
pushPathElement(new WildcardTheRestPathElement(this.pos, separator));
this.pos += 2;
}
else {
pushPathElement(new SeparatorPathElement(this.pos, separator));
}
}
}
// ...
}
앞서 빠른 연산을 위해 PathElement
체이닝으로 비교해 계산된 결과를 누적한다고 말씀드렸는데, pushPathElement 과정으로 동작할 때 체이닝하는 것을 볼 수 있었습니다.
InternalPathPatternParser#pushPathElement
새로은 PathElement
가 오게 되면 이중 연결 리스트로 체이닝하게 됩니다.
private void pushPathElement(PathElement newPathElement) {
if (newPathElement instanceof CaptureTheRestPathElement) {
//...
}
else {
if (this.headPE == null) {
// ...
}
else if (this.currentPE != null) {
this.currentPE.next = newPathElement;
newPathElement.prev = this.currentPE;
this.currentPE = newPathElement;
}
}
}
체이닝된 PathElement
는 PathPattern
에 의해 사용될 때, SeparatorPathElement#matches
에서 다음 원소로 이동할 수 있도록 도와줍니다.
SeparatorPathElement#matches
@Override
public boolean matches(int pathIndex, MatchingContext matchingContext) {
if (pathIndex < matchingContext.pathLength && matchingContext.isSeparator(pathIndex)) {
if (isNoMorePattern()) {
//...
}
else {
// 다음 PathElement 호출
return (this.next != null && this.next.matches(pathIndex, matchingContext));
}
}
return false;
}
Spring Security의 SpringAntMatcher
는 AntPathMatcher
를 컴포지션해 사용하며, AntPathRequestMatcher
의 내부 정적 클래스로 선언하고 있습니다.
public final class AntPathRequestMatcher implements RequestMatcher, RequestVariablesExtractor {
//
private static final class SpringAntMatcher implements Matcher {
private final AntPathMatcher antMatcher;
private final String pattern;
@Override
public boolean matches(String path) {
return this.antMatcher.match(this.pattern, path);
}
// ...
}
}
조금 다른 점은 AntPathRequestMatcher는 HttpMethod 비교를 지원하고 있습니다.
public static AntPathRequestMatcher antMatcher(HttpMethod method, String pattern) {
//...
return new AntPathRequestMatcher(pattern, method.name());
}
추가로 request를 인자로 받기 때문에 사용자 입장에서 편리하게 사용할 수 있습니다.
@Override
public boolean matches(HttpServletRequest request) {
// ...
return this.matcher.matches(url);
}
PathPatternParser
는 wildcard 패턴과 caturing variant에 제약이 없고, 경로 구조를 변경할 위험이 없습니다.AntPathRequestMatcher
를 활용하는 것도 방법입니다.Method 필터링을 사용해야 한다면 PathPatternParser
를 컴포지션해야 한다는 것을 알 수 있었습니다.