java - Spring Websocket 安全抛出 AccessDeniedException

标签 java spring spring-boot spring-security spring-websocket

首先让我说,对于我的场景,访问应该被拒绝。内置 Spring Boot 和 Spring Security 4。我允许任何人连接到 websocket 并订阅一个主题,但我确保能够使用以下 web 套接字安全配置向该主题发送消息:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpSubscribeDestMatchers("/main-page-feed/thought-queue/**").permitAll()
                .simpDestMatchers("/thought-bubble/push-to-queue/**").authenticated();


    }

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }

}

因此,当您尝试在未经身份验证的情况下向 /thought-bubble/push-to-queue 发送消息时,它会拒绝您(这是正确的,我想强调这一点,因为我能找到的唯一其他问题是错误抛出异常时)并抛出 AccessDeniedException。我不明白的是,与 websocket 安全性相反,Spring HTTP 安全性会锁定某些内容并拒绝访问,它不会抛出异常,它只是发送 HTTP 状态。我试过使用 @ExceptionHandler、AccessDenied 处理程序,但我尝试过的任何方法都无法捕获和处理此异常。下面是堆栈跟踪和其他相关文件,任何想法都会受到赞赏,因为我很困惑。我已尝试在调试中单步执行源代码,但我并没有真正看出问题所在。

堆栈跟踪:

org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:127) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:104) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:298) ~[spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.java:307) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.java:58) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession.delegateMessages(AbstractSockJsSession.java:382) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession.handleMessage(WebSocketServerSockJsSession.java:193) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler.handleTextMessage(SockJsWebSocketHandler.java:92) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:110) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:42) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:78) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:399) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:106) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:500) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:295) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:131) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:69) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:802) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1410) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_101]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_101]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_101]
    Caused by: org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor.preSend(ChannelSecurityInterceptor.java:71) ~[spring-security-messaging-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:158) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:113) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    ... 30 common frames omitted 

安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserProfileService userProfileService;

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;

    @Autowired
    public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userProfileService);
        auth.authenticationProvider(authenticationProvider());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userProfileService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/javascript/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http    .httpBasic()
                    .and()
                .authorizeRequests()
                    .antMatchers("/", "/index.html", "/home.html", "/getLatestPost", "/application-socket-conn/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()
                .csrf().disable();
    }
}

网络套接字配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //Javascript connection subscribes to this URI
        config.enableSimpleBroker("/main-page-feed");

        //STOMP messages are sent to this URI + suffix
        config.setApplicationDestinationPrefixes("/thought-bubble");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //URI used for SockJS connection
        registry.addEndpoint("/application-socket-conn").withSockJS();
    }
}

自定义 AccessDenied 处理程序(未捕获它):

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    @Qualifier("clientOutboundChannel")
    private MessageChannel clientOutboundChannel;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Message<String> message = new Message<String>() {
            @Override
            public String getPayload() {
                return "Access denied.";
            }

            @Override
            public MessageHeaders getHeaders() {
                return null;
            }
        };

        clientOutboundChannel.send(message);
    }
}

网络套接字 Controller

@RestController
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/push-to-queue")
    public void pushThoughtToQueue(@Payload ThoughtEntity entity) throws Exception {
        Date today = new Date(Calendar.getInstance().getTimeInMillis());

        entity.setPostDate(today);
        entity.setFavoriteCount(0);

        this.simpMessagingTemplate.convertAndSend("/main-page-feed/thought-queue", entity);
    }
}

我认为 AffirmativeBased 是被抛出的类,但我不确定为什么,或者为什么普通(非网络套接字)spring security 在拒绝访问时不这样做到某事。就像我说的,它拒绝访问是正确的,但它会抛出运行时异常并通过 websocket 将丑陋的堆栈跟踪发送回客户端。

更新:

我已经意识到无论用户是否登录都会抛出异常,所以我认为这是一个不同的问题。就像我说的,我没有为这个应用程序使用 ROLE,我认为这与它有关。我目前正在研究匿名角色,因为我认为这与正在发生的事情有关。

最佳答案

