2

我正在尝试使用 websockets 和 STOMP 设置 spring。

在客户端,我发送一个标头变量 'simpSessionId':%session_id%

但是,在接收到消息时,spring 总是将提供的标头放在名为 nativeHeaders 的键中,并将默认的 simpSessionId 放在标头根中。

{simpMessageType=MESSAGE, stompCommand=SEND, nativeHeaders={SPRING.SESSION.ID=[5b1f11d0-ad92-4855-ae44-b2052ecd76d8], Content-Type=[application/json], X-Requested-With=[XMLHttpRequest], simpSessionId=[5b1f11d0-ad92-4855-ae44-b2052ecd76d8], accept-version=[1.2,1.1,1.0], heart-beat=[0,0], destination=[/mobile-server/ping], content-length=[15]}, simpSessionAttributes={}, simpSessionId=1, simpDestination=/mobile-server/ping}

有什么想法可以让 spring 获取提供的会话 ID 吗?

已编辑

好的,我有一个手机应用程序和一个网站访问同一台服务器。我需要能够在手机应用程序上设置 webocket。

在手机应用程序上,我通过传统的 REST 端点登录到服务器,如果成功,我会在响应中收到一个 session-id。

我在手机上使用webstomp-client,Spring 4.1.9,Spring Security 4.1,Spring Session 1.2.0。

理想情况下,我会使用令牌登录套接字 CONNECT 上的 STOMP websocket,但我知道他目前是不可能的,因为 webstomp-client 不会在 CONNECT 上传递自定义标头。

我有两个问题:

  • 如何在后续请求中传递我在 REST 登录中检索到的会话 ID?我尝试添加诸如 SPRING.SESSION.ID 之类的标头,但单步执行代码时,我总是看到消息处理返回到始终默认为 1、2 等的 simpSessionId。我尝试扩展 AbstractSessionWebsocketMessageBrokerConfigurer,但它没有获取我的会话 ID,它总是在 simpSessionAttributes 中查找,它总是为空的。

  • 该代码似乎还试图获取 http 会话,这是一个 Web 浏览器场景。我假设我应该忽略这个

  • 会话到期。对于可能已过期的会话应该采取什么策略?我不应该也传递一个记住我风格的身份验证令牌吗?还是我应该依赖一些永恒的无状态会话?这对我来说并不清楚,这方面似乎没有记录。

显然,我做错了什么。这是我的配置:

@Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds=1200) 公共类 SessionConfig {

@Inject
ContentNegotiationManager contentNegotiationManager;

@Bean
public RedisConnectionFactory redisConnectionFactory(
        @Value("${spring.redis.host}") String host,
        @Value("${spring.redis.password}") String password,
        @Value("${spring.redis.port}") Integer port) {
    JedisConnectionFactory redis = new JedisConnectionFactory();
    redis.setUsePool(true);
    redis.setHostName(host);
    redis.setPort(port);
    redis.setPassword(password);
    redis.afterPropertiesSet();
    return redis;
}

@Bean
  public RedisTemplate<String,ExpiringSession> redisTemplate(RedisConnectionFactory connectionFactory) {
      RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
      template.setKeySerializer(new StringRedisSerializer());
      template.setHashKeySerializer(new StringRedisSerializer());
      template.setConnectionFactory(connectionFactory);
      return template;
  }

@Bean
public <S extends ExpiringSession>SessionRepositoryFilter<? extends ExpiringSession> sessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    return new SessionRepositoryFilter<S>(sessionRepository);
}

@Bean
  public HttpSessionEventPublisher httpSessionEventPublisher() {
          return new HttpSessionEventPublisher();
  }

@Bean
public HttpSessionStrategy httpSessionStrategy(){
    return new SmartSessionStrategy();
}

@Bean
  public CookieSerializer cookieSerializer() {
          DefaultCookieSerializer serializer = new DefaultCookieSerializer();
          serializer.setCookieName("JSESSIONID"); 
          serializer.setCookiePath("/");
          serializer.setUseSecureCookie(true);
          serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); 
          return serializer;
  }

}

===

public class SessionWebApplicationInitializer extends AbstractHttpSessionApplicationInitializer {

    public SessionWebApplicationInitializer() {
    }

