如何防止 Spring 生成默认的 simpSessionId?

How to prevent Spring from generating default simpSessionId?

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

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

但是,在收到消息时,spring 它总是将提供的 header 放在名为 nativeHeaders 的键中,并将默认的 simpSessionId 放在 header 根目录中.

{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 获取提供的 session id 吗?

已编辑

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

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

我在手机上使用 webstomp-client phone,Spring 4.1.9,Spring 安全 4.1,Spring Session 1.2。 0.

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

我有两个问题:

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

@配置 @EnableRedisHttpSession(maxInactiveIntervalInSeconds=1200) public class Session配置{

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

}

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

@配置 @EnableWebSecurity @Order(100) public class 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;
    }
  }

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

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

如果您还没有阅读 Spring 消息传递的 Authentication 参考文档,我会先介绍一下:

When a WebSocket handshake is made and a new WebSocket session is created, Spring’s WebSocket support automatically propagates the java.security.Principal from the HTTP request to the WebSocket session. After that every message flowing through the application on that WebSocket session is enriched with the user information. It’s present in the message as a header.

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

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

这一切都应该无缝运行。无需通过 STOMP 登录或随时传递 Spring 会话 ID。