Ответ 1
Похоже, что поддержка строки запроса была добавлена к клиенту SockJS, см. https://github.com/sockjs/sockjs-client/issues/72.
Я собираюсь создать веб-приложение RESTful с помощью Spring Boot (1.3.0.BUILD-SNAPSHOT), который включает в себя STOMP/SockJS WebSocket, который я намерен использовать в приложении iOS, а также в Интернете браузеры. Я хочу использовать JSON Web Tokens (JWT) для защиты запросов REST и интерфейса WebSocket, но Im имеет трудности с последним.
Приложение защищено с помощью Spring Безопасность: -
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public WebSecurityConfiguration() {
super(true);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("steve").password("steve").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().and()
.anonymous().and()
.servletApi().and()
.headers().cacheControl().and().and()
// Relax CSRF on the WebSocket due to needing direct access from apps
.csrf().ignoringAntMatchers("/ws/**").and()
.authorizeRequests()
//allow anonymous resource requests
.antMatchers("/", "/index.html").permitAll()
.antMatchers("/resources/**").permitAll()
//allow anonymous POSTs to JWT
.antMatchers(HttpMethod.POST, "/rest/jwt/token").permitAll()
// Allow anonymous access to websocket
.antMatchers("/ws/**").permitAll()
//all other request need to be authenticated
.anyRequest().hasRole("USER").and()
// Custom authentication on requests to /rest/jwt/token
.addFilterBefore(new JWTLoginFilter("/rest/jwt/token", authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
// Custom JWT based authentication
.addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Конфигурация WebSocket является стандартной: -
@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
}
У меня также есть подкласс AbstractSecurityWebSocketMessageBrokerConfigurer
для защиты WebSocket: -
@Configuration
public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().hasRole("USER");
}
@Override
protected boolean sameOriginDisabled() {
// We need to access this directly from apps, so can't do cross-site checks
return true;
}
}
Существует также несколько аннотированных классов @RestController
для обработки различных битов функциональности, и они успешно защищены через JWTTokenFilter
, зарегистрированный в моем классе WebSecurityConfiguration
.
Однако я не могу заставить WebSocket быть защищенным с помощью JWT. Я использую SockJS 1.1.0 и STOMP 1.7.1 в браузере и не могу понять, как для передачи токена. Появится сообщение что SockJS не позволяет отправлять параметры с помощью начальных запросов /info
и/или рукопожатия.
Любое входящее сообщение CONNECT требует действительного токена CSRF для принудительной реализации той же политики происхождения
Что, по-видимому, подразумевает, что первоначальное рукопожатие должно быть необеспеченным и аутентификация, вызываемая в момент получения сообщения STOMP CONNECT. К сожалению, я не могу найти никакой информации в отношении ее реализации. Кроме того, для этого подхода потребуется дополнительная логика для отключения клиента-изгоя, который открывает соединение WebSocket и никогда не отправляет STOMP CONNECT.
Быть (очень) новым для Spring Я также не уверен, что и как Spring Сессии вписываются в это. Несмотря на то, что документация очень подробно, нет ничего приятного и простого (ака-идиота) руководства о том, как различные компоненты подходят друг другу/взаимодействуют друг с другом.
Как я могу обеспечить безопасность SockJS WebSocket, предоставляя токен JSON Web, желательно в момент рукопожатия (возможно ли это)?
Похоже, что поддержка строки запроса была добавлена к клиенту SockJS, см. https://github.com/sockjs/sockjs-client/issues/72.
ОБНОВЛЕНИЕ 2016-12-13: проблема, о которой идет речь ниже, теперь отмечена как фиксированная, поэтому теперь не требуется hack ниже Spring 4.3.5 или выше. См. https://github.com/spring-projects/spring-framework/blob/master/src/docs/asciidoc/web/web-websocket.adoc#token-based-authentication.
В настоящее время (сентябрь 2016) это не поддерживается Spring за исключением параметра запроса, как ответил @rossen-stoyanchev, который много писал (все?) поддержки Spring WebSocket. Мне не нравится подход параметров запроса из-за потенциальной утечки HTTP-реферера и хранения токена в журналах сервера. Кроме того, если последствия безопасности не беспокоят вас, обратите внимание, что я нашел, что этот подход работает для истинных подключений WebSocket, но если вы используете SockJS с резервными копиями для других механизмов, метод determineUser
никогда не вызывается для резервного копирования. См. Spring 4.x резервная аутентификация WebSocket SockJS на основе токенов.
Я создал проблему Spring, чтобы улучшить поддержку аутентификации на основе маркеров на основе токенов: https://jira.spring.io/browse/SPR-14690
В то же время я нашел взломан, который хорошо работает при тестировании. Обход встроенного устройства Spring на уровне соединения Spring. Вместо этого установите токен аутентификации на уровне сообщения, отправив его в заголовки Stomp на стороне клиента (это прекрасно отражает то, что вы уже делаете с обычными вызовами HTTP XHR), например:
stompClient.connect({'X-Authorization': 'token'}, ...);
stompClient.subscribe(..., {'X-Authorization': 'token'});
stompClient.send("/wherever", {'X-Authorization': 'token'}, ...);
На стороне сервера получите токен из сообщения Stomp с помощью ChannelInterceptor
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
String token = null;
if(tokenList == null || tokenList.size < 1) {
return message;
} else {
token = tokenList.get(0);
if(token == null) {
return message;
}
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = [...];
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
}
})
Это просто и получает от нас 85% пути, однако этот подход не поддерживает отправку сообщений конкретным пользователям. Это связано с тем, что Spring механизм, связанный с пользователями для сеансов, не влияет на результат ChannelInterceptor
. Spring WebSocket предполагает, что аутентификация выполняется на транспортном уровне, а не на уровне сообщений, и, таким образом, игнорирует аутентификацию на уровне сообщений.
В любом случае взломать эту работу, чтобы создать наши экземпляры DefaultSimpUserRegistry
и DefaultUserDestinationResolver
, выставить их в среду, а затем использовать перехватчик, чтобы обновить их, как если бы он сам выполнял Spring. Другими словами, что-то вроде:
@Configuration
@EnableWebSocketMessageBroker
@Order(HIGHEST_PRECEDENCE + 50)
class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer() {
private DefaultSimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
private DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry);
@Bean
@Primary
public SimpUserRegistry userRegistry() {
return userRegistry;
}
@Bean
@Primary
public UserDestinationResolver userDestinationResolver() {
return resolver;
}
@Override
public configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic");
}
@Override
public registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stomp")
.withSockJS()
.setWebSocketEnabled(false)
.setSessionCookieNeeded(false);
}
@Override public configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
Message<*> preSend(Message<*> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
List tokenList = accessor.getNativeHeader("X-Authorization");
accessor.removeNativeHeader("X-Authorization");
String token = null;
if(tokenList != null && tokenList.size > 0) {
token = tokenList.get(0);
}
// validate and convert to a Principal based on your own requirements e.g.
// authenticationManager.authenticate(JwtAuthentication(token))
Principal yourAuth = token == null ? null : [...];
if (accessor.messageType == SimpMessageType.CONNECT) {
userRegistry.onApplicationEvent(SessionConnectedEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.SUBSCRIBE) {
userRegistry.onApplicationEvent(SessionSubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.UNSUBSCRIBE) {
userRegistry.onApplicationEvent(SessionUnsubscribeEvent(this, message, yourAuth));
} else if (accessor.messageType == SimpMessageType.DISCONNECT) {
userRegistry.onApplicationEvent(SessionDisconnectEvent(this, message, accessor.sessionId, CloseStatus.NORMAL));
}
accessor.setUser(yourAuth);
// not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders);
}
})
}
}
Теперь Spring полностью осведомлен об аутентификации, то есть он вводит Principal
в любые требуемые ему методы контроллера, предоставляет его в контексте для Spring Security 4.x и связывает пользователя с WebSocket сеанс для отправки сообщений определенным пользователям/сеансам.
Наконец, если вы используете поддержку Spring Security 4.x Messaging, убедитесь, что @Order
вашего AbstractWebSocketMessageBrokerConfigurer
имеет более высокое значение, чем Spring Безопасность AbstractSecurityWebSocketMessageBrokerConfigurer
(Ordered.HIGHEST_PRECEDENCE + 50
будет работать, как показано выше). Таким образом, ваш перехватчик устанавливает Principal
до Spring Безопасность выполняет свою проверку и устанавливает контекст безопасности.
С последним SockJS 1.0.3 вы можете передавать параметры запроса как часть URL-адреса соединения. Таким образом, вы можете отправить токен JWT для авторизации сеанса.
var socket = new SockJS('http://localhost/ws?token=AAA');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
stompClient.subscribe('/topic/echo', function(data) {
// topic handler
});
}
}, function(err) {
// connection error
});
Теперь все запросы, связанные с websocket, будут иметь параметр "? токен = AAA"
http://localhost/ws/info?token=AAA&t=1446482506843
http://localhost/ws/515/z45wjz24/websocket?token=AAA
Затем с помощью Spring вы можете настроить фильтр, который идентифицирует сеанс, используя предоставленный токен.