    public SessionWebApplicationInitializer(Class<?>... configurationClasses) {
        super(configurationClasses);
    }

    @Override
    protected void beforeSessionRepositoryFilter(ServletContext servletContext) {
        Dynamic registration = servletContext.addFilter("openSessionInViewFilter", new OpenSessionInViewFilter());
        if (registration == null) {
            throw new IllegalStateException(
                    "Duplicate Filter registration for openSessionInViewFilter. Check to ensure the Filter is only configured once.");
        }
        registration.setAsyncSupported(false);
        EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes();
        registration.addMappingForUrlPatterns(dispatcherTypes, false,"/*");
    }

}

==

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebsocketMessageBrokerConfigurer<S>{

    @Inject
    SessionRepository<S> sessionRepository;

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");

        config.setApplicationDestinationPrefixes("/mobile-server");

        config.setUserDestinationPrefix("/mobile-user");

    }

    @Override
    public void configureStompEndpoints(StompEndpointRegistry registry) {
        registry
            .addEndpoint("/ws")
            .setHandshakeHandler(new SessionHandShakeHandler(new TomcatRequestUpgradeStrategy()))
            .setAllowedOrigins("*")
            .withSockJS()
            .setSessionCookieNeeded(false)
            ;
    }

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(512 * 1024);
        registration.setSendBufferSizeLimit(1024 * 1024);
        registration.setSendTimeLimit(40000);
    }

    @Bean
    public WebSocketConnectHandler<S> webSocketConnectHandler(SimpMessageSendingOperations messagingTemplate, UsorManager userMgr) {
        return new WebSocketConnectHandler<S>(messagingTemplate, userMgr);
    }

    @Bean
    public WebSocketDisconnectHandler<S> webSocketDisconnectHandler(SimpMessageSendingOperations messagingTemplate, WebSocketManager repository) {
        return new WebSocketDisconnectHandler<S>(messagingTemplate, repository);
    }

}

====

@Configuration
public class WebSocketSecurity extends AbstractSecurityWebSocketMessageBrokerConfigurer{

    ApplicationContext context = null;

    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }

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

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
            .nullDestMatcher().permitAll()
            .simpSubscribeDestMatchers("/user/queue/errors").permitAll()
            .simpDestMatchers("/mobile-server/ping").authenticated()
            .simpDestMatchers("/mobile-server/csrf").authenticated()
            .simpDestMatchers("/mobile-server/**").hasRole("ENDUSER")
            .simpSubscribeDestMatchers("/user/**", "/topic/**").hasRole("ENDUSER")
            .anyMessage().denyAll();
    }

}

=== 为了简洁起见,我已经删除了一些额外的安全配置。

@Configuration @EnableWebSecurity @Order(100) 公共类 SecurityConfig 扩展 WebSecurityConfigurerAdapter {

private static final String REMEMBER_ME_COOKIE = "SPRING_SECURITY_REMEMBER_ME_COOKIE";

@Inject
FilterInvocationSecurityMetadataSource securityMetadataSource;

@Inject
SessionRepositoryFilter<? extends ExpiringSession> sessionRepositoryFilter;

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {

    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setSaltSource(saltSource);
    provider.setUserDetailsService(userMgr);
    provider.setPasswordEncoder(passwordEncoder);
    provider.setMessageSource(messages);
    auth.authenticationProvider(provider);

}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
}

@Bean
public AuthenticationTokenProcessingFilter authenticationTokenProcessingFilter() throws Exception{
    return new AuthenticationTokenProcessingFilter(authenticationManagerBean());
}

@Bean
public FilterSecurityInterceptor myFilterSecurityInterceptor(
        AuthenticationManager authenticationManager, 
        AccessDecisionManager accessDecisionManager,
        FilterInvocationSecurityMetadataSource metadataSource){
    FilterSecurityInterceptor interceptor = new FilterSecurityInterceptor();
    interceptor.setAuthenticationManager(authenticationManager);
    interceptor.setAccessDecisionManager(accessDecisionManager);
    interceptor.setSecurityMetadataSource(securityMetadataSource);
    interceptor.setSecurityMetadataSource(metadataSource);
    return interceptor;
}