您的 websocket 客户端代码似乎没有发送授权信息。通常默认情况下不会处理它,您应该创建自己的方式来连接安全性。

在我的例子中,我使用的是 oauth 授权,并且必须指定特定的 header Authorization : Bearer _uuid_token_,这是在 stompClient 连接期间指定的。

查看此代码段以获得总体思路。 (我正在使用 AngularJS)

(function() {
    'use strict';
    /* globals SockJS, Stomp */

    angular
        .module('myApp')
        .factory('global_WebSocket', GlobalWebSocketClient);

    GlobalWebSocketClient.$inject = ['$window', 'localStorageService', '$q'];

    function GlobalWebSocketClient($window, localStorageService, $q) {

        var connected = $q.defer();

        var established = {established: false} ;

        var loc = $window.location;
        var url = loc.protocol + '//' + loc.host + loc.pathname + 'websocket';
        var token = localStorageService.get('token');
        if (token && token.expires_at && token.expires_at > new Date().getTime()) {
            url += '?access_token=' + token.access_token;
        } else {
            url += '?access_token=no token';
        }

        /*jshint camelcase: false */
        var socket = new SockJS(url);


        /*jshint camelcase: false */
        var stompClient = Stomp.over(socket);
        var headers = {
            Authorization : 'Bearer ' + token.access_token,
        };

        stompClient.debug = null;


        var establishConnection = function() {
            stompClient.connect(headers, function() {
                established.established = true;
                connected.resolve('success');
            }, function(error) {
                console.log("ERROR CONNECTNG!");
                console.log(error.headers);
                establishConnection();
            });
        };


        establishConnection();



        return {
            connected: connected,
            client: stompClient,
            established: established
        };
    }
})();

如你所见,这段代码在构造url时添加了access_token,并指定了Authorization headers,这些Authorization headers是之前建立的,已经存储在localStorageService中。我猜您假设您的客户端默认发送 header ,但使用 sockjs 时并非如此。

然后我可以创建这样的客户端服务

(function() {
    'use strict';
    /* globals SockJS, Stomp */

    angular
        .module('myApp')
        .factory('synchronization_Status', SynchronizationFileTrackerService);

    SynchronizationFileTrackerService.$inject = ['global_WebSocket'];

    function SynchronizationFileTrackerService (global_WebSocket) {
        var stompClient = global_WebSocket;
        var subscriber = {} ;


        return {
            subscribe: subscribe,
            unsubscribe: unsubscribe
        };

        function unsubscribe(target) {
            if (subscriber[target]) {
                subscriber[target].unsubscribe();
            }
        };


        function subscribe(forTarget, handler) {
                if (stompClient.established.established) {
                    subscriber[forTarget] = stompClient.client.subscribe('/synchronization/status/' + forTarget, function(data) {
                        data = angular.fromJson(data.body);
                        handler(angular.fromJson(data));

                    });
                } else {
                    stompClient.connected.promise.then(
                        function() {
                            subscriber[forTarget] = stompClient.client.subscribe('/synchronization/status/' + forTarget, function(data) {
                                data = angular.fromJson(data.body);
                                handler(angular.fromJson(data));

                            });
                        }, null, null);
                }
        }
    }
})();

并在我的 UI 代码中重用此类服务​​,就像这样简单:

synchronization_Status.subscribe(ctrl.id, funciton(response){ctrl.currentStatus = response} );

关于java - Spring Websocket 安全抛出 AccessDeniedException,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41924104/

相关文章:

java - CORS 隐藏 header ?

java - Spring Boot打包jar时,Thymeleaf不会加载模板

java - Spring boot问题主类给出编译错误

java - 由 IllegalStateException : This web container has not yet been started 引起的 Hibernate InstantiationException

java - 使用适配器设置没有 TextView 的 ListView 高度

java - DynamoDB 通过对象内属性的属性相等性扫描

java - 在Android中实现库

java - 如何伪造 vaadin session 进行集成测试?

java - JPA 实体是否存储对实体管理器的引用?

java - 运行我的应用程序时出现错误 "context initialization failed"