Spring-websockets: Spring безопасности авторизация не работает внутри веб-сокетов - PullRequest
0 голосов
/ 22 января 2019

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

WebsocketConfig:

@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());
        }
    }
}

security-applicationContext.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>

1 Ответ

0 голосов
/ 22 января 2019

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

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

Создайте класс, который реализует ChannelInterceptorAdapter Spring. Внутри этого класса вы можете внедрить любые 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 вам необходимо зарегистрировать ваш перехватчик. Добавьте приведенный выше класс в качестве компонента и зарегистрируйте его. После этих изменений ваш класс будет выглядеть так:

@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 или использовать то, что вы используете.

...