Spring RSocket 마스터 노트 시리즈 6편. RSocket의 보안이 HTTP와 다른 점, Spring Security RSocket으로 인증·인가 구현, SETUP-time vs Per-request 인증의 결정적 차이, JWT 토큰 검증, 라우트별 권한 제어, TLS·mTLS 설정, ReactiveAuthenticationManager 통합까지.
이 글은 Spring RSocket 마스터 노트 시리즈의 여섯 번째 편입니다. 1~5편이 기능이었다면, 이번엔 그것을 안전하게 — 보안.
RSocket은 메시징 프로토콜이라 보안 모델이 HTTP와 살짝 다릅니다. SETUP 시 한 번 인증·매 메시지 검증·연결 단위 권한. Spring Security RSocket이 답.
처음 RSocket 보안이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, HTTP 보안과 다른 점이 막연합니다. JWT? OAuth? 둘째, SETUP 인증 vs Per-request 인증 차이가 헷갈립니다.
해결법은 한 가지예요. "SETUP = 한 번 인증 / Per-request = 매 메시지 인증" 한 줄. 일반 = SETUP만, 민감 정보 = 둘 다. 이 차이가 RSocket 보안의 핵심.
RSocket 보안 — 3 축
1. Authentication (인증) — 너 누구냐
2. Authorization (인가) — 뭘 할 수 있냐
3. Encryption (암호화) — TLS·mTLS
HTTP와 같은 3 축. 다만 구현 디테일이 다름.
SETUP 인증 vs Per-request 인증
SETUP 인증 — 한 번
Client → SETUP {auth: token} → Server
↓ 검증
연결 OK
↓ 이후 모든 메시지 같은 인증
- 한 번만 인증
- 빠름·효율적
- 일반 권장
Per-request 인증 — 매번
Request 1: 메타데이터에 토큰 → 검증
Request 2: 메타데이터에 토큰 → 검증
...
- 매 메시지 검증
- 느림
- 민감 작업·외부 호출에만
여기서 정말 중요한 시험 함정 — 둘 결합도 OK. SETUP 시 일반 인증, 민감 라우트만 Per-request 추가 검증.
Spring Security RSocket 의존성
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-rsocket'
기본 설정
@Configuration
@EnableRSocketSecurity
public class RSocketSecurityConfig {
@Bean
public PayloadSocketAcceptorInterceptor security(RSocketSecurity security) {
return security
.authorizePayload(authorize -> authorize
.route("user.public.**").permitAll()
.route("user.list").authenticated()
.route("admin.**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.simpleAuthentication(Customizer.withDefaults())
.build();
}
@Bean
public MapReactiveUserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("alice")
.password("pass")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("USER", "ADMIN")
.build();
return new MapReactiveUserDetailsService(user, admin);
}
}
클라이언트 — Basic Authentication
import org.springframework.security.rsocket.metadata.UsernamePasswordMetadata;
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("alice", "pass");
requester.route("user.list")
.metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.retrieveFlux(User.class);
SETUP 시 인증
RSocketRequester.Builder builder = RSocketRequester.builder()
.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.rsocketStrategies(strategies);
SETUP 시 한 번 인증 → 이후 모든 메시지 같은 사용자.
JWT 토큰 인증
서버
@Bean
public PayloadSocketAcceptorInterceptor security(RSocketSecurity security) {
return security
.authorizePayload(authorize -> authorize
.anyExchange().authenticated()
)
.jwt(jwt -> jwt.authenticationManager(jwtAuthManager()))
.build();
}
@Bean
public ReactiveAuthenticationManager jwtAuthManager() {
JwtReactiveAuthenticationManager manager = new JwtReactiveAuthenticationManager(...);
return manager;
}
클라이언트
import org.springframework.security.rsocket.metadata.BearerTokenMetadata;
BearerTokenMetadata token = new BearerTokenMetadata("eyJhbGc...");
requester.route("api.secure")
.metadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
.retrieveMono(Result.class);
여기서 시험 함정이 하나 있어요. JWT는 stateless 친화. 서버가 토큰 자체로 검증. DB 조회 X. RSocket 마이크로서비스에 적합.
라우트별 권한 — Method Security
@Controller
public class UserController {
@MessageMapping("user.{id}")
@PreAuthorize("hasRole('USER')")
public Mono<User> get(@DestinationVariable String id) { ... }
@MessageMapping("admin.delete")
@PreAuthorize("hasRole('ADMIN')")
public Mono<Void> delete(String id) { ... }
@MessageMapping("user.profile")
@PreAuthorize("authentication.name == #id") // 본인만
public Mono<Profile> getProfile(String id) { ... }
}
@EnableReactiveMethodSecurity 활성화 필요.
Authentication 객체 주입
@MessageMapping("user.profile")
public Mono<Profile> getProfile(
@AuthenticationPrincipal UserDetails user
) {
return profileService.getByUser(user.getUsername());
}
또는
@MessageMapping("user.profile")
public Mono<Profile> getProfile(Mono<Authentication> auth) {
return auth.flatMap(a -> profileService.getByUser(a.getName()));
}
TLS — Transport Layer Security
서버
spring:
rsocket:
server:
port: 7000
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${KS_PASS}
key-store-type: PKCS12
protocol: TLSv1.3
클라이언트
SslContext sslContext = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE) // 개발만!
.build();
TcpClient tcpClient = TcpClient.create()
.host("server")
.port(7000)
.secure(spec -> spec.sslContext(sslContext));
RSocketRequester requester = RSocketRequester.builder()
.transport(TcpClientTransport.create(tcpClient));
여기서 정말 중요한 시험 함정 — 운영 = TLS 필수. 메타데이터·페이로드 모두 평문 노출 가능. 인증 토큰 = TLS 없으면 가로채임.
mTLS — Mutual TLS
서버·클라이언트 모두 인증서로 인증.
서버
spring.rsocket.server.ssl:
enabled: true
key-store: classpath:server-keystore.p12
trust-store: classpath:server-truststore.p12 # 클라이언트 인증서 검증
client-auth: need # 강제
클라이언트
SslContext sslContext = SslContextBuilder.forClient()
.keyManager(clientCert, clientKey)
.trustManager(serverCert)
.build();
용도:
- 서비스 간 인증 — 마이크로서비스 ↔ 마이크로서비스
- API 키 대체
- 제로 트러스트 환경
OAuth2 / OIDC
@Bean
public PayloadSocketAcceptorInterceptor security(
RSocketSecurity security,
ReactiveJwtDecoder jwtDecoder
) {
return security
.authorizePayload(authorize -> authorize.anyExchange().authenticated())
.jwt(jwt -> jwt.authenticationManager(
new JwtReactiveAuthenticationManager(jwtDecoder)))
.build();
}
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders
.fromIssuerLocation("https://auth.example.com");
}
OAuth2 Resource Server처럼 동작.
CORS — RSocket WebSocket Transport
WebSocket transport 시 CORS 고려. TCP는 무관.
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://example.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/rsocket", config);
return new CorsWebFilter(source);
}
보안 체크리스트
✓ TLS 활성화 (운영)
✓ SETUP 시 인증 (한 번)
✓ 토큰 만료 시간 짧게 (수 분~수 시간)
✓ JWT 또는 OAuth2 (stateless)
✓ 라우트별 @PreAuthorize
✓ Method Security 활성화
✓ 메타데이터에 민감 정보 X (TLS 없을 때)
✓ Refresh Token 별도 보관
✓ 로그에 토큰 출력 X
✓ Audit Log
✓ Rate Limit (per user)
✓ Input Validation
DDoS·Rate Limit
@Bean
public PayloadInterceptor rateLimitInterceptor() {
return new RateLimitInterceptor(...);
}
또는 외부 (Nginx·API Gateway).
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 6편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 보안 3 축 — 인증 / 인가 / 암호화
- SETUP 인증 = 한 번 (일반 권장)
- Per-request 인증 = 매 메시지 (민감 작업)
- 둘 결합 가능
spring-security-rsocket의존성@EnableRSocketSecurityRSocketSecurity—authorizePayload + simpleAuthentication / jwt- 라우트별 —
permitAll·authenticated·hasRole - Basic Auth —
UsernamePasswordMetadata+BASIC_AUTHENTICATION_MIME_TYPE - JWT (Bearer) —
BearerTokenMetadata+BEARER_AUTHENTICATION_MIME_TYPE - JWT = stateless, 마이크로서비스 친화
@PreAuthorize— 라우트별·SpEL 표현식@EnableReactiveMethodSecurity@AuthenticationPrincipal또는Mono<Authentication>주입- TLS 필수 (운영)
- 메타데이터·페이로드 평문 → 가로채임
- TLS 1.3 권장
- mTLS = 서버·클라이언트 모두 인증서
- 마이크로서비스 사이·제로 트러스트
- OAuth2 / OIDC =
ReactiveJwtDecoder.fromIssuerLocation - WebSocket transport = CORS 고려
- 운영 체크 — TLS·짧은 토큰·JWT·@PreAuthorize·민감 정보 노출 X·Audit·Rate Limit
시리즈 다른 편
- 1편 — 기본 개념·프레임
- 2편 — 4 Interaction Models
- 3편 — Spring RSocket 서버
- 4편 — Spring RSocket 클라이언트
- 5편 — 메타데이터·Composite Metadata
- 6편 — 보안·Spring Security RSocket·TLS (현재 글)
- 7편 — 로드 밸런싱·확장
- 8편 — 테스트
- 9편 — RSocket vs gRPC vs WebSocket
공식 문서: Spring Security RSocket 에서 더 깊이.
다음 글(7편)에서는 로드 밸런싱·확장 — 클라이언트 사이드 LB, RSocketLoadbalanceTarget, 헬스 체크, 다중 서버 라우팅까지 풀어 갑니다.