Incorrect response(401) when using X-HTTP-Method-Override

我們使用Spring的Rest,先前為了支援X-HTTP-Method-Override,在已存在的Filter做了此功能。某天,同事發現使用apache client api操作我們的API發生了401錯誤:

		
{"code":401,"message":"Incorrect response","links":[{"rel":"more info","href":"http://192.168.1.110:8080/TestWeb/api/documents"}]}

要發生這個問題有兩個條件,

  1. 使用Digest認證
  2. 使用X-HTTP-Method-Override

因此在我有空時,就開始追蹤這問題。

Sprint Security提供了DigestAuthentationFilter負責處理Digest認證。在使用者發出第一次請求後,Spring Security在察覺未經過驗證的情況下,會透過AuthenticationEntryPoint送出請求認證資訊。而為了在認證失敗時,能夠輸出xml或json格式的錯誤訊息(可參考上方),我們extend了DigestAuthenticationEntryPoint:

public class CustomziedDigestAuthenticationEntryPoint extends DigestAuthenticationEntryPoint {
 
	private static final Log logger = LogFactory.getLog(JsonDigestAuthenticationEntryPoint.class);
 
	@Autowired
	private MessageProcessor mMessageProcessor;
 
	@Override
	public void commence(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException authException)
			throws IOException, ServletException {
 
		// ... skip
 
		httpResponse.addHeader("WWW-Authenticate", authenticateHeader);
 
		try {
			ErrorInfo erroInfo = ErrorResponseEntityCreator.createErrorInfo(request, HttpStatus.UNAUTHORIZED.value(),
					authException.getMessage());
 
			response.setStatus(HttpStatus.UNAUTHORIZED.value());
			mMessageProcessor.handle(erroInfo, request, response);
		} catch (Exception e) {
			throw new ServletException(e);
		}
	}
 
}

在這裡的authException,就是我們所看到的Incorrect Response。於上我就往上trace,發現DigestAuthentationFilter會透過request method去算reponse值。

仔細看一下Digest認證流程,其中吸引我目光的是:

HA1=MD5(username:realm:password)
HA2=MD5(method:digestURI)
response=MD5(HA1:nonce:HA2)

接著套入我們的情境:

  1. Client收到Server認證請求。
  2. Client對Server發送認證內容,其中包含response值。
  3. Server的RestHeaderFilter首先處理X-HTTP-Method-Override請求,將Request method由Post改為Delete。
  4. Server的DigestAuthenticationFilter發現response不相同。
  5. Server回應錯誤訊息給Client。

這問題就在於: Client使用POST當method去計算HA2;而Server由於經過Request method的取代,會用DELETE去計算HA2。

那調整一下順序不就好了嗎? 的確,在我將RestHeaderFilter設定在Security相關Filter後,請求就可以正常處理了。但其實我們的RestHeaderFilter,還身兼將Rest API版本資訊塞到Header的責任。因此最後調整的順序為:

  1. RestHeaderFilter
  2. SpringSecurityFilter
  3. HttpMethodOverrideFilter
public class HttpMethodOverrideFilter extends OncePerRequestFilter implements Filter {
	private static final Logger logger = LoggerFactory.getLogger(HttpMethodOverrideFilter.class);
	protected static final String X_HTTP_METHOD_OVERRIDE_HEADER = "X-HTTP-Method-Override";
 
	@Override
	protected void doFilterInternal(HttpServletRequest aRequest, HttpServletResponse aResponse, FilterChain aFilterChain)
			throws ServletException, IOException {
 
		String methodOverrideValue = aRequest.getHeader(X_HTTP_METHOD_OVERRIDE_HEADER);
		if (methodOverrideValue != null && !methodOverrideValue.isEmpty()) {
			String overrideMethod = methodOverrideValue.toUpperCase(Locale.ENGLISH);				
			aRequest = new HttpMethodRequestWrapper(aRequest, overrideMethod);
		}
		aFilterChain.doFilter(aRequest, aResponse);
	}
 
	private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
		private final String method;
 
		public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
			super(request);
			this.method = method;
		}
 
		@Override
		public String getMethod() {
			return this.method;
		}
	}
}

如果有使用PostMan,可以仔細看看選擇不同的HTTP method,是否會產生不同的digest response。