WebSocket实战集成SSL,到阿里云生成SSL(网络编程安全二)

2,826 阅读10分钟

从阿里云服务器购买、域名购买、SSL免费购买。

一 WebSocket实战

1.1 认识WebSocket

上图说明

  1. 发送连接请求 客户端通过ws://host:port/ 的请求地址发起WebSocket请求连接。由JavaScript实现
    WebSocket API与服务器建立WebSocket连接。host服务器ip,port为端口。

  2. 握手 服务器端接受请求后,会解析请求头信息,根据升级协议判断请求是否为WebSocket请求,并取出请求信息中的 Sec-WebSocket-Key字段的数据。按照某种算法生成一个新的字符串序列放到请求头Sec-WebSocket-Accept中。 Sec-WebSocket-Accept:服务器接受客户端HTTP协议升级证明。

  3. 建立WebSocket连接 客户端接受服务器的响应后,同样会解析请求头信息,取出Sec-WebSocket-Accept字段。 并用服务器端相同的算法对之前Sec-WebSocket-Accept的数据处理,看看处理后的和取出 的Sec-WebSocket-Accept对比。一样连接建立成功,不一样建立失败。

1.2 HTTP和WebSocket


上图说明
上图可以看出,HTTP每次请求都需要建立连接。WebSocket类似一个长链接,一旦建立后,后续数据
都是以帧序列方式传递。

  1. HTTP和WebSocket关系
    相同点:HTTP和WebSocket都是可靠的传输协议,都是应用层协议。
    不同点:WebSocket是双向通信协议,模拟socket协议,可以双向发送和接受数据。HTTP是单向的。

  2. WebSocket建立连接细节 WebSocket建立握手,数据是通过HTTP传输的。但是建立连接后,在真正传输的时候不需要HTTP协议的。 WebSocket中,浏览器和服务器进行一个握手之后,然后单独建立一条TCP的通信通道,进行数据传递。

  3. WebSocket优点 双工通信代替轮询。可以做即时通信和消息推送。

1.3 SpringBoot集成WebSocket

  1. 依赖
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 开启WebSocket支持端点
@Configuration
public class Config {
    @Bean
    public ServerEndpointExporter  serverEndpointExporter(){
        return  new ServerEndpointExporter();
    }
}
  1. 创建server核心类
@ServerEndpoint("/ws/{userId}")
@Component
public class WebSocketServer {
    //日志
    static Log log = LogFactory.getLog(WebSocketServer.class);
    //在线数量
    private static final AtomicInteger onlineCount = new AtomicInteger(0);
    //处理客户端连接socket
    private static Map<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    //会话信息
    private Session session;
    //用户信息
    private String userId = "";

    /*
     * @Description:  打开WebSokcet连接
     */
    @OnOpen
    public void onOPen(@PathParam("userId") String userId, Session session) {
        //处理session和用户信息
        this.session = session;
        this.userId = userId;
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            webSocketMap.put(userId, this);
        } else {
            webSocketMap.put(userId, this);
            //增加在线人数
            addOnlineCount();
        }
        try {
            //处理连接成功消息的发送
            sendMessage("Server>>>>远程WebSoket连接成功");
            log.info("用户" + userId + "成功连接,当前的在线人数为" + getOnlineCount());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
     * @Description:  关闭连接
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            subOnlineCount();
        }
        log.info("用户退出....");
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount.decrementAndGet();
    }

    /*
     * @Description:消息中转
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        if (StringUtils.isNotEmpty(message)) {
            try {
                //解析消息
                JSONObject jsonObject = JSON.parseObject(message);
                String toUserId = jsonObject.getString("toUserId");
                String msg = jsonObject.getString("msg");
                if (StringUtils.isNotEmpty(toUserId) && webSocketMap.containsKey(toUserId)) {
                    webSocketMap.get(toUserId).sendMessage(msg);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /*
     * @Description: 服务端向客户端发送数据
     */
    public void sendMessage(String s) throws IOException {
        this.session.getBasicRemote().sendText(s);
    }

    /*
     * @Description: 获取在线人数的数量
     */
    public static synchronized AtomicInteger getOnlineCount() {
        return onlineCount;
    }

    /*
     * @Description: 增加在线人数
     */
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount.incrementAndGet();
    }

    /*
     * @Description: 服务器消息推送
     */
    public static boolean sendInfo(String message, @PathParam("userId") String userId) throws IOException {
        boolean  flag=true;
        if (StringUtils.isNotEmpty(userId) && webSocketMap.containsKey(userId)) {
            webSocketMap.get(userId).sendMessage(message);
        } else {
            log.error("用户" + userId + "不在线");
            flag=false;
        }
        return  flag;
    }
}
  1. 创建控制器
