【问题标题】:Spring-websockets : Spring security authorization not working inside websocketsSpring-websockets:Spring 安全授权在 websockets 中不起作用
【发布时间】:2019-06-15 21:10:35
【问题描述】:

我正在开发一个 Spring-MVC 应用程序,其中我们使用 Spring-security 进行身份验证和授权。我们正在努力迁移到 Spring websockets,但是在将经过身份验证的用户获取到 websocket 连接中时遇到了问题。 websocket 连接中根本不存在安全上下文,但可以正常使用常规 HTTP。我们做错了什么?

Websocket配置:

@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>

【问题讨论】:

    标签: java spring spring-security websocket spring-websocket


    【解决方案1】:

    我记得在我正在从事的一个项目中偶然发现了同样的问题。由于我无法使用 Spring 文档找出解决方案 - 并且 Stack Overflow 上的其他答案对我不起作用 - 我最终创建了一个解决方法。

    诀窍本质上是强制应用程序在 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 来自哪个类吗?谢谢。
    • 类应该是WebSocketAuthInterceptorAdapter。我编辑了我的答案。
    • 我实现了类似的代码,我可以在 Interceptor 中获取用户,但是当我使用 SecurityContextHolder.getContext() 时在服务中出现错误:An Authentication object was not found in the SecurityContext
    • 抱歉这么晚才回复。自从我做任何与 Spring Websockets 相关的事情以来已经有一段时间了。我认为这个错误一定意味着 Spring Security 中没有正确配置某些东西,并且框架没有解释您设置的授权。
    【解决方案2】:

    如果您使用 SockJS + Stomp 并正确配置了您的安全性,您应该能够通过常规用户名/密码验证器(如 @AlgorithmFromHell)进行连接

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

    您也可以通过 http://{END_POINT}/access_token={ACCESS_TOKEN} 进行连接。 Spring security 应该能够选择它并通过 ResourceServerTokenServices 执行 loadAuthentication(access_token)。完成后,您可以通过将其添加到 AbstractSessionWebSocketMessageBrokerConfigurer 或 WebSocketMessageBrokerConfigurer 的 impl 中来获取您的委托人。这样做时,由于某种原因,加载的 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;
          }
        });
      }

    【讨论】:

      猜你喜欢
      • 2016-08-04
      • 2017-05-10
      • 2012-04-07
      • 2015-02-05
      • 1970-01-01
      • 2013-11-02
      • 1970-01-01
      • 2022-12-15
      相关资源
      最近更新 更多