阅读 1635

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

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

一 WebSocket实战

1.1 认识WebSocket

上图说明

  1. 发送连接请求

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

  1. 握手

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

  1. 建立WebSocket连接

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

1.2 HTTP和WebSocket


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

  1. HTTP和WebSocket关系

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

  1. WebSocket建立连接细节

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

  1. 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 code为101代表了服务端已经理解了客户端请求。
复制代码

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

  1. 关于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访问,就会有提示尚未备案的提示
复制代码

文章分类
后端
文章标签