@RestController
public class WebSocketController {
    @RequestMapping("im")
    public ModelAndView page() {
        return new ModelAndView("ws");
    }

    /*
     * @Description: 消息推送
     */
    @RequestMapping("/push/{toUserId}")
    public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws Exception {

        boolean flag = WebSocketServer.sendInfo(message, toUserId);
        return flag == true ? ResponseEntity.ok("消息推送成功...") : ResponseEntity.ok("消息推送失败,用户不在线...");
    }


}
  1. 创建消息发送HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <link href="../css/index.css" rel="stylesheet">

</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openWebSocket() {
        if (typeof(WebSocket) == "undefined") {
            console.log("对不起,您的浏览器不支持WebSocket");
        } else {
            var webSocketUrl = "ws://localhost/ws/" + $("#userId").val();
            if (socket != null) {
                socket.close();
                socket = null;
            }
            socket = new WebSocket(webSocketUrl);
            //打开
            socket.onopen = function () {
                console.log("Client>>>>WebSocket已打开");
            };
            //获取消息
            socket.onmessage = function (msg) {
                console.log(msg.data);
                $("#msg").val(msg.data)

            };
            //关闭
            socket.onclose = function () {
                console.log("Client>>>>WebSocket已关闭");
            };
            //发生错误
            socket.onerror = function () {
                console.log("Client>>>>WebSocket发生了错误");
            }
        }
    }

    function sendMessage() {
        if (typeof(WebSocket) == "undefined") {
            console.log("对不起,您的浏览器不支持WebSocket");
        } else {
            socket.send('{"toUserId":"' + $("#toUserId").val() + '","msg":"' + $("#msg").val() + '"}');
        }
    }
</script>

<body>
<div id="panel">
    <div class="panel-header">
        <h2>即时通讯IM</h2>
    </div>
    <div class="panel-content">
        <div class="user-pwd">
            <button class="btn-user"></button>
            <input id="userId" name="userId" type="text" value="张三">
        </div>
        <div class="user-pwd">
            <button class="btn-user"></button>
            <input id="toUserId" name="toUserId" type="text" value="李四">
        </div>
        <div class="user-msg">
            <input id="msg" name="msg" type="text" value="">
        </div>
        <div class="panel-footer">
            <button class="login-btn"  onclick="openWebSocket()" >连接WebSocket</button>
            <button class="login-btn" onclick="sendMessage()">发送消息</button>
        </div>
    </div>
</body>
</html>
  1. 访问路径
http://localhost/im
  1. WebSocket建立连接请求头分析
WebSocket利用HTTP建立我握手连接,必须由浏览器发起。相对于HTTP协议多了几个东西,
告诉Apache、Nginx等服务器,本次发起的是Websocket协议。
a GET请求的地址不是类似http/,而是以ws://开头的地址;
b 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
c Sec-WebSocket-Key是由浏览器随机生成的,是用于标识这个连接,并非用于加密数据;
d Sec-WebSocket-Version指定了WebSocket的协议版本;
e Sec-WebSocket-Extensions:请求扩展;
f code101代表了服务端已经理解了客户端请求。

1.4 自签名证书HTTPS开发

1.4.1 生成keystore证书

  1. JDK中keytool是一个证书管理工具,可以生成自签名证书
keytool -genkey -alias czbk -keypass 123456 -keyalg RSA -keysize 1024 -validity
365 -keystore c:/czbk.keystore -storepass 123456

命令解释

keytool
-genkey
-alias tomcat(别名)
-keypass 123456(别名密码)
-keyalg RSA(生证书的算法名称,RSA是一种非对称加密算法)
-keysize 1024(密钥长度,证书大小)
-validity 365(证书有效期,天单位)
-keystore c:/czbk.keystore(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)
- storetype (指定密钥仓库类型)

2. 查看生成文件 并将文件复制到spring boot项目中的resources目录中。

  1. 配置application.properties
# 端口
http.port=7777
server.port=8888
# 指定签名文件,对应生成的密钥库文件
server.ssl.key-store=classpath:czbk.keystore
# 指定签名密码,设置的密钥库指令
server.ssl.key-store-password=123456
# 指定密钥仓库类型,JKS
server.ssl.key-store-type=JKS
# 指定别名,生成密钥库的时候进行了设定
server.ssl.key-alias=czbk
  1. 增加配置类
