上一篇文章是对需求的分析,本次将逐渐进入代码阶段。本次主要的内容包括服务端wss的部署以及小程序端用户授权的时序及逻辑。
wss的配置与部署
微信小程序出于安全考虑,要求所有涉及到网络的操作,必须使用安全的网络请求,如https和wss,却使用的域名必须备案以及不能带端口号。详情请参考微信小程序官方文档
虽然在开发的过程中,微信提供的开发者工具可以忽略https和wss的校验,但程序上线时,就必须使用https和wss协议,所以,在这里,先来介绍下wss协议的部署。
WSS 是 Web Socket Secure 的简称, 它是 WebSocket 的加密版本. 我们知道 WebSocket 中的数据是不加密的, 但是不加密的数据很容易被别有用心的人窃取, 因此为了保护数据安全, 人们将 WebSocket 与 SSL 结合, 实现了安全的 WebSocket 通信, 即 WebSocket Secure.
所以说 WSS 是使用 SSL 进行加密了的 WebSocket 通信技术.
因为我们的对战答题功能对及时性要求比较高,传统的http无法满足要求,所以,我们使用WebSocket作为客户端与服务端,客户端与客户端之间的通讯。
要使用wss首先我们需要购买个域名证书,现在腾讯云和阿里云都有提供免费的域名证书。下面我已腾讯云为例,简单介绍下域名证书的购买。
进入腾讯云官网,在顶部导航中,找到云产品。如下图所示:
点击ssl证书管理。进入证书管理页面,
点击购买证书,进入下级页面,选择免费版
进入下级页面,填写证书基本信息
下一步,验证域名所有权。这里我选择手动DNS验证。点击确认申请。
点击查看证书详情,然后根据腾讯云提供的DNS信息,添加域名解析。
下图是域名解析填写的信息(前提是你要有个域名):
然后,回到腾讯云,点击如下图中的查询按钮:
当出现如下图所示的信息后,稍后几秒后,即可证书即可申请成功。
申请成功后,刷新证书详情页面,如下图,点击下载按钮,将证书下载下来。
将下载后的文件解压备用。
下面进入WebSocket服务端代码实现阶段。这里我使用.net平台开源的Socket框架SuperSocket。首先打开vs,新建一个控制台应用程序项目。
这里我们通过Nuget的方式引用SuperSocket。如下图所示:
分别安装上图中标注的包。安装完成后,然后将之前下载的证书拷贝过来。使用之前解压的文件里的iis文件夹里的证书文件。将iis文件夹中的证书文件拷贝到项目。然后右击证书文件,选择属性,进入属性设置页面。
在复制到输出目录中,选择始终复制。
控制台中的代码:
1 static void Main(string[] args) 2 3 { 4 5 #region 证书配置 6 7 var certConfig = new CertificateConfig(); 8 9 certConfig.FilePath = "ttt.vqicard.com.pfx";//证书路径 10 11 certConfig.Password = "123123";//证书密码。申请证书时填写的密码。没填,则此处为空 12 13 certConfig.KeyStorageFlags = X509KeyStorageFlags.UserKeySet; 14 15 certConfig.ClientCertificateRequired = false; 16 17 #endregion 18 19 var ws = new WebSocketServer(); 20 21 var serverConfig = new ServerConfig(); 22 23 serverConfig.Security = "tls"; 24 25 serverConfig.Certificate = certConfig; 26 27 serverConfig.Ip = IPAddress.Any.ToString();//绑定的ip 28 29 serverConfig.Port = 2018;//监听的端口号。此处填写默认端口。由于我的服务器的443端口已经被占用, 30 31 //所以,这里使用其他端口。因为微信不支持带端口的地址,所以,正式部署后,必须设置为443端口。 32 33 ws.NewDataReceived += Ws_NewDataReceived;//接收到新数据的回调 34 35 ws.NewMessageReceived += Ws_NewMessageReceived;//接收到新字符串的回调 36 37 ws.SessionClosed += Ws_SessionClosed;//回话关闭的回调 38 39 ws.NewSessionConnected += Ws_NewSessionConnected;//新用户接入的回调 40 41 if (ws.Setup(serverConfig)) 42 43 { 44 45 ws.Start(); 46 47 Console.WriteLine("监听开始"); 48 49 Console.ReadKey(); 50 51 } 52 53 } 54 55 private static void Ws_NewSessionConnected(WebSocketSession session) 56 57 { 58 59 //接收到新连接后,回复消息给客户端 60 61 session.Send("hello"); 62 63 } 64 65 private static void Ws_SessionClosed(WebSocketSession session, SuperSocket.SocketBase.CloseReason value) 66 67 { 68 69 } 70 71 private static void Ws_NewMessageReceived(WebSocketSession session, string value) 72 73 { 74 75 } 76 77 private static void Ws_NewDataReceived(WebSocketSession session, byte[] value) 78 79 { 80 81 } 82 83 }
代码编写完成后,运行。然后编写小程序端连接WebSocket的代码。
使用wx.connectSocket接口放回一个SocketTask对象。代码如下:
1 let task = wx.connectSocket({ 2 3 url: 'wss://ttt.vqicard.com:2018', 4 5 success: function (res) { 6 7 console.log(res) 8 9 } 10 11 })
然后SocketTask.onOpen监听连接打开事件。
SocketTask.onClose监听连接关闭事件。
SocketTask.onMessage(CALLBACK)
监听接收到服务器消息的事件。
通过SocketTask.send方法可以向服务器发送数据。
实例代码如下:
1 task.onOpen(res => { 2 3 console.log('连接服务器成功') 4 5 }) 6 7 task.onMessage(res => { 8 9 console.log(res) 10 11 })
wss的基本配置到这里就完成了。
微信小程序登录时序分析
下图是微信官方提供的小程序的登录逻辑:
从上图我们可以大概分析出用户的小程序端用户授权登录的流程与逻辑。
1.小程序端,调用wx.login()获取code。
2.使用wx.request()将code发送给开发者服务器。
3.开发者服务器使用appid,appsecret,code调用微信提供的接口,获取当前用户的session_key以及openid,这里的session_key是微信服务器生成的针对用户数据加密签名的密钥。
4.开发者服务器使用指定的算法生成足够安全的第三方session。目的是保证session_key的安全性。所以,生成的第三方session应该满足如下条件:长度足够长,避免使用时间戳作为随机参数,设置一定的有效时间,过期即视为不合法。
5.以3rd_session为key,session_key+openid为value,写入session存储。目的是,可以通过3rd_session获取到真实的session_key。
6.将3rd_session返回到小程序端,在小程序端,使用storage存储到本地。
7.后续使用时,先判断3rd_session是否存在,如果不存在则重新从第一步开始。
以上为授权的基本流程,实际操作中,可能会比以上分析的麻烦一点,因为可能会涉及到用户不同意授权。或者以前点过不同意,现在又想点同意的情况。所以,具体的操作,还是通过代码来理解的比较透彻。
代码中,有两个地方是需要给服务器交互的,一个是验证本地存储的session是否合法,另一个是通过code换取第三方session。通常情况下,是使用https的方式与服务器交互,相关的代码在示例中我也写到。但这个答题项目主要是使用wss的方式与服务端通讯,所以,为了方便代码的管理,检测session和换取session的操作我都是用wss的方式,具体的看代码。下面是小程序的代码,注释已经很清楚了,我就不一一解释了。
1 var wsTask 2 3 //app.js 4 5 App({ 6 7 onLaunch: function () { 8 9 wsTask = wx.connectSocket({ 10 11 url: 'ws://192.168.0.253:2018' 12 13 }) 14 15 this.wsTask = wsTask 16 17 wsTask.onOpen(()=>{ 18 19 console.log('连接服务器成功') 20 21 }) 22 23 wsTask.onMessage(msg=>{ 24 25 var res = JSON.parse(msg.data) 26 27 switch(res.option){ 28 29 case 'checkSession': 30 31 if(!res.status){ 32 33 this.login() 34 35 }else{ 36 37 console.log('登录成功') 38 39 } 40 41 break 42 43 case 'login': 44 45 if(res.status){ 46 47 wx.setStorage({ 48 49 key: '3rd_session', 50 51 data: res.session 52 53 }) 54 55 console.log('登录成功') 56 57 } 58 59 break 60 61 } 62 63 }) 64 65 }, 66 67 checkSession: function () { 68 69 //首先检测登录状态是否失效 70 71 wx.checkSession({ 72 73 complete: cr => { 74 75 if (cr.errMsg == 'checkSession:ok') { 76 77 //授权状态有效,需判断3rd_session是否存在 78 79 let rd_session = wx.getStorageSync('3rd_session') 80 81 if (rd_session) { 82 83 //第三方session存在 84 85 wsTask.send({ 86 87 data: JSON.stringify({ option:'checkSession',session:rd_session}) 88 89 }) 90 91 return 92 93 //将第三方session发送到服务器,验证合法性已经是否有效 94 95 wx.request({ 96 97 url: 'checkSessionUrl', 98 99 success: function (res) { 100 101 if (res.status) { 102 103 //根据服务端返回的验证结果进行判断,如果status为1,则表示3rdsession合法,且在有效期内。 104 105 } else { 106 107 //session无效 108 109 this.login() 110 111 } 112 113 }, 114 115 fail: function (e) { 116 117 console.error(e);//打印错误信息 118 119 } 120 121 }) 122 123 } else { 124 125 //session不存在,则需重新进入授权流程 126 127 this.login() 128 129 } 130 131 } else { 132 133 //授权状态失效,则需重新进入授权流程 134 135 this.login() 136 137 } 138 139 } 140 141 }) 142 143 }, 144 145 login: function () { 146 147 //检查用户是否已同意授权 148 149 wx.authorize({ 150 151 scope: 'scope.userInfo', 152 153 complete: res => { 154 155 //不允许授权 156 157 if (res.errMsg != 'authorize:ok') { 158 159 //则获取用户的授权设置 160 161 wx.getSetting({ 162 163 success: r => { 164 165 //未开启授权 166 167 if (!r.authSetting['scope.userInfo']) { 168 169 //询问是否开启授权 170 171 wx.showModal({ 172 173 title: '登录', 174 175 content: '小程序需要使用您的授权信息,是否继续?', 176 177 success: res => { 178 179 console.log(res) 180 181 if (res.confirm) { 182 183 //同意开启授权,则跳转到设置页面,由用户打开授权。用户打开授权后,由用户操作,返回小程序,此时可以再onShow方法中再次调用login方法。 184 185 wx.openSetting() 186 187 } 188 189 } 190 191 }) 192 193 } 194 195 } 196 197 }) 198 199 } else { 200 201 //表示已授权,此时,可以调用登录接口 202 203 wx.login({ 204 205 success: res => { 206 207 if(res.errMsg=='login:ok'){ 208 209 wsTask.send({data:JSON.stringify({option:'login',code:res.code})}) 210 211 return 212 213 wx.request({ 214 215 url: 'loginUrl', 216 217 data:{code:res.code}, 218 219 success:rq=>{ 220 221 //将此处返回的3rdsession保存在storage中,整个授权流程结束 222 223 } 224 225 }) 226 227 } 228 229 } 230 231 }) 232 233 } 234 235 } 236 237 }) 238 239 }, 240 241 onShow: function () { 242 243 this.checkSession() 244 245 }, 246 247 globalData: { 248 249 userInfo: null 250 251 } 252 253 })
服务端的代码如下:
1 private static void Ws_NewMessageReceived(WebSocketSession session, string value) 2 3 { 4 5 var jobj = JsonConvert.DeserializeObject(value); 6 7 var option = jobj.Value<string>("option"); 8 9 switch (option) 10 11 { 12 13 case "checkSession": 14 15 var rdsession = jobj.Value<string>("session"); 16 17 var model = wxUserlist.FirstOrDefault(f => f.MyKey == rdsession); 18 19 session.Send(JsonConvert.SerializeObject(new { option = option, status =model!=null?1:0})); 20 21 break; 22 23 case "login": 24 25 var code = jobj.Value<string>("code"); 26 27 var res = LoginApi.CodeToMySession("你的appid", "你的appsecret", code); 28 29 wxUserlist.Add(res); 30 31 session.Send(JsonConvert.SerializeObject(new { status=1,session=res.MyKey,option=option})); 32 33 break; 34 35 } 36 37 } 38 39
如需源码,请扫描二维码,关注微信公众号。回复:对战二