问题背景

在18年工作的时候遇到一个WebSocket连接通过nginx转发后死活获取不到客户端真实ip的问题,在重新学习WebSocket后对于WebSocket的认识更加深入,遂总结下,方便大家交流沟通。

WebSocket协议

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

其中握手的动作是HTTP协议,进行协议升级后,建立WebSocket连接(建议多读几遍)

问题分析

如果在Websocket连接建立之后在获取ip,能够获取的信息是有限的,因为socket连接本身就获取不到什么信息.在nginx中转后,你建立的socket连接是和nginx建立的.所以ip获取的是127.0.0.1。

核心 通过在握手的第一步进行 鉴权 获取ip 获取Session中的信息

下面是简单的WebSocket连接,支持spring bean


import org.springframework.session.Session;

import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Optional;

/**
 * @author wangqimeng
 * @date 2020/3/25 下午9:53
 */
public class HttpSessionConfigurator extends SpringBootConfigurator {


    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        //如果header中没有token字段 校验失败.
        //如果会话过期 校验失败...
        if (request.getHttpSession() instanceof Session) {
            //认为会话是有效的
            //然后讲Session中的信息取出,进行认证.
        }
        //这块就可以通过header进行认证了
        sec.getUserProperties().put("X-Real-Ip", Optional.ofNullable(request.getHeaders().get("X-Real-Ip")));
    }
}

import cn.boommanpro.tomato.web.context.ApplicationContextRegister;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.socket.server.standard.SpringConfigurator;

import javax.websocket.server.ServerEndpointConfig;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * copy from {@link SpringConfigurator}
 *
 * @author wangqimeng
 * @date 2020/3/25 下午11:07
 */
public class SpringBootConfigurator extends ServerEndpointConfig.Configurator {


    private static final String NO_VALUE = ObjectUtils.identityToString(new Object());

    private static final Log logger = LogFactory.getLog(SpringConfigurator.class);

    private static final Map<String, Map<Class<?>, String>> cache =
            new ConcurrentHashMap<>();


    @SuppressWarnings("unchecked")
    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
        ApplicationContext context = ApplicationContextRegister.getApplicationContext();
        if (context == null) {
            String message = "Failed to find the root WebApplicationContext. Was ContextLoaderListener not used?";
            logger.error(message);
            throw new IllegalStateException(message);
        }

        String beanName = ClassUtils.getShortNameAsProperty(endpointClass);
        if (context.containsBean(beanName)) {
            T endpoint = context.getBean(beanName, endpointClass);
            if (logger.isTraceEnabled()) {
                logger.trace("Using @ServerEndpoint singleton " + endpoint);
            }
            return endpoint;
        }

        Component ann = AnnotationUtils.findAnnotation(endpointClass, Component.class);
        if (ann != null && context.containsBean(ann.value())) {
            T endpoint = context.getBean(ann.value(), endpointClass);
            if (logger.isTraceEnabled()) {
                logger.trace("Using @ServerEndpoint singleton " + endpoint);
            }
            return endpoint;
        }

        beanName = getBeanNameByType(context, endpointClass);
        if (beanName != null) {
            return (T) context.getBean(beanName);
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Creating new @ServerEndpoint instance of type " + endpointClass);
        }
        return context.getAutowireCapableBeanFactory().createBean(endpointClass);
    }

    @Nullable
    private String getBeanNameByType(ApplicationContext context, Class<?> endpointClass) {
        String contextId = context.getId();

        Map<Class<?>, String> beanNamesByType = cache.get(contextId);
        if (beanNamesByType == null) {
            beanNamesByType = new ConcurrentHashMap<>();
            cache.put(contextId, beanNamesByType);
        }

        if (!beanNamesByType.containsKey(endpointClass)) {
            String[] names = context.getBeanNamesForType(endpointClass);
            if (names.length == 1) {
                beanNamesByType.put(endpointClass, names[0]);
            } else {
                beanNamesByType.put(endpointClass, NO_VALUE);
                if (names.length > 1) {
                    throw new IllegalStateException("Found multiple @ServerEndpoint's of type [" +
                            endpointClass.getName() + "]: bean names " + Arrays.asList(names));
                }
            }
        }

        String beanName = beanNamesByType.get(endpointClass);
        return (NO_VALUE.equals(beanName) ? null : beanName);
    }
}

import cn.boommanpro.tomato.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

/**
 * @author wangqimeng
 * @date 2020/3/25 下午8:49
 */
@Slf4j
@Component
@ServerEndpoint(value = "/websocket", configurator = HttpSessionConfigurator.class)
public class WebSocketPractice {

    private final SysUserService sysUserService;

    public WebSocketPractice(SysUserService sysUserService) {
        this.sysUserService = sysUserService;
    }

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        log.info("客户端ID:[{}],onOpen 建立成功", session.getId());
    }

    @OnClose
    public void onClose(Session session) {
        log.info("客户端ID:[{}],onClose 连接关闭", session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("客户端ID:[{}],收到客户端消息:{}", session.getId(), message);
    }

    @OnError
    public void onError(Session session, Throwable t) {
        log.error("Websocket Error 客户端ID:" + session.getId(), t);
    }
}