Autenticación y autorización de Websocket en spring

He estado luchando mucho para implementar correctamente la Autenticación y Autorización de Stomp (websocket) con Spring-Security. Para la posteridad, responderé mi propia pregunta para proporcionar una guía.

El problema

La documentación de Spring WebSocket (para Autenticación) parece poco clara ATM (en mi humilde opinión). Y no pude entender cómo manejar adecuadamente la Autenticación y la Autorización .

Lo que quiero

  • Autentica usuarios con login / contraseña.
  • Evite que usuarios anónimos se CONECTEN a través de WebSocket.
  • Añadir capa de autorización (usuario, administrador, …).
  • Tener el Principal disponible en los controladores.

Lo que no quiero

  • Autentíquese en puntos finales de negociación HTTP (ya que la mayoría de las bibliotecas JavaScript no envía encabezados de autenticación junto con la llamada de negociación HTTP).

Como se indicó anteriormente, la documentación (ATM) no está clara, hasta que Spring proporcione cierta documentación clara, aquí hay una plantilla para evitar que pases dos días tratando de entender qué está haciendo la cadena de seguridad.

Rob-Leggett hizo un buen bash, pero él estaba haciendo una clase de Springs y deberías evitar eso tanto como sea posible .

Cosas que saber:

  • La cadena de seguridad y la configuración de seguridad para http y WebSocket son completamente independientes.
  • Spring AuthentionProvider no toma parte en la autenticación de Websocket.
  • Una vez configurado en la solicitud CONNECT, el usuario ( simpUser ) se almacenará y no se requerirá más autenticación en otras solicitudes.

Maven deps

  org.springframework.boot spring-boot-starter-websocket   org.springframework spring-messaging   org.springframework.boot spring-boot-starter-security   org.springframework.security spring-security-messaging  

Configuración de WebSocket

La siguiente configuración registra un intermediario de mensajes simple (Tenga en cuenta que no tiene nada que ver con la autenticación ni la autorización).

 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(final MessageBrokerRegistry config) { // These are endpoints the client can subscribes to. config.enableSimpleBroker("/queue/topic"); // Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(final StompEndpointRegistry registry) { // Handshake endpoint registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*") } } 

Configuración de seguridad de spring

Dado que el protocolo Stomp depende de una primera solicitud HTTP, necesitaremos autorizar la llamada HTTP a nuestro punto final handshake stomp.

 @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(final HttpSecurity http) throws Exception { // This is not for websocket authorization, and this should most likely not be altered. http .httpBasic().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests().antMatchers("/stomp").permitAll() .anyRequest().denyAll(); } } 

Luego crearemos un servicio responsable de autenticar usuarios.

 @Component public class WebSocketAuthenticatorService { // This method MSUT return a UsernamePasswordAuthenticationToken, another component in the security chain is testing it with 'instanceof' public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException { if (username == null || username.trim().length()) { throw new AuthenticationCredentialsNotFoundException("Username was null or empty."); } if (password == null || password.trim().length()) { throw new AuthenticationCredentialsNotFoundException("Password was null or empty."); } // Add your own logic for retrieving user in fetchUserFromDb() if (fetchUserFromDb(username, password) == null) { throw new BadCredentialsException("Bad credentials for user " + username); } // null credentials, we do not pass the password along return new UsernamePasswordAuthenticationToken( username, null, Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role ); } } 

Tenga en cuenta que: UsernamePasswordAuthenticationToken DEBE tener GrantedAuthorities, si usa otro constructor, Spring se auto-configurará isAuthenticated = false .

Casi allí, ahora tenemos que crear un Interceptor que establecerá el encabezado simpUser o arrojar AuthenticationException en los mensajes CONNECT.

 @Component public class AuthChannelInterceptorAdapter extends ChannelInterceptor { private static final String USERNAME_HEADER = "login"; private static final String PASSWORD_HEADER = "passcode"; private final WebSocketAuthenticatorService webSocketAuthenticatorService; @Inject public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) { this.webSocketAuthenticatorService = webSocketAuthenticatorService; } @Override public Message preSend(final Message message, final MessageChannel channel) throws AuthenticationException { final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT == accessor.getCommand()) { final String username = accessor.getFirstNativeHeader(USERNAME_HEADER); final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER); final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password); accessor.setUser(user); } return message; } } 

Tenga en cuenta que: preSend() DEBE devolver un UsernamePasswordAuthenticationToken , otro elemento en la cadena de seguridad de spring prueba esto. Tenga en cuenta que: si su UsernamePasswordAuthenticationToken se construyó sin pasar GrantedAuthority , la autenticación fallará, porque el constructor sin autorizaciones otorgadas se estableció automáticamente authenticated = false ESTE ES UN DETALLE IMPORTANTE que no está documentado en spring-security .

Finalmente cree dos clases más para manejar respectivamente Autorización y Autenticación.

 @Configuration @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer { @Inject private AuthChannelInterceptorAdapter authChannelInterceptorAdapter; @Override public void registerStompEndpoints(final StompEndpointRegistry registry) { // Endpoints are already registered on WebSocketConfig, no need to add more. } @Override public void configureClientInboundChannel(final ChannelRegistration registration) { registration.setInterceptors(authChannelInterceptorAdapter); } } 

Tenga en cuenta que: @Order es @Order , no lo olvide, permite que nuestro interceptor se registre primero en la cadena de seguridad.

 @Configuration public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) { // You can customize your authorization mapping here. messages.anyMessage().authenticated(); } // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint. @Override protected boolean sameOriginDisabled() { return true; } } 

Buena suerte !

para el lado del cliente de Java use este ejemplo probado:

 StompHeaders connectHeaders = new StompHeaders(); connectHeaders.add("login", "test1"); connectHeaders.add("passcode", "test"); stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler);