- Spring
Spring BootのWebSocket|STOMPでチャットを作る完成コード
Spring Boot で WebSocket のチャットを作るなら、spring-boot-starter-websocket と STOMP(WebSocket の上でやりとりの形を決めるテキストの規約)を使うのが近道です。 サーバーは @EnableWebSocketMessageBroker の設定クラスと @MessageMapping のコントローラ、ブラウザは @stomp/stompjs の Client で書けます。
| 役割 | サーバー(Spring) | ブラウザ(@stomp/stompjs) |
|---|---|---|
| 接続口 | registry.addEndpoint("/ws") |
brokerURL: 'ws://…/ws' |
| 送る | — | client.publish({ destination: '/app/chat', … }) |
| 受け取って処理 | @MessageMapping("/chat") |
— |
| 全員に配る | @SendTo("/topic/messages") か SimpMessagingTemplate |
client.subscribe('/topic/messages', …) |
| 1人に送る | convertAndSendToUser(名前, "/queue/notice", …) |
client.subscribe('/user/queue/notice', …) |
確認環境:Spring Boot 4.1.1・Spring Framework 7.0.9・Spring Security 7.1.1。設定の既定値(メッセージの大きさの上限・ハートビート・外部ブローカーの接続先)、ハンドシェイクで 403 になる条件、CSRF のエラー文は各 GitHub タグのソースで確かめました。ブラウザ側は @stomp/stompjs 7.4.0 を Node.js 22.22.2(ws 8.22.0)と Chromium で動かし、STOMP の通信をそのまま記録する小さなサーバーに接続して、送られるフレーム・403 のときの見え方・ERROR フレームの受け取り方を確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring BootのWebSocket|STOMPでチャットを作る完成コード」
しくみ:宛先の3種類
STOMP では、メッセージに「宛先(destination)」を付けて送ります。Spring は宛先の頭の文字で、行き先を分けます。
| 宛先の頭 | 行き先 | 例 |
|---|---|---|
/app(setApplicationDestinationPrefixes) |
自分のコントローラの @MessageMapping |
/app/chat → @MessageMapping("/chat") |
/topic・/queue(enableSimpleBroker) |
ブローカー(購読している全員に配る係) | /topic/messages |
/user(setUserDestinationPrefix) |
ログインしているその人だけへの宛先に変換 | /user/queue/notice |
流れは「ブラウザが /app/chat に送る → コントローラが受け取って加工 → /topic/messages に送る → ブローカーが購読者全員に配る」です。
完成コード(サーバー)
依存
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.boot:spring-boot-starter-security") // 認証する場合
implementation("org.springframework.security:spring-security-messaging") // 認証する場合
}
設定クラス
package com.example.chat;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
// 別のオリジン(開発中の Vite など)から接続するとき。既定は同じオリジンだけ
.setAllowedOriginPatterns("http://localhost:5173", "https://*.example.com");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
var scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
scheduler.setThreadNamePrefix("ws-heartbeat-");
scheduler.initialize();
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/queue")
.setTaskScheduler(scheduler); // これでハートビートが 10000,10000 になる
registry.setUserDestinationPrefix("/user"); // 既定も "/user"
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(64 * 1024) // 受け取る1メッセージの上限(既定 64KB)
.setSendTimeLimit(10 * 1000) // 1人への送信にかけてよい時間(既定 10秒)
.setSendBufferSizeLimit(512 * 1024); // 1人への送信待ちのバッファ(既定 512KB)
}
}
Spring Framework 7.0.9 のソースで確かめた既定値です。
| 項目 | 既定値 | 決めている場所 |
|---|---|---|
| 受け取る STOMP メッセージの上限 | 64KB(64 * 1024) |
StompSubProtocolHandler |
| 1つの接続への送信にかけてよい時間 | 10秒 | SubProtocolWebSocketHandler |
| 1つの接続への送信待ちのバッファ | 512KB | 同上 |
| 接続してから最初のメッセージを待つ時間 | 60秒 | 同上 |
| シンプルブローカーのハートビート | TaskScheduler を設定したときだけ 10000,10000(無ければ送らない) |
SimpleBrokerMessageHandler |
| ユーザー宛ての頭 | /user |
MessageBrokerRegistry |
| 受け付ける STOMP の版 | 1.2・1.1・1.0 | StompSubProtocolHandler |
送信の時間・バッファの上限を超えると、遅いクライアントの接続は閉じられます。1人の遅いブラウザのせいで、ほかの人への配信が止まるのを防ぐしくみです。
メッセージの型とコントローラ
package com.example.chat;
import java.time.Instant;
public record ChatIn(String text) {}
public record ChatOut(String from, String text, Instant at) {}
package com.example.chat;
import java.security.Principal;
import java.time.Instant;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
// ブラウザが /app/chat に送る → 戻り値が /topic/messages の購読者全員に届く
@MessageMapping("/chat")
@SendTo("/topic/messages")
public ChatOut chat(ChatIn in, Principal principal) {
if (in.text() == null || in.text().isBlank() || in.text().length() > 500) {
throw new IllegalArgumentException("メッセージは1〜500文字です");
}
// 送り主の名前は、ブラウザが送ってきた値ではなく、認証した Principal から取る
String from = (principal != null) ? principal.getName() : "guest";
return new ChatOut(from, in.text(), Instant.now());
}
// 例外は、送った本人の /user/queue/errors にだけ返す
@MessageExceptionHandler
@SendToUser("/queue/errors")
public String handle(IllegalArgumentException ex) {
return ex.getMessage();
}
}
@MessageMapping のメソッドは、@Payload(本文。注釈は省略できる)、Principal、@Header、@DestinationVariable(宛先の {roomId} の部分)などを引数に取れます。
@MessageMapping("/rooms/{roomId}/chat")
@SendTo("/topic/rooms/{roomId}")
public ChatOut roomChat(@DestinationVariable String roomId, ChatIn in, Principal principal) { … }
コントローラの外から送る:SimpMessagingTemplate
REST API の処理や、バッチ・イベントの中から送るときは SimpMessagingTemplate を使います。
package com.example.chat;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@Service
public class NoticeService {
private final SimpMessagingTemplate template;
public NoticeService(SimpMessagingTemplate template) {
this.template = template;
}
// 全員へ
public void broadcast(String text) {
template.convertAndSend("/topic/messages", new ChatOut("system", text, java.time.Instant.now()));
}
// 1人へ:ブラウザは /user/queue/notice を購読する
public void notifyUser(String username, String text) {
template.convertAndSendToUser(username, "/queue/notice", text);
}
}
convertAndSendToUser の第1引数は、ログインしている人の名前(Principal.getName())です。 認証していない接続には Principal が無いので、ユーザー宛てのメッセージは届きません。
接続・切断を知りたいときは、SessionConnectedEvent・SessionDisconnectEvent を @EventListener で受け取ります。
@Component
public class PresenceListener {
@EventListener
public void onDisconnect(SessionDisconnectEvent e) {
var user = e.getUser();
System.out.println("disconnect: " + (user != null ? user.getName() : e.getSessionId()));
}
}
完成コード(ブラウザ側)
@stomp/stompjs(7.4.0)を使います。
npm install @stomp/stompjs
<!-- src/main/resources/static/index.html -->
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>チャット</title></head>
<body>
<ul id="log"></ul>
<form id="f"><input id="text" autocomplete="off"><button>送信</button></form>
<p id="status">接続中…</p>
<script type="module" src="/app.js"></script>
</body>
</html>
// src/main/resources/static/app.js(バンドラーを使わない場合は CDN の ESM 版を import する)
import { Client } from 'https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.4.0/+esm';
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
const client = new Client({
brokerURL: `${scheme}://${location.host}/ws`,
reconnectDelay: 5000, // 切れたら5秒後につなぎ直す(既定 5000)
heartbeatIncoming: 10000, // 既定 10000
heartbeatOutgoing: 10000, // 既定 10000
// connectHeaders: { 'X-CSRF-TOKEN': token }, // Spring Security の CSRF を使うとき(下で説明)
onConnect: () => {
document.getElementById('status').textContent = '接続しました';
client.subscribe('/topic/messages', (msg) => {
const m = JSON.parse(msg.body);
const li = document.createElement('li');
li.textContent = `${m.from}: ${m.text}`; // innerHTML は使わない(XSS 対策)
document.getElementById('log').append(li);
});
client.subscribe('/user/queue/errors', (msg) => alert(msg.body));
},
onStompError: (frame) => {
console.error('STOMP ERROR:', frame.headers['message'], frame.body);
},
onWebSocketClose: (evt) => {
document.getElementById('status').textContent = `切断(code=${evt.code})。再接続します…`;
},
});
client.activate();
document.getElementById('f').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('text');
if (!client.connected) return;
client.publish({ destination: '/app/chat', body: JSON.stringify({ text: input.value }) });
input.value = '';
});
@stomp/stompjs 7.4.0 のソースで確かめた既定値は、reconnectDelay が 5000ms、heartbeatIncoming・heartbeatOutgoing が 10000ms、connectionTimeout が 0(時間切れなし)、splitLargeFrames が false です。
実際に流れるフレーム
上のクライアントを Node.js で動かし、STOMP の通信をそのまま記録するサーバーにつないだときの送受信です(\n は改行、本文は省略)。
>>> CONNECT\naccept-version:1.2,1.1,1.0\nheart-beat:10000,10000\n\n
<<< CONNECTED\nuser-name:taro\nheart-beat:10000,10000\nversion:1.2\ncontent-length:0\n\n
>>> SUBSCRIBE\nid:sub-0\ndestination:/topic/messages\n\n
>>> SEND\ndestination:/app/chat\ncontent-length:26\n\n
<<< MESSAGE\nmessage-id:m-1\nsubscription:sub-0\ncontent-type:application/json\ndestination:/topic/messages\ncontent-length:40\n\n
>>> DISCONNECT\nreceipt:close-1\n\n
<<< RECEIPT\nreceipt-id:close-1\ncontent-length:0\n\n
サーバーが受け取った生の SEND フレームは次のとおりで、最後に \0(NULL 文字)が付いていました。
SEND\ndestination:/app/chat\ncontent-length:26\n\n{"text":"こんにちは"}\u0000
content-length は文字数ではなくバイト数です。 {"text":"こんにちは"} は11文字+ひらがな5文字ですが、UTF-8 ではひらがなが1文字3バイトなので 26 になりました。メッセージの上限(既定 64KB)もバイト数で数えます。ハンドシェイクでは、ws のサブプロトコルとして v12.stomp が選ばれました。
認証:Spring Security と組み合わせる
1. ハンドシェイク(HTTP)を守る
WebSocket の接続は、最初に HTTP のリクエスト(ハンドシェイク)で始まります。ふつうの画面と同じ SecurityFilterChain で /ws を守ると、ログイン済みのセッション Cookie を持つ接続だけが通ります。その人の Principal が、STOMP のメッセージにも引き継がれます。
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**").permitAll()
.requestMatchers("/ws/**").authenticated()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
ログインの作り方は、関連記事「Spring Security入門|SecurityFilterChainとDB認証」で扱っています。
2. STOMP のメッセージを守る:@EnableWebSocketSecurity
package com.example.chat;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.socket.EnableWebSocketSecurity;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
return messages
.nullDestMatcher().authenticated() // CONNECT・DISCONNECT など
.simpTypeMatchers(SimpMessageType.DISCONNECT, SimpMessageType.HEARTBEAT).permitAll()
.simpDestMatchers("/app/**").authenticated() // 送信
.simpSubscribeDestMatchers("/topic/**", "/user/queue/**").authenticated() // 購読
.simpDestMatchers("/topic/**", "/queue/**").denyAll() // ブローカーへの直接送信は禁止
.anyMessage().denyAll()
.build();
}
}
Spring Security 7.1.1 のソースでは、@EnableWebSocketSecurity を付けて AuthorizationManager<Message<?>> の Bean を定義しないと、すべてのメッセージに認証を求める設定(anyMessage().authenticated())になります。上のように、ブラウザから /topic/** へ直接送ることを禁止しておくと、コントローラを通らない書き込み(なりすましの送信)を防げます。
3. CSRF:CONNECT に X-CSRF-TOKEN を付ける
@EnableWebSocketSecurity は、STOMP の CONNECT に CSRF トークンを求めます(Spring Security 7.1.1 の XorCsrfChannelInterceptor)。ハンドシェイクのときに HTTP セッションの CSRF トークンを WebSocket のセッションへ写し、CONNECT のヘッダーの値と比べます。足りないと、ソースでは次の例外になります。
| 状況 | 例外とメッセージ |
|---|---|
| ハンドシェイクのセッションに CSRF トークンが無い | MissingCsrfTokenException:Could not verify the provided CSRF token because no token was found to compare. |
| CONNECT のヘッダーの値が違う・無い | InvalidCsrfTokenException:Invalid CSRF Token '…' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'. |
画面のテンプレートやエンドポイントからトークンを渡し、connectHeaders に入れます。
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken token) { // {"token": "...", "headerName": "X-CSRF-TOKEN", ...}
return token;
}
}
const csrf = await fetch('/csrf').then((r) => r.json());
const client = new Client({
brokerURL: `${scheme}://${location.host}/ws`,
connectHeaders: { [csrf.headerName]: csrf.token },
// …
});
CSRF のしくみと 403 の切り分けは、関連記事「Spring Securityで403になる原因|POSTだけ失敗する時」で扱っています。
トークン(JWT)で認証する場合
SPA から JWT で認証する場合、ブラウザの WebSocket はハンドシェイクに Authorization ヘッダーを付けられません。そのため、STOMP の CONNECT のヘッダーにトークンを入れ、サーバー側の ChannelInterceptor で検証して setUser します。
@Configuration
@EnableWebSocketMessageBroker
public class JwtStompConfig implements WebSocketMessageBrokerConfigurer {
private final JwtDecoder jwtDecoder;
private final JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
public JwtStompConfig(JwtDecoder jwtDecoder) { this.jwtDecoder = jwtDecoder; }
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor acc = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (acc != null && StompCommand.CONNECT.equals(acc.getCommand())) {
String auth = acc.getFirstNativeHeader("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
throw new AccessDeniedException("missing token");
}
Jwt jwt = jwtDecoder.decode(auth.substring(7));
acc.setUser(converter.convert(jwt)); // 以降のメッセージの Principal になる
}
return message;
}
});
}
// registerStompEndpoints・configureMessageBroker は上と同じ
}
この形では、セッション Cookie を使わないので CSRF の確認は要りません。@EnableWebSocketSecurity と組み合わせる場合は、CSRF の ChannelInterceptor を csrfChannelInterceptor という名前の Bean で差し替えられます(7.1.1 のソースで、この名前の Bean があれば既定のかわりに使う)。JWT の検証は、関連記事「Spring Security JWT認証の実装|Boot 4対応の設定コード全文」で扱っています。
スケールアウト:外部ブローカー(STOMP ブローカーリレー)
enableSimpleBroker のシンプルブローカーは、そのサーバーのメモリの中で購読者を覚えています。サーバーを2台にすると、A に接続した人のメッセージは、B に接続した人に届きません。複数台にするなら、RabbitMQ・ActiveMQ などの STOMP 対応ブローカーに中継させます。
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("rabbitmq.internal")
.setRelayPort(61613)
.setClientLogin("chat-app")
.setClientPasscode(System.getenv("STOMP_PASSWORD"))
.setSystemLogin("chat-app")
.setSystemPasscode(System.getenv("STOMP_PASSWORD"));
}
| 設定 | 既定値(v7.0.9 のソース) | 意味 |
|---|---|---|
relayHost |
127.0.0.1 |
ブローカーのホスト |
relayPort |
61613 |
STOMP のポート |
clientLogin / clientPasscode |
guest / guest |
利用者ごとの接続に使う ID |
systemLogin / systemPasscode |
guest / guest |
サーバー自身の共有接続に使う ID |
systemHeartbeatSendInterval / ReceiveInterval |
10000ms / 10000ms | 共有接続のハートビート |
既定の guest は、RabbitMQ では localhost 以外から使えない設定が一般的です。 専用のユーザーを作ります。ブローカーリレーは TCP の接続に Reactor Netty を使うので、io.projectreactor.netty:reactor-netty を依存に足します。RabbitMQ 側では STOMP プラグイン(rabbitmq_stomp)を有効にします。宛先の書き方(/topic/…・/queue/…)はブローカーごとに意味が違うので、ブローカーのドキュメントで確かめます。
| 方法 | 複数台 | 向いている場面 |
|---|---|---|
| シンプルブローカー | × | 1台で動かす・開発中 |
| STOMP ブローカーリレー(RabbitMQ など) | ○ | 本番で台数を増やす |
| シンプルブローカー+Redis の Pub/Sub で配り直す | ○(自作) | すでに Redis がある |
ロードバランサーや nginx を前に置くときは、Upgrade・Connection ヘッダーを転送し、アイドルのタイムアウトをハートビートの間隔(10秒)より長くします。nginx の設定は、関連記事「Node.js WebSocket実装|ws・再接続・認証・nginxまで」で扱っています。
よくあるエラー
ハンドシェイクで 403
ブラウザのコンソールに出る文と、onclose の値です(Chromium で確認)。
WebSocket connection to 'ws://localhost:18081/ws' failed: Error during WebSocket handshake: Unexpected response code: 403
onerror type=error
onclose code=1006 wasClean=false reason=""
ブラウザの JavaScript には「403 だった」ことは渡されず、code=1006(異常な切断)としか分かりません。 理由はサーバーのログで見ます。@stomp/stompjs を Node.js で使った場合は Unexpected server response: 403 と出ました。
| 原因 | サーバー側で起きていること | 直し方 |
|---|---|---|
| オリジンが許可されていない | OriginHandshakeInterceptor が 403 を返す。DEBUG ログに Handshake request rejected, Origin header value … not allowed |
setAllowedOriginPatterns(…) に画面のオリジンを足す |
Spring Security で /ws が未認証 |
ログインしていない、Cookie が送られていない | ログインする・permitAll の範囲を見直す |
| ほかのオリジンの画面から Cookie が届かない | SameSite の制限で Cookie が付かない |
同じオリジンに置く・トークン認証にする |
Spring Framework 7.0.9 のソースでは、オリジンの拒否は DEBUG レベルでしかログに出ません。 403 の理由が分からないときは、logging.level.org.springframework.web.socket=DEBUG にします。setAllowedOriginPatterns("*") にするとどのサイトの画面からでも接続できてしまうので、本番では具体的なオリジンを書きます。CORS 全般は、関連記事「Spring Boot CORS設定|3つの書き方とSecurity併用のコード全文」で扱っています。
ハンドシェイクのそのほかの失敗(v7.0.9 のソース)
| HTTP の状態 | DEBUG ログ・理由 | よくある原因 |
|---|---|---|
| 400 | Handshake failed due to invalid Upgrade header: … |
プロキシが Upgrade ヘッダーを転送していない |
| 400 | Handshake failed due to invalid Connection header… |
同上(Connection: upgrade) |
| 400 | Missing "Sec-WebSocket-Key" header |
WebSocket ではない普通の HTTP で開いた |
| 405 | Handshake failed due to unexpected HTTP method: … |
GET 以外で呼んだ |
| 426 | Handshake failed due to unsupported WebSocket version: … |
WebSocket の版が合わないクライアント |
| 404 | — | addEndpoint のパスと URL が違う・コンテキストパスの付け忘れ |
接続後のエラー
| 症状 | 原因 | 直し方 |
|---|---|---|
onStompError に STOMP 'content-length' header value … exceeds configured buffer size limit … |
メッセージが上限(既定 64KB)を超えた | setMessageSizeLimit を上げる・画像は HTTP で送る |
| 送ったのに誰にも届かない | 宛先の頭が違う(/app を付けずに /chat に送った等) |
宛先の表を見直す |
@SendTo を付けたのに届かない |
購読している宛先と @SendTo の宛先が違う |
ブラウザの subscribe と合わせる |
convertAndSendToUser が届かない |
接続に Principal が無い(未認証) |
ハンドシェイクか CONNECT で認証する |
| CONNECT のあとすぐ切れる | CSRF トークンが無い・違う | connectHeaders に X-CSRF-TOKEN |
| 数十秒〜数分で切れる | プロキシのアイドルタイムアウト・ハートビートが無い | シンプルブローカーに TaskScheduler、プロキシのタイムアウトを延ばす |
| 2台目のサーバーの人に届かない | シンプルブローカーはサーバーごと | ブローカーリレーにする |
@stomp/stompjs を記録用のサーバーにつないで、上限を超えたメッセージに ERROR フレームを返させたときの受け取り方です。
<<< ERROR\ncontent-length:0\nmessage:STOMP 'content-length' header value 211 exceeds configured buffer size limit 100\ncontent-length:0\n\n
onStompError message=STOMP 'content-length' header value 211 exceeds configured buffer size limit 100
onWebSocketClose code=1005
エラーの理由は message ヘッダーに入ります(frame.headers['message'])。Spring の StompSubProtocolHandler も、例外のメッセージを ERROR フレームの message ヘッダーに入れて送ります(v7.0.9 のソース)。
よくある質問
Q. SockJS は使いますか。
A. Spring Framework 7.0.9 にも SockJS のサポート(withSockJS())は残っています。WebSocket が通らない環境(WebSocket に対応していないプロキシなど)向けの代わりの通信です。いまの主なブラウザは WebSocket に対応しているので、この記事では素の WebSocket と @stomp/stompjs を使っています。
Q. STOMP を使わず、素の WebSocket で書けますか。
A. WebSocketHandler(TextWebSocketHandler)と @EnableWebSocket で書けます。宛先・購読・ユーザー宛てなどを自分で作ることになるので、チャットのように「誰に配るか」を扱うなら STOMP のほうが短く書けます。
Q. WebSocket と Server-Sent Events(SSE)はどちらを使いますか。
A. サーバーからの通知だけなら SSE(SseEmitter)のほうが単純です。ブラウザからも頻繁に送るチャット・共同編集なら WebSocket です。
まとめ
- 依存は
spring-boot-starter-websocket、設定は@EnableWebSocketMessageBroker - 宛先は
/app→ コントローラ、/topic・/queue→ ブローカー、/user→ その人だけ - ブラウザは
@stomp/stompjsのClient。content-lengthはバイト数で、上限は既定 64KB - シンプルブローカーのハートビートは
TaskSchedulerを設定したときだけ - 認証はハンドシェイクのセッション、または CONNECT のヘッダー+
ChannelInterceptor。@EnableWebSocketSecurityは CONNECT に CSRF トークンを求める - 送り主の名前は
Principalから取る。ブラウザの値を信じない - 複数台なら
enableStompBrokerRelay(RabbitMQ など) - 403 はブラウザでは
code=1006としか見えない。サーバーを DEBUG ログにしてOriginを確かめる
参考・出典
確認日はいずれも 2026年10月13日です。
- Spring Framework Reference「STOMP」:https://docs.spring.io/spring-framework/reference/web/websocket/stomp.html
- Spring Framework v7.0.9 ソース(
spring-websocket・spring-messaging):https://github.com/spring-projects/spring-framework/tree/v7.0.9 - Spring Security Reference「WebSocket Security」:https://docs.spring.io/spring-security/reference/servlet/integrations/websocket.html
- Spring Security 7.1.1 ソース(
spring-security-messaging・EnableWebSocketSecurity):https://github.com/spring-projects/spring-security/tree/7.1.1 - @stomp/stompjs(npm・7.4.0):https://www.npmjs.com/package/@stomp/stompjs
- STOMP Protocol Specification, Version 1.2:https://stomp.github.io/stomp-specification-1.2.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring BootのWebSocket|STOMPでチャットを作る完成コード」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。