1. Введение
В предыдущей статье мы показали, как добавить WebSockets в проект Spring MVC.
Здесь мы опишем, как добавить безопасность к Spring WebSockets в Spring MVC . Прежде чем продолжить, убедитесь, что у вас уже есть базовое покрытие безопасности Spring MVC — если нет, ознакомьтесь с этой статьей .
2. Зависимости Maven
Есть две основные группы зависимостей Maven , которые нам нужны для нашей реализации WebSocket.
Во-первых, давайте укажем общие версии Spring Framework и Spring Security, которые мы будем использовать:
<properties>
<spring.version>5.3.13</spring.version>
<spring-security.version>5.6.0</spring-security.version>
</properties>
Во-вторых, давайте добавим основные библиотеки Spring MVC и Spring Security, необходимые для реализации базовой аутентификации и авторизации:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
Последние версии spring-core , spring-web , spring-webmvc , spring-security-web , spring-security-config можно найти на Maven Central.
Наконец, давайте добавим необходимые зависимости:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
<version>${spring-security.version}</version>
</dependency>
Вы можете найти последнюю версию spring-websocket , spring-messaging и spring-security-messaging на Maven Central.
3. Базовая безопасность веб-сокетов
Безопасность, специфичная для WebSocket, с использованием библиотеки spring-security-messaging
сосредоточена на классе AbstractSecurityWebSocketMessageBrokerConfigurer
и его реализации в вашем проекте:
@Configuration
public class SocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer {
//...
}
Класс AbstractSecurityWebSocketMessageBrokerConfigurer
обеспечивает дополнительную защиту, обеспечиваемую WebSecurityConfigurerAdapter .
Библиотека spring-security-messaging
— не единственный способ реализовать безопасность для WebSockets. Если мы будем придерживаться обычной библиотеки spring-websocket
, мы сможем реализовать интерфейс WebSocketConfigurer
и подключить перехватчики безопасности к нашим обработчикам сокетов.
Поскольку мы используем библиотеку обмена сообщениями spring-security
, мы будем использовать подход AbstractSecurityWebSocketMessageBrokerConfigurer .
3.1. Реализация configureInbound()
Реализация configureInbound()
является наиболее важным шагом в настройке вашего подкласса AbstractSecurityWebSocketMessageBrokerConfigurer :
@Override
protected void configureInbound(
MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/secured/**").authenticated()
.anyMessage().authenticated();
}
В то время как WebSecurityConfigurerAdapter
позволяет указывать различные требования авторизации для разных маршрутов в рамках всего приложения, AbstractSecurityWebSocketMessageBrokerConfigurer
позволяет указывать конкретные требования авторизации для назначений сокетов.
3.2. Соответствие типа и назначения
MessageSecurityMetadataSourceRegistry
позволяет нам указать ограничения безопасности, такие как пути, роли пользователей и разрешенные сообщения.
Сопоставители типов ограничивают, какие SimpMessageType
разрешены и каким образом :
.simpTypeMatchers(CONNECT, UNSUBSCRIBE, DISCONNECT).permitAll()
Сопоставители назначения ограничивают, какие шаблоны конечных точек доступны и каким образом :
.simpDestMatchers("/app/**").hasRole("ADMIN")
Сопоставители адресатов подписки сопоставляют список
экземпляров
SimpDestinationMessageMatcher
, которые соответствуют SimpMessageType.SUBSCRIBE :
``
.simpSubscribeDestMatchers("/topic/**").authenticated()
Вот полный список всех доступных методов для сопоставления типов и назначений .
4. Защита маршрутов сокетов
Теперь, когда мы познакомились с базовой безопасностью сокетов и конфигурацией сопоставления типов, мы можем комбинировать безопасность сокетов, представления, STOMP (протокол обмена текстовыми сообщениями), брокеры сообщений и контроллеры сокетов, чтобы включить безопасные веб-сокеты в нашем приложении Spring MVC.
Во-первых, давайте настроим наши представления сокетов и контроллеры для базового охвата Spring Security:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
@ComponentScan("com.foreach.springsecuredsockets")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/index", "/authenticate").permitAll()
.antMatchers(
"/secured/**/**",
"/secured/success",
"/secured/socket",
"/secured/success").authenticated()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/authenticate")
//...
}
}
Во-вторых, давайте настроим фактическое назначение сообщения с требованиями аутентификации:
@Configuration
public class SocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/secured/**").authenticated()
.anyMessage().authenticated();
}
}
Теперь в нашем WebSocketMessageBrokerConfigurer
мы можем зарегистрировать фактическое сообщение и конечные точки STOMP:
@Configuration
@EnableWebSocketMessageBroker
public class SocketBrokerConfig
implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/secured/history");
config.setApplicationDestinationPrefixes("/spring-security-mvc-socket");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/secured/chat")
.withSockJS();
}
}
Давайте определим пример контроллера сокета и конечной точки, для которых мы обеспечили безопасность выше:
@Controller
public class SocketController {
@MessageMapping("/secured/chat")
@SendTo("/secured/history")
public OutputMessage send(Message msg) throws Exception {
return new OutputMessage(
msg.getFrom(),
msg.getText(),
new SimpleDateFormat("HH:mm").format(new Date()));
}
}
5. Та же политика происхождения
Политика того же источника требует, чтобы все взаимодействия с конечной точкой происходили из того же домена, в котором взаимодействие было инициировано.
Например, предположим, что ваша реализация WebSockets размещена на foo.com
, и вы применяете ту же политику происхождения . Если пользователь подключается к вашему клиенту, размещенному на foo.com,
а затем открывает другой браузер для bar.com
, тогда bar.com
не будет иметь доступа к вашей реализации WebSocket.
5.1. Переопределение той же политики происхождения
Spring WebSockets применяет ту же политику происхождения из коробки, в то время как обычные WebSockets этого не делают.
Фактически, Spring Security требует токена CSRF ( подделка межсайтовых запросов
) для любого допустимого типа сообщения CONNECT :
@Controller
public class CsrfTokenController {
@GetMapping("/csrf")
public @ResponseBody String getCsrfToken(HttpServletRequest request) {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
return csrf.getToken();
}
}
Вызывая конечную точку по адресу /csrf
, клиент может получить токен и пройти аутентификацию через уровень безопасности CSRF.
Однако ту же политику происхождения для Spring можно переопределить , добавив следующую конфигурацию в ваш AbstractSecurityWebSocketMessageBrokerConfigurer
:
@Override
protected boolean sameOriginDisabled() {
return true;
}
5.2. STOMP, поддержка SockJS и параметры фрейма
Обычно STOMP используется вместе с SockJS для реализации клиентской поддержки Spring WebSockets.
SockJS по умолчанию настроен на запрет транспорта через HTML-элементы iframe .
Это делается для предотвращения угрозы кликджекинга .
Тем не менее, есть определенные варианты использования, в которых может быть полезно разрешить iframe использовать транспорты SockJS .
Для этого вы можете переопределить конфигурацию по умолчанию в WebSecurityConfigurerAdapter
:
@Override
protected void configure(HttpSecurity http)
throws Exception {
http
.csrf()
//...
.and()
.headers()
.frameOptions().sameOrigin()
.and()
.authorizeRequests();
}
Обратите внимание, что в этом примере мы следуем той же политике происхождения , несмотря на то, что разрешаем транспорт через iframe
.
6. Покрытие Oauth2
Поддержка Spring WebSockets, специфичная для Oauth2, стала возможной благодаря реализации покрытия безопасности Oauth2 в дополнение к стандартному покрытию WebSecurityConfigurerAdapter и путем его расширения
.
Вот пример того, как реализовать Oauth2.
Чтобы пройти аутентификацию и получить доступ к конечной точке WebSocket, вы можете передать Oauth2 access_token
в параметр запроса при подключении от вашего клиента к серверному WebSocket.
Вот пример, демонстрирующий эту концепцию с использованием SockJS и STOMP:
var endpoint = '/ws/?access_token=' + auth.access_token;
var socket = new SockJS(endpoint);
var stompClient = Stomp.over(socket);
7. Заключение
В этом кратком руководстве мы показали, как повысить безопасность Spring WebSockets. Взгляните на справочную документацию Spring WebSocket и WebSocket Security , если вы хотите узнать больше об этой интеграции.
Как всегда, проверьте наш проект Github для примеров, используемых в этой статье.