@Bean
public AccessDecisionManager accessDecisionManager(SiteConfig siteConfig){
    URLBasedSecurityExpressionHandler expressionHandler = new URLBasedSecurityExpressionHandler();
    expressionHandler.setSiteConfig(siteConfig);

    WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
    webExpressionVoter.setExpressionHandler(expressionHandler);

    return new AffirmativeBased(Lists.newArrayList(
            webExpressionVoter,
            new RoleVoter(),
            new AuthenticatedVoter()
    ));
}

public PasswordFixingAuthenticationProvider customAuthenticationProvider(PasswordEncoder passwordEncoder, SaltSource saltSource){
    PasswordFixingAuthenticationProvider provider = new PasswordFixingAuthenticationProvider();
    provider.setUserDetailsService(userMgr);
    provider.setPasswordEncoder(passwordEncoder);
    provider.setSaltSource(saltSource);

    return provider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(sessionRepositoryFilter, ChannelProcessingFilter.class)
        .antMatcher("/ws/**")
        .exceptionHandling()
            .accessDeniedPage("/mobile/403")
            .and()
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
        .csrf().disable()
        .authorizeRequests()
            .antMatchers("/ws").permitAll()
            .antMatchers("/ws/websocket").permitAll()
            .antMatchers("/ws/**").denyAll();           
       .anyRequest().requiresSecure()

    ;
}

}

===

  public class SmartSessionStrategy implements HttpSessionStrategy {

    private HttpSessionStrategy browser;

    private HttpSessionStrategy api;

    private RequestMatcher browserMatcher = null;

    public SmartSessionStrategy(){
        this.browser = new CookieHttpSessionStrategy();
        HeaderHttpSessionStrategy headerSessionStrategy = new HeaderHttpSessionStrategy();
        headerSessionStrategy.setHeaderName(CustomSessionRepositoryMessageInterceptor.SPRING_SESSION_ID_ATTR_NAME);
        this.api = headerSessionStrategy;
    }

    @Override
    public String getRequestedSessionId(HttpServletRequest request) {
        return getStrategy(request).getRequestedSessionId(request);
    }

    @Override
    public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
        getStrategy(request).onNewSession(session, request, response);
    }

    @Override
    public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
        getStrategy(request).onInvalidateSession(request, response);
    }

    private HttpSessionStrategy getStrategy(HttpServletRequest request) {
        if(this.browserMatcher != null)
            return this.browserMatcher.matches(request) ? this.browser : this.api;

        return SecurityRequestUtils.isApiRequest(request) ? this.api : this.browser;
    }
  }
4

1 回答 1

3

我认为这个问题一开始是基于无效的期望。您不能传递会话 id,它也不应该被传递。您不能在 STOMP 协议级别登录,这不是它的设计工作方式。

尽管 STOMP 协议确实允许在 CONNECT 帧中传递用户凭据,但这对 STOMP over TCP 更有用。在 HTTP 场景中,我们已经有了可以依赖的身份验证和授权机制。当您到达 STOMP CONNECT 时,您必须通过 WebSocket 握手 URL 的身份验证和授权。

如果您还没有阅读过,我将从关于 STOMP/WebSocket 消息的身份验证的 Spring 参考文档开始:

当进行 WebSocket 握手并创建新的 WebSocket 会话时,Spring 的 WebSocket 支持自动将 java.security.Principal 从 HTTP 请求传播到 WebSocket 会话。之后,在该 WebSocket 会话上流经应用程序的每条消息都使用用户信息进行了丰富。它作为标题出现在消息中。

换句话说,身份验证与现有 Web 应用程序相同。暴露 WebSocket 端点的 URL 只是应用程序的另一个 HTTP 端点。保护所有其他 HTTP 端点的方式与保护 WebSocket 握手的方式相同。就像其他 HTTP 端点一样,您不传递会话 ID。相反,您位于通过 cookie 维护的现有 HTTP 会话中。

除非 Spring Security 首先对 HTTP URL 进行身份验证和授权,否则无法建立握手。STOMP 会话将从那里获取经过身份验证的用户,并且 Spring Security 提供了进一步的方法来授权单个 STOMP 消息。

这一切都应该无缝地工作。无需通过 STOMP 登录或随时传递 Spring Session id。

于 2016-06-15T16:32:30.237 回答