skip to content

Explain the WebSocket HTTP upgrade handshake and how you hook into it in Spring (HandshakeInterceptor).

level: seniorimportance: should knowfreq 40%

answer

  1. GET + Upgrade: websocket + Sec-WebSocket-Key -> 101 Switching Protocols
  2. Sec-WebSocket-Accept = SHA1(key + fixed GUID)
  3. HandshakeInterceptor.beforeHandshake -> attributes -> session.getAttributes()
  4. return false to reject; authenticate here
  5. proxies must forward Upgrade/Connection headers

basics

~20 s

A WebSocket connection starts as an HTTP GET with Upgrade: websocket and Connection: Upgrade headers plus a Sec-WebSocket-Key. The server replies 101 Switching Protocols, after which the same TCP socket carries WebSocket frames. In Spring you hook this with a HandshakeInterceptor to inspect/authorize the request and pass attributes into the session.

solid answer

~50 s

WebSocket reuses the HTTP port and starts with a normal HTTP/1.1 GET carrying `Upgrade: websocket`, `Connection: Upgrade`, `Sec-WebSocket-Version: 13`, and a random `Sec-WebSocket-Key`. If the server accepts, it responds **101 Switching Protocols** with `Sec-WebSocket-Accept` (a hash of the key + a fixed GUID) and, from then on, the same TCP connection carries binary WebSocket frames instead of HTTP. Spring performs this via a `HandshakeHandler` (default `DefaultHandshakeHandler`) and lets you intercept it with a `HandshakeInterceptor`: `beforeHandshake(request, response, handler, attributes)` runs before the upgrade — return `false` to reject (e.g. auth failure, set a 4xx status), or populate the `attributes` map, which becomes `session.getAttributes()`. `afterHandshake(...)` runs after. `HttpSessionHandshakeInterceptor` is a built-in that copies HTTP session data across. This is the natural place to authenticate, since the handshake is the last time you have full HTTP request context (headers, cookies).

code

java · 28 lines
java
public class AuthHandshakeInterceptor implements HandshakeInterceptor {

    private final TokenService tokens;
    public AuthHandshakeInterceptor(TokenService tokens) { this.tokens = tokens; }

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler handler, Map<String, Object> attributes) {
        // Full HTTP context still available here.
        String token = request.getURI().getQuery(); // e.g. ?token=...
        var user = tokens.verify(token);
        if (user == null) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return false; // aborts the upgrade
        }
        attributes.put("userId", user.id()); // -> session.getAttributes().get("userId")
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                              WebSocketHandler handler, Exception ex) { /* no-op */ }
}

// registration:
// registry.addHandler(chatHandler, "/ws/chat")
//         .addInterceptors(new AuthHandshakeInterceptor(tokenService))
//         .setAllowedOrigins("https://app.example.com");

go deeper

for a junior

Know it's an HTTP GET that upgrades to 101 Switching Protocols and stays on the same connection.

for a middle

Explain the Upgrade/Connection/Sec-WebSocket-Key headers and that HandshakeInterceptor.beforeHandshake lets you inspect the request and set session attributes.

for a senior

Tie the handshake to authentication (last HTTP context), attribute passing, allowed-origins as the security boundary, and rejecting with a status.

for a principal

Cover infra realities (proxy/LB Upgrade forwarding, idle timeouts, SockJS fallback), Sec-WebSocket-Accept semantics, and browser handshake header limitations shaping the auth design.

## The handshake, wire-level WebSocket doesn't use a new port — it **upgrades** an existing HTTP connection: 1. **Client request** — an HTTP/1.1 `GET /ws/chat` with headers: - `Upgrade: websocket` - `Connection: Upgrade` - `Sec-WebSocket-Version: 13` - `Sec-WebSocket-Key: <base64 random 16 bytes>` (not security — just proves the peer understands the protocol) - optionally `Sec-WebSocket-Protocol: <subprotocol>` and `Origin`. 2. **Server response** — if it agrees: - status **`101 Switching Protocols`** - `Upgrade: websocket`, `Connection: Upgrade` - `Sec-WebSocket-Accept: <base64( SHA-1( key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) )>` — the fixed GUID is from the RFC; this proves the server spoke WebSocket rather than blindly echoing. 3. After the 101, **the same TCP socket stops being HTTP** and carries WebSocket frames (text/binary/ping/pong/close) full-duplex. If the server declines, it returns a normal HTTP status (e.g. 403/404) and no upgrade happens. ## Where Spring plugs in - **`HandshakeHandler`** (default `DefaultHandshakeHandler`, backed by a server-specific `RequestUpgradeStrategy`) performs the actual upgrade and can negotiate the subprotocol and determine the `Principal`. - **`HandshakeInterceptor`** is your hook around it. Register with `.addInterceptors(...)` on the handler registration. Two methods: - `boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String,Object> attributes)` — runs **before** the upgrade. You still have the full HTTP request: headers, cookies, query params, the HTTP session. Return `false` to abort the handshake (optionally set `response.setStatusCode(...)`); return `true` to proceed. Anything you put into `attributes` becomes the session's attribute map (`session.getAttributes()`), so this is how per-connection data (user id, tenant, subprotocol choice) crosses from HTTP into the WebSocket world. - `void afterHandshake(...)` — runs after the response is (about to be) written. - **`HttpSessionHandshakeInterceptor`** — built-in; copies selected `HttpSession` attributes (and optionally the session id) into the handshake attributes. ## Why the handshake is the auth checkpoint Once upgraded, there is **no per-message HTTP request** — no new cookies/headers per frame. So the handshake (an ordinary HTTP request) is where you authenticate/authorize: read the JWT cookie/`Authorization` header, validate it, and stash the resulting principal in `attributes`. Spring Security can secure the handshake URL like any other endpoint. (Browsers can't set custom headers on the native WebSocket handshake, so token-in-cookie or a query param — or a first-message auth for STOMP — are common patterns.) ## Gotchas & edge cases - **Origin checks matter**: the browser sends `Origin`; Spring's default allowed-origins (same-origin) is your CSRF-equivalent defense for WebSocket. Native WebSocket requests are not subject to CORS the same way XHR is, so don't rely on CORS. - **Proxies/load balancers** must be configured to pass `Upgrade`/`Connection` headers and support long-lived connections, or the 101 never completes (a classic "works locally, breaks behind nginx" bug). This is a reason to offer SockJS fallback. - **Rejecting**: returning `false` from `beforeHandshake` without setting a status yields a generic failure; set an explicit status for clients. - **Attributes vs fields**: attributes are the safe per-session store; handler fields are shared singletons. - **Sec-WebSocket-Key is not authentication** — a frequent misconception; it only guards against caches/non-WS servers. - **Timeouts**: idle WebSocket connections may be closed by infra; heartbeats/ping-pong keep them alive.

  • Why is authentication usually done at the handshake rather than per message?
    After the 101 upgrade there are no further HTTP requests — frames carry no cookies or headers. The handshake is the last point with full HTTP context (cookies, Authorization header, HTTP session), so you authenticate there and store the principal/user in session attributes for the connection's lifetime.
  • What HTTP status code signals a successful WebSocket upgrade, and what proves the server understood the protocol?
    101 Switching Protocols. The server proves it by returning Sec-WebSocket-Accept, which is base64(SHA-1(clientKey + the RFC-defined GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)); a naive echo server couldn't produce it.

saying these in an interview costs you the question

  • Thinking WebSocket uses a different port than HTTP
  • Believing Sec-WebSocket-Key is an authentication/security token
  • Trying to authenticate per-frame instead of at the handshake
  • Forgetting proxies must forward Upgrade/Connection headers
  • Relying on CORS rather than allowed-origins for WebSocket

context