java

Spring Webclient queryParam 특수문자 encoding 이슈

maruoov 2022. 12. 20. 19:31

spring 5.2.6 버전 기준

webClient.get()
.uri(uriBuilder -> uriBuilder
    .path("/search")
    .queryParam("keyword", query.getKeyword())
    ...
    .build()
)

위와 같이 요청을 만들면 keyword 가 인코딩 되어 나가는걸 기대 했다.
한글이나 일부 특수문자들도 잘 되길래 크게 의심없이 넘어갔었다.
하지만 이런 방식으로 하면 1+1 이라는 키워드중 ‘+’ 가 인코딩 되지 않는 이슈가 있다.

webClient.get()
.uri(uriBuilder -> uriBuilder
    .path("/search")
    .queryParam("keyword", URLEncoder.encode(query.getKeyword())
    ...
    .build()
)

그래서 파라미터를 직접 인코딩을 해보았다. 하지만 이렇게 하면 인코딩이 두번된다.
(기대값 1%2B1, 실제 api 호출시 나가는 값 1%252B1)

queryParam 을 사용하는 경우에 인코딩 되는 부분을 따라가보니

HierarchicalUriComponents.encodeUriComponent 에서 인코딩을 해주는데,
isAllowed 에 허용되지 않는 것들을 인코딩 해준다.

static String encodeUriComponent(String source, Charset charset, Type type) {
    if (!StringUtils.hasLength(source)) {
        return source;
    }
    Assert.notNull(charset, "Charset must not be null");
    Assert.notNull(type, "Type must not be null");

    byte[] bytes = source.getBytes(charset);
    boolean original = true;
    for (byte b : bytes) {
        if (!type.isAllowed(b)) {
            original = false;
            break;
        }
    }
    if (original) {
        return source;
    }

    ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length);
    for (byte b : bytes) {
        if (type.isAllowed(b)) {
            baos.write(b);
        }
        else {
            baos.write('%');
            char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
            char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
            baos.write(hex1);
            baos.write(hex2);
        }
    }
    return StreamUtils.copyToString(baos, charset);
}

QUERY_PARAM {
    @Override
    public boolean isAllowed(int c) {
        if ('=' == c || '&' == c) {
            return false;
        }
        else {
            return isPchar(c) || '/' == c || '?' == c;
        }
    }
},

protected boolean isPchar(int c) {
    return (isUnreserved(c) || isSubDelimiter(c) || ':' == c || '@' == c);
}

protected boolean isSubDelimiter(int c) {
    return ('!' == c || '$' == c || '&' == c || '\'' == c || '(' == c || ')' == c || '*' == c || '+' == c ||
            ',' == c || ';' == c || '=' == c);
}

HierarchicalUriComponents.TYPE.QUERY_PARAM 을 보면
isSubDelimeter 에 '+' 가 있어서 이걸 통과해버린다.

https://github.com/spring-projects/spring-framework/issues/21259

이와 관련된 깃헙 이슈를 찾을수 있었는데, spring 4.3.5 까지는 + 문자를 인코딩 해줬지만 5.0.5 부터는 안해주는 것으로 바뀌었다.
그 이유는 + 문자가 sub-delims 에 속해서 라고 하는데 이건 rfc 문서를 더 자세히 살펴봐야할듯 하다 (https://www.ietf.org/rfc/rfc3986.txt)

The commit references #19394 which also references the RFC3986 stating that '+' is a valid URL query parameter value and should not be encoded.
(sub-delims 는 valid 한 URL query parameter 이기 때문에 encode 되지 않는다고 한다)

인코딩이 되게 하려면 다음과 같이 해야한다.

 uriBuilder
    .path("/search")
    .queryParam("keyword", "{keyword}")
    ...
    .build(request.getKeyword())

이렇게 하면 가능한 이유는 QUERY_PARAM 이 아닌 URI 로 간주되어 위와 다른 경로를 타게된다.

URI {
    @Override
    public boolean isAllowed(int c) {
        return isUnreserved(c);
    }
};

이 부분을 타게 되고, sub-delims 라도 인코딩이 되게 된다.

'java' 카테고리의 다른 글

java subList memory leak  (0) 2023.02.20
java mapping library MapStruct  (0) 2022.12.18