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.
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 .
Principal
disponible en los controladores. 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:
AuthentionProvider
no toma parte en la autenticación de Websocket. simpUser
) se almacenará y no se requerirá más autenticación en otras solicitudes. org.springframework.boot spring-boot-starter-websocket org.springframework spring-messaging org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-messaging
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("*") } }
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);