1. Form Data
- Servlet API는 POST 요청에 대해서만 request.getParamaeter*() 사용 가능
FormContentFilter
- PUT, PATCH, DELETE 요청은 서블릿 스펙상으로는 바디 패싱을 하지 않음
- PUT, PATCH, DELETE 요청에서도 body를 파싱하여 getParamaeter*()로 접근 가능하게 함
- body를 application/x-www-form-urlencoded로 파싱함
- getParamaeter*()로 인해 FormData를 @RequestParam, @ModelAttribute에서 바인딩할 수 있음
예제) FormContentFilter 등록
더보기
더보기
더보기
public class MyWebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(DatabaseConfig.class);
ctx.register(WebConfig.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(ctx);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
FilterRegistration.Dynamic formContentFilter = servletContext.addFilter("formContentFilter", new FormContentFilter());
formContentFilter.addMappingForUrlPatterns(null, false, "/*");
}
}
2. Forwarded Headers
- 서버 앞에 프록시가 있는 경우, 실제 클라이언트의 정보를 직접 알 수 없음
- 이럴 경우 Forwarded or X-Forwarded 헤더를 통해 정보를 전달받음
Forwarded (표준 헤더)
- RFC 7239
- 원 요청의 host, post, schema 정보를 전달함
X-Forwarded (비표준 헤더)
Header | 설명 |
X-Forwarded-Host | 원래 요청의 host |
X-Forwarded-Port | 원래 요청의 port |
X-Forwarded-Proto |
원래 요청의 프로토콜 (http/https)
|
X-Forwarded-Ssl |
원래 요청이 HTTPS였는지 (on/off)
|
X-Forwarded-Prefix | 원래 URL 경로 prefix |
ForwardedHeaderFilter
- Forwarded, X-Forwarded-* 헤더를 읽어서 HttpServletRequest의 host, post, schema 값을 수정해주는 필터
- 옵션 설정에 따라 해당 헤더 제거 가능
구분 | 내용 |
보안 설정 |
removeOnly=true 설정 시, 헤더 제거
|
주의사항 |
신뢰 경계에서는 외부 클라이언트가 Forwarded 헤더를 위조할 수 있으므로 제거하는 것이 안전함 (예: 프록시)
|
DispatcherType |
REQUEST, ASYNC, ERROR 타입 모두에 적용해야 함
⇒ 비동기/에러 상황에서도 헤더 정보가 일관되게 적용되기 위함 |
예시) ForwardedHeaderFilter 등록
더보기
더보기
더보기
public class MyWebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(DatabaseConfig.class);
ctx.register(WebConfig.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(ctx);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
FilterRegistration.Dynamic forwardedHeaderFilter = servletContext.addFilter("forwardedHeaderFilter", new ForwardedHeaderFilter());
forwardedHeaderFilter.addMappingForUrlPatterns(null, false, "/*");
}
}
@GetMapping("/test")
@ResponseBody
public String test(HttpServletRequest request) {
String scheme = request.getScheme();
String remoteAddr = request.getRemoteAddr();
String host = request.getServerName();
return String.format("scheme: %s, remoteAddr: %s, host: %s", scheme, remoteAddr, host);
}
3. Shallow ETag
구분 | 내용 |
기능 |
- 응답 body 내용을 MD5 해시로 계산해 ETag 생성
- 클라이언트가 If-None-Match 헤더로 같은 ETag를 보내면 → 304 Not Modified 응답 반환 |
성능 특성 |
- 클라이언트는 중복 요청 시 응답 body를 생략받을 수 있어 네트워크 절약
- 하지만 서버는 항상 응답을 계산하므로 CPU 사용량은 줄지 않음 |
옵션 (writeWeakETag) |
- writeWeakETag=true 설정 시, 약한 ETag(W/"...") 형식 사용
→ 콘텐츠가 약간 달라도 동일한 것으로 간주 가능 (변경 무시 허용) |
DispatcherType 설정 |
- DispatcherType.ASYNC 포함하여 매핑 필요
→ 비동기 응답에서도 ETag 캐싱이 제대로 작동하게 함 |
예제) ETagFilter 등록
더보기
public class ETagFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
ETagResponseWrapper wrappedResponse = new ETagResponseWrapper(response);
chain.doFilter(request, wrappedResponse);
byte[] content = wrappedResponse.getContentAsBytes();
String etag = generateETag(content);
String clientETag = request.getHeader("If-None-Match");
if (etag.equals(clientETag)) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
} else {
response.setHeader("ETag", etag);
response.setContentLength(content.length);
response.getOutputStream().write(content);
}
}
private String generateETag(byte[] content) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(content);
return "\"" + Base64.getEncoder().encodeToString(hash) + "\"";
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class ETagResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private ServletOutputStream out = new WrappedOutputStream(buffer);
private PrintWriter writer = new PrintWriter(new OutputStreamWriter(buffer));
public ETagResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
@Override
public PrintWriter getWriter() throws IOException {
return writer;
}
public byte[] getContentAsBytes() {
try {
writer.flush();
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
return buffer.toByteArray();
}
private static class WrappedOutputStream extends ServletOutputStream {
private OutputStream stream;
public WrappedOutputStream(OutputStream stream) {
this.stream = stream;
}
@Override
public void write(int b) throws IOException {
stream.write(b);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
// no-op
}
}
}
public class MyWebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(DatabaseConfig.class);
ctx.register(WebConfig.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(ctx);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
// ...
FilterRegistration.Dynamic ETagHeaderFilter = servletContext.addFilter("eTagHeaderFilter", new ETagFilter());
ETagHeaderFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
}
}
예시 흐름
- 서버가 응답에 ETag: "abcd1234"를 담아 응답
- 클라이언트가 다음 요청 시 If-None-Match: "abcd1234" 헤더 포함
- 서버가 body를 계산해 해시 확인 → 동일하면 304 Not Modified 반환 (body 없음)
실습) curl 테스트
더보기
1. 첫번째 요청
curl -i http://localhost:8080/
// 첫번째 요청 응답 헤더
HTTP/1.1 200
ETag: "6rUuhDrhGxkVNvh9jflwh8AA9a4zg3TcvsYCTJw6fbw="
Content-Type: text/html;charset=UTF-8
Content-Language: ko-KR
Content-Length: 194
Date: Fri, 11 Apr 2025 09:38:03 GMT
Keep-Alive: timeout=20
Connection: keep-alive
2. 두번째 요청
curl -i -H "If-None-Match: \"6rUuhDrhGxkVNvh9jflwh8AA9a4zg3TcvsYCTJw6fbw=\"" http://localhost:8080/
HTTP/1.1 304
Date: Fri, 11 Apr 2025 09:38:34 GMT
Keep-Alive: timeout=20
Connection: keep-alive
4. CORS
- 브라우저에서 리소스 기반 요청 시, 다른 도메인이 내 서버 API에 접근할 수 있도록 허용하는 보안 매커니즘
동작 흐름
- 브라우저는 요청 헤더에 자동으로 Origin 헤더를 추가함 (프로토콜 + 도메인 + 포트)
- 서버는 이 Origin을 보고 클라이언트의 도메인을 확인
- CORS를 위반한 클라이언트는 자신의 요청이 CORS 허용과 관련한 응답 헤더에 부합하는지에 따라 응답 여부가 결정됨
예시) CorsFilter 등록
더보기
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
String origin = req.getHeader("Origin");
if (Objects.nonNull(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Methods", "*");
res.setHeader("Access-Control-Allow-Headers", "Authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
res.setStatus(HttpServletResponse.SC_OK);
return;
}
}
chain.doFilter(request, res);
}
}
- Access-Control-Allow-Headers는 * 사용 시 브라우저에 따라 동작 안할수도 있음
public class MyWebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(DatabaseConfig.class);
ctx.register(WebConfig.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(ctx);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
// ...
FilterRegistration.Dynamic corsFilter = servletContext.addFilter("corsFilter", new CorsFilter());
corsFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
}
}
CORS 관련 응답 헤더
헤더 이름 | 의미 | 주의사항 |
Access-Control-Allow-Origin | 요청을 허용할 Origin (출처) |
*와 Access-Control-Allow-Credentials: true
같이 쓰면 안 됨! |
Access-Control-Allow-Methods | 허용할 HTTP 메서드들 |
"*"는 브라우저에 따라 비표준
(정확한 메서드 명시 권장) |
Access-Control-Allow-Headers | 허용할 요청 헤더들 | "*"는 브라우저에 따라 비표준 (정확한 메서드 명시 권장) |
Access-Control-Allow-Credentials | 쿠키/인증정보 포함 허용 여부 |
기본적으로 허용되지 않음
이게 true면 Allow-Origin은 *이 되면 안 됨 |
Preflight 요청
- 실제 요청 전, 브라우저가 서버에 안전성 검사를 요청하는 단계
- OPTIONS 메서드로 요청함
발생 조건
분류 | 조건 | 예시 |
Request Method
|
PUT, DELETE, PATCH | 안전하지 않은 메서드 |
POST + 비표준 Content-Type |
application/json, applcation/xml, text/html 등
(application/x-www-form-urlencoded, multipart/form-data, text/plain 제외) |
|
Request Header
|
커스텀 헤더 사용 |
Authorization, X-Custom-*, X-Requested-With 등
|
인코딩 관련 헤더 |
Content-Encoding, Accept-Encoding
|
|
Response Type | blob, arrayBuffer 응답 사용 |
일부 브라우저에서 preflight 발생 가능
|
Fetch 설정 | credentials: "include" + Access-Control-Allow-Origin: * |
동시에 사용 시 CORS 오류 또는 preflight 발생
|
5. UrlHandlerFilter
- URL 끝에 슬래시(/)가 붙는지 여부를 처리하기 위한 기능
처리 방식
처리 방식 | 설명 | 예시 |
Redirect | 슬래시가 있는 URL을 없는 URL로 리디렉트 |
/blog/my-post/ → /blog/my-post (308 Permanent Redirect)
|
WrapRequest | 슬래시 여부에 상관없이 내부적으로 요청 경로를 처리 |
/admin/user/ → 내부적으로 /admin/user와 동일하게 처리
|
출처
'Spring > Spring MVC' 카테고리의 다른 글
[Spring MVC] 6. Error Response (0) | 2025.04.12 |
---|---|
[Spring MVC] 3. HTTP Message Conversion (0) | 2025.04.12 |
[Spring MVC] 4-3. Handler Methods: Controller Advice (0) | 2023.10.17 |
[Spring MVC] 5. @InitBinder (1) | 2023.10.17 |
[Spring MVC] 4-2. Handler Methods: Type Conversion (0) | 2023.10.17 |