@Configuration
public class HttpRedirectHttps {
    @Value("${http.port}")
    Integer httpPort;
    @Value("${server.port}")
    Integer httpsPort;

    /*
     * @Description: http重定向到https
     * @Method: servletWebServerFactory
     * @Param: []
     * @Update:
     * @since: 1.0.0
     * @Return: org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
     *
     */
    @Bean
    public TomcatServletWebServerFactory servletWebServerFactory() {
        TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcatServletWebServerFactory.addAdditionalTomcatConnectors(createConnector());
        return tomcatServletWebServerFactory;
    }


    public Connector createConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(httpPort);
        connector.setSecure(false);
        connector.setRedirectPort(httpsPort);
        return connector;
    }
}
  1. 验证集成结果
http://localhost:7777/im 跳转到 https://localhost:8888/im

上图说明 我们自己生成的https证书不被谷歌浏览器认可。更换浏览器即可

1.5 Nginx代理SSL

  1. 好处 springboot程序不需要加入ssl任何配置。 解决http重定向https 解决Wss服务问题 Nginx版本必须大于1.3

  2. 关于nginx

自1.3 版本开始,Nginx就支持 WebSocket,并且可以为 WebSocket 应用程序做反向代理和负载均衡。
WebSocket  HTTP 是两种不同的协议,但是 WebSocket 中的握手和 HTTP 中的握手兼容,它使用
HTTP 中的 Upgrade 协议头将连接从 HTTP 升级到 WebSocket,当客户端发过来一个 Connection:
Upgrade请求头时,其实Nginx是不知道的。所以,当 Nginx 代理服务器拦截到一个客户端发来的
Upgrade 请求时,需要我们显式的配置Connection、Upgrade头信息,并使用 101(交换协议)返回响
应,在客户端、代理服务器和后端应用服务之间建立隧道来支持 WebSocket。
  1. Nginx流程
使用 Nginx 反向代理来解决 WebSocket 的 wss 服务问题,即客户端通过 Wss 协议连接 Nginx 然后
Nginx 通过 Ws 协议和 Server 通讯。也就是说 Nginx 负责通讯加解密,Nginx 到 Server 是明文的。


Nginx配置文件

#********分割线******************
events {
	worker_connections 1024;
}
#********分割线******************
http {
  map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;}
  #********分割线******************
  server {
    listen 80;
    server_name websocket.nginx.com;
    add_header Strict-Transport-Security max-age=15768000;
    return 301 https://$server_name$request_uri;
  }
  #********分割线******************
  server {
      listen 443 ssl;
      server_name websocket.nginx.com
      ssl on;
      ssl_certificate c:\czbk.crt;
      ssl_certificate_key c:\czbk.key;
      ssl_session_timeout 5m;
      ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;
      ssl_ciphers ECDHE-RSA-AES128-GCMSHA256:
      ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
      ssl_prefer_server_ciphers on;
      #********页面http访问的时候也可以使用/im*****************
      #location /im/ {
      #proxy_pass http://localhost:8888/im;
      #}
      #********http跳转与wss协议调用(自动判断请求协议)******************
      location / {
          proxy_pass http://localhost:8888;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "Upgrade";
          proxy_set_header X-Real-IP $remote_addr;
      }
  }
  #********分割线******************
}

map指令的作用

#该作用主要是根据客户端请求中$http_upgrade 的值,来构造改变$connection_upgrade的值,即根
据变量$http_upgrade的值创建新的变量$connection_upgrade,
#创建的规则就是{}里面的东西。其中的规则没有做匹配,因此使用默认的,即 $connection_upgrade
的值会一直是 upgrade。然后如果 $http_upgrade为空字符串的话,那值会是 close

核心配置

ssl_certificate 证书其实是个公钥,它会被发送到连接服务器的每个客户端
ssl_certificate_key 私钥是用来解密的,所以它的权限要得到保护但nginx的主进程能够读取。
ssl_session_timeout : 客户端可以重用会话缓存中ssl参数的过期时间

ssl_protocols指令用于支持加密协议

ssl_ciphers选择加密套件,不同的浏览器所支持的套件(和顺序)可能会不同。这里指定的是
OpenSSL库能够识别的写法,你可以通过 openssl -v cipher 'RC4:HIGH:!aNULL:!MD5'(后面是你所指
定的套件加密算法) 来看所支持算法。
加密套件 之间用冒号分隔,加密套件 前有感叹号的表示必须废弃。
ssl_prefer_server_ciphers on设置协商加密算法时,优先使用我们服务端的加密套件,而不是客户端浏
览器的加密套件

访问流程

1、http://websocket.nginx.com/im/访问进入到 listen 80;
2、接着跳转到listen 443
3、进入到443后分别(wss或者http)进行跳转

重载 Nginx 服务

主流数字证书都有哪些格式?
一般来说,主流的Web服务软件,通常都基于两种基础密码库:OpenSSL和Java。
Tomcat、Weblogic、JBoss等,使用Java提供的密码库。通过Java的Keytool工具,生成Java
Keystore(JKS)格式的证书文件。
Apache、Nginx等,使用OpenSSL提供的密码库,生成PEM、KEY、CRT等格式的证书文件
此处我们需要crt和key文件
我们需要将证书kestore导出crt和key
1、使用kse-543转p12
2、通过P12生成crt和key文件

2 阿里云HTTPS开发

2.1 准备工作

  1. 准备工作
a 阿里云ecs一个
b 域名一个(www.itheima.cloud)
c ca证书一份(用来支持https)(需要绑定域名)
d 本地打包好的springboot项目 (需要打包上传到阿里云)
e ftp客户端一个,用来把jar传到阿里云服务器上
  1. 阿里云域名申请
https://mi.aliyun.com/


3. 选择域名直接购买 4. 查看域名状态

https://dc.console.aliyun.com/next/index?
spm=5176.100251.recommends.ddomain.6ffe4f15tozYLa#/domain/details/info?
saleId=DT49H3EX462ZAYP&domain=itheima.cloud


5. whois结果
6. 阿里云ECS服务器申请

https://www.aliyun.com/activity/618/index?
spm=5176.12825654.a9ylfrljh.d111.e9392c4aU65uab&scm=20140722.2188.2.2170

7. 支付成功,查看ECS实例

https://ecs.console.aliyun.com/?spm=5176.2020520132.productsgrouped.
decs.a1597218YHc0M5#/server/region/cn-beijing
  1. 远程登录--重置密码
阿里云服务器购买之后,新的实例需要设置root登录密码之后才能正常操作,不然就登录不了。重置实例
登录密码的时候,适用于在新创建时未设置密码或者忘记密码的情况。对于正在运行的实例,需要在重置
实例登录密码之后重启实例才能使新的密码生效

9. 通过外网IP进行连接

2.2 阿里云SSL证书申请

https://common-buy.aliyun.com/?
spm=5176.7968328.1266638..213d1232uExCSm&commodityCode=cas#/buy
  1. 选择SSL证书(应用安全)
  2. 购买证书
  3. 购买免费域名(单域名---DV/SSL---免费版)
  4. 开始支付
  5. 支付成功
  6. 支付成功后在
  7. 点击证书申请,进行【证书与域名绑定】

2.3 域名与ECS服务器绑定

https://dns.console.aliyun.com/?
spm=5176.100251.111252.22.3a8b4f15HNfTsl#/dns/setting/itheima.cloud
  1. DNS解析
  2. 点击确定后出现www和@

2.4 SpringBoot部署到阿里云

  1. 阿里云远程部署
  2. 配置文件修改(证书路径要带上 classpath)
http:
	port: 7777
server:
  # 端口 使用HTTPS默认端口
  port: 443
  #HTTPS加密配置
  ssl:
    #证书路径
    key-store: classpath:4108720_www.itheima.cloud.pfx
    #证书密码
    key-store-password: 5nLB0iXq
  1. centos7 下杀死占用端口的进程
# 根据端口号得到其占用的进程的详细信息
netstat -lnp|grep 443
# 查看进程的详细信息
ps "pid"
#直接杀掉占用端口的进程 -9是强制关闭
kill -9 "pid"
或者
netstat -tunlp|grep 443
jobs 查看nohup运行的程序
ps -ef | grep java
kill- 9 id
  1. 将springboot项目打包(使用idea的packge),并启动
nohup java -jar itheima-websocket-aliyun-https-1.0-SNAPSHOT.jar &
  1. 访问阿里云HTTPS应用程序(安全组配置)
https://ecs.console.aliyun.com/?spm=a2c1d.8251892.recommends.decs.10565b76fQAfCc#/ser
ver/region/cn-beijing

ip访问
http://101.201.232.138:7777/im
域名访问
https://www.itheima.cloud/im

注意:

第一次部署证书的时候,是可以访问的,大约过滤两分钟后,被阿里云监测到后就无法访问了
因为域名尚未备案成功
转到http访问,就会有提示尚未备案的提示