Spring-websockets: авторизация безопасности Spring не работает внутри веб-сокетов

Я работаю над приложением Spring-MVC, в котором у нас есть Spring-безопасность для аутентификации и авторизации. Мы работаем над переходом на веб-сокеты Spring, но у нас возникла проблема с получением аутентифицированного пользователя внутри соединения с веб-сокетом. Контекст безопасности просто не существует в соединении через веб-сокет, но отлично работает с обычным HTTP. Что мы делаем не так?

Конфигурация веб-сокета:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/app").withSockJS();
    }
}

В приведенном ниже контроллере мы пытаемся получить текущего аутентифицированного пользователя, и он всегда нулевой.

@Controller
public class OnlineStatusController extends MasterController{

    @MessageMapping("/onlinestatus")
    public void onlineStatus(String status) {
        Person user = this.personService.getCurrentlyAuthenticatedUser();
        if (user!=null){
            this.chatService.setOnlineStatus(status, user.getId());
        }
    }
}

безопасность-приложениеContext.xml :

  <security:http pattern = "/resources/**" security = "none"/>
    <security:http pattern = "/org/**" security = "none"/>
    <security:http pattern = "/jquery/**" security = "none"/>
    <security:http create-session = "ifRequired" use-expressions = "true" auto-config = "false" disable-url-rewriting = "true">
        <security:form-login login-page = "/login" username-parameter = "j_username" password-parameter = "j_password"
                             login-processing-url = "/j_spring_security_check" default-target-url = "/canvaslisting"
                             always-use-default-target = "false" authentication-failure-url = "/login?error=auth"/>
        <security:remember-me key = "_spring_security_remember_me" user-service-ref = "userDetailsService"
                              token-validity-seconds = "1209600" data-source-ref = "dataSource"/>
        <security:logout delete-cookies = "JSESSIONID" invalidate-session = "true" logout-url = "/j_spring_security_logout"/>
        <security:csrf disabled = "true"/>
        <security:intercept-url pattern = "/cometd/**" access = "permitAll" />
        <security:intercept-url pattern = "/app/**" access = "hasAnyRole('ROLE_ADMIN','ROLE_USER')" />
<!--        <security:intercept-url pattern = "/**" requires-channel = "https"/>-->
        <security:port-mappings>
            <security:port-mapping http = "80" https = "443"/>
        </security:port-mappings>
        <security:logout logout-url = "/logout" logout-success-url = "/" success-handler-ref = "myLogoutHandler"/>
        <security:session-management session-fixation-protection = "newSession">
            <security:concurrency-control session-registry-ref = "sessionReg" max-sessions = "5" expired-url = "/login"/>
        </security:session-management>
    </security:http>
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
1 474
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я помню, как наткнулся на ту же самую проблему в проекте, над которым работал. Поскольку я не мог найти решение, используя документацию Spring, а другие ответы на переполнение стека у меня не работали, в итоге я создал обходной путь.

Хитрость, по сути, заключается в том, чтобы заставить приложение аутентифицировать пользователя в запросе на подключение WebSocket. Для этого вам нужен класс, который перехватывает такие события, а затем, получив контроль над этим, вы можете вызвать свою логику аутентификации.

Создайте класс, реализующий Spring ChannelInterceptorAdapter. Внутри этого класса вы можете внедрить любые bean-компоненты, необходимые для фактической аутентификации. В моем примере используется базовая аутентификация:

@Component
public class WebSocketAuthInterceptorAdapter extends ChannelInterceptorAdapter {

    @Autowired
    private DaoAuthenticationProvider userAuthenticationProvider;

    @Override
    public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {

        final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        StompCommand cmd = accessor.getCommand();

        if (StompCommand.CONNECT == cmd || StompCommand.SEND == cmd) {
            Authentication authenticatedUser = null;
            String authorization = accessor.getFirstNativeHeader("Authorization:");
            String credentialsToDecode = authorization.split("\\s")[1];
            String credentialsDecoded = StringUtils.newStringUtf8(Base64.decodeBase64(credentialsToDecode));
            String[] credentialsDecodedSplit = credentialsDecoded.split(":");
            final String username = credentialsDecodedSplit[0];
            final String password = credentialsDecodedSplit[1];
            authenticatedUser = userAuthenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            if (authenticatedUser == null) {
                throw new AccessDeniedException();
            } 
            SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
            accessor.setUser(authenticatedUser);    
        }
        return message;
    }
}

Затем в вашем классе WebSocketConfig вам нужно зарегистрировать свой перехватчик. Добавьте указанный выше класс как bean-компонент и зарегистрируйте его. После этих изменений ваш класс будет выглядеть так:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private WebSocketAuthInterceptorAdapter authInterceptorAdapter;
    

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/app").withSockJS();
    }
    
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(authInterceptorAdapter);
        super.configureClientInboundChannel(registration);
    }
}

Очевидно, что детали логики аутентификации зависят от вас. Вы можете вызвать службу JWT или что-то еще, что вы используете.

Можете ли вы сказать мне, из какого класса WebSocketChannelInterceptorAdapter? Спасибо.

We are Borg 13.02.2019 12:36

Класс должен быть WebSocketAuthInterceptorAdapter. Я отредактировал свой ответ.

AlgorithmFromHell 13.02.2019 14:51

Я реализовал аналогичный код, и я могу получить пользователя внутри Interceptor, но в сервисе, когда я использую SecurityContextHolder.getContext(), я получаю сообщение об ошибке: An Authentication object was not found in the SecurityContext

aleksaRazer22 29.01.2020 14:40

Извините, что отвечаю так поздно. Прошло некоторое время с тех пор, как я сделал что-либо, связанное с Spring Websockets. Я предполагаю, что эта ошибка должна означать, что что-то неправильно настроено в Spring Security, и авторизация, которую вы устанавливаете, не интерпретируется фреймворком.

AlgorithmFromHell 29.05.2020 15:44

Если вы используете SockJS + Stomp и правильно настроили свою безопасность, вы сможете подключиться через обычный аутентификатор имени пользователя/пароля, такой как @AlgorithmFromHell, и сделать

accessor.setUser(authentication.getPrincipal()) // stomp header accessor
   

Вы также можете подключиться через http://{END_POINT}/access_token = {ACCESS_TOKEN}. Безопасность Spring должна иметь возможность выбрать его и выполнить loadAuthentication (access_token) через ResourceServerTokenServices. Когда это будет сделано, вы можете получить своего принципала, добавив его в свою реализацию AbstractSessionWebSocketMessageBrokerConfigurer или WebSocketMessageBrokerConfigurer. При этом по какой-то причине загруженный Pricipal вместо этого сохраняется в заголовке «simpUser».

@Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(new ChannelInterceptor() {
      @Override
      public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
          if (message.getHeaders().get("simpUser") != null && message.getHeaders().get("simpUser") instanceof OAuth2Authentication) { // or Authentication depending on your impl of security
            OAuth2Authentication authentication = (OAuth2Authentication) message.getHeaders().get("simpUser");
            accessor.setUser(authentication != null ? (UserDetails) authentication.getPrincipal() : null);
          }

        }
        return message;
      }
    });
  }

Другие вопросы по теме