您的位置:首页 > 移动开发

WebSocket实现app扫描二维码登录以及ws应用进行负载均衡?

2017-12-15 17:53 627 查看
最近在做一个扫码登录功能,为此我还在网上搜了一下关于微信的扫描登录的实现方式。当这个功能完成了后,我决定将整个实现思路整理出来,方便自己以后查看也方便其他有类似需求的程序猿些。

要实现扫码登录我们需要解决两个问题:

1.  在没有输入用户名及密码的情况下,如何解决权限安全问题?换句话讲,如何让服务器知道扫码二维码的客户端是一个合法的用户?

2.  服务器根据用户在客户端的选择如何实时在网页上作出相应的响应?


首先我们先理一下微信的实现思路,来方便我们理解解决这一难题的思路方向。微信登录的二维码实际上是将一个URL转换成二维码的形式,而通过微信客户端扫码后,无非就是打开了这个url, 我捕捉到的微信二维码的url为https://login.weixin.qq.com/l/YdmTu30I5A== ,这个url里的YdmTu30I5A==代表的是本次会话的唯一ID,
这个有点儿类似浏览器里的session id,通过这个ID,微信就能定向将确认结果反馈到网页上。使用微信二维码登录功能,需要有两个前提:一是客户端上需要安装微信app。 二是用户需要登录到到微信app。https://wx.qq.com/

WebsocketWeb实时消息后台服务器推送技术

为什么要有这两个条件呢?那是因为微信在确认是否允许登录到网页版的时候,微信需要提取当前app的登录信息并将上面的session ID一并发给服务器,这样服务器收到了登录信息和sessionID后就可以确认两件事:一是用来确认登录的客户端的用户是验证过的;二是通过session ID服务器知道将反馈结果推送到哪个网页。

所以针对第一点,我们的关键在于,在扫描前要确保用户是已经被验证过且合法的用户(验证方式可以是用户名+密码,也可以是一个secure key),在选择是否登录时将这个结果一并推送到服务器端,就好了。如果用户没有验证是否合法,可以像微信的处理方式一样直接告诉用户二维码不可识别或提示请先登录到app。

有了身份验证,那么现在就解决第二个问题,如何将反馈结果实时地显示在网页上呢?有朋友可能会说,客户端这边很简单发一个请求到后台就好了,而网页上用ajax定时发送到服务器端看是否有反馈。我不赞成这种做法,因为ajax轮询方式十分消耗客户端和服务器端资源!这里涉及到另一个技术-web实时推送技术,使用推送技术可以节约服务器端和客户端的资源,可以稳定地推送和接收任何消息。我在实现的过程中我采用了第三方推送服务-GoEasy推送,用它是实现非常简单,我们项目里的其他功能也用到了GoEasy
web实时推送服务,所以在此我直接就用的GoEasy推送来将登录反馈结果推送到服务器。我的实现步骤非常简单,将传送的session ID作为客户端与网页端的通信channel,网页端订阅用session ID作为值得channel,客户端将验证结果和session ID发送到服务器端,服务器端可以通过这个channel主动将结果推送给网页版!如果客户端也需要做相应的反馈的话,那么客户端也只需要订阅这个channel,然后服务器端会同时将结果推送给网页版和客户端,收到消息后,就可以根据需求在goeasy的回调函数里做你想做的事情了。关于goeasy推送的使用,大家可以参考这篇博客: http://www.cnblogs.com/jishaochengduo/articles/5552645.html
另外GoEasy推送官网上也有一个demo:GoEasy二维码扫码登录demo,大家可以去看看效果.

话不多说,直接上代码,上代码,上代码。项目整起!!!!!

后台框架采用SpringMVC,不同的框架可根据逻辑更改即可:

【思路】- PC端生成二维码,二维码包含uuid(全局唯一标识符),且打通websocket通道,等待服务器返回登录成功信息;APP扫描二维码,获取uuid及登录信息,推送给服务端,处理后的登录信息通过websocket返回给PC端,PC端得到登录信息后保存即登录成功。APP扫描确认登录的信息可以采用ActiveMQ进行推送。


生成二维码部分引入依赖文件

[html] view
plain copy

<dependency>

<groupId>com.google.zxing</groupId>

<artifactId>core</artifactId>

<version>3.1.0</version>

</dependency>

<dependency>

<groupId>com.google.zxing</groupId>

<artifactId>javase</artifactId>

<version>3.1.0</version>

</dependency>


二维码登录后台控制层Controller

[java] view
plain copy

/**

* 项目名称:dream_user

* 项目包名:org.fore.user.controller

* 创建时间:2017年8月8日下午5:29:41

* 创建者:Administrator-宋发元

* 创建地点:杭州

*/

package org.fore.user.controller;

import java.io.IOException;

import java.io.OutputStream;

import java.util.HashMap;

import java.util.Map;

import java.util.UUID;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.fore.model.user.UserAccount;

import org.fore.model.user.UserModel;

import org.fore.user.qrcode.websocket.WebSocketHandler;

import org.fore.user.service.UserAccountService;

import org.fore.user.service.UserService;

import org.fore.utils.jms.JmsSender;

import org.fore.utils.mvc.TokenUtil;

import org.fore.utils.mvc.annotation.LimitLess;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;

import com.alibaba.fastjson.JSONObject;

import com.google.zxing.BarcodeFormat;

import com.google.zxing.EncodeHintType;

import com.google.zxing.MultiFormatWriter;

import com.google.zxing.WriterException;

import com.google.zxing.client.j2se.MatrixToImageWriter;

import com.google.zxing.common.BitMatrix;

import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

/**

* 描述:控制层

* @author songfayuan

* 2017年8月8日下午5:29:41

*/

@Controller

@RequestMapping("/qrcodelogin")

public class QrCodeLoginController {

private Logger logger = LoggerFactory.getLogger(QrCodeLoginController.class);

public static int defaultWidthAndHeight=260;

@Autowired

private WebSocketHandler webSocketHandler;

@Autowired

private UserService userService;

@Autowired

private UserAccountService userAccountService;

@Autowired

@Qualifier(value = "qrCodeLoginSender")

private JmsSender jmsSender;

/**

* 描述:PC获取二维码

* @param uuid

* @param request

* @param response

* @throws ServletException

* @throws IOException

* @author songfayuan

* 2017年8月11日上午9:04:43

*/

@RequestMapping("/getLoginQrCode")

@ResponseBody

@LimitLess

public void getLoginQrCode(String uuid, HttpServletRequest request,

HttpServletResponse response) throws ServletException, IOException {

//生成参数

//String uuid = generateUUID();

String host = request.getHeader("Host");

JSONObject data = new JSONObject();

data.put("code", 200);

data.put("msg", "获取二维码成功");

data.put("uuid", uuid);

data.put("host", host);

logger.info("【二维码内容】:{}",data);

//生成二维码

Map<EncodeHintType, Object> hints=new HashMap<EncodeHintType, Object>();

// 指定纠错等级

hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

// 指定编码格式

hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");

hints.put(EncodeHintType.MARGIN, 1);

try {

BitMatrix bitMatrix = new MultiFormatWriter().encode(data.toString(),BarcodeFormat.QR_CODE, defaultWidthAndHeight, defaultWidthAndHeight, hints);

OutputStream out = response.getOutputStream();

MatrixToImageWriter.writeToStream(bitMatrix, "png", out);//输出二维码

out.flush();

out.close();

} catch (WriterException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

/**

* 描述:app确认请求处理

* @param uuid

* @param host

* @param userid

* @author songfayuan

* 2017年8月11日上午9:05:56

*/

@RequestMapping("/sendCodeLoginInfo")

@ResponseBody

@LimitLess

public void sendCodeLoginInfo(String uuid, String host, Integer userid) {

// 注册成功后 或 登录,需要同步账户信息,获取用户基本信息

UserAccount account = userAccountService.findCurrentUserAccount(userid);

userAccountService.syncAccount(account);

UserModel userModel = userService.findUserById(userid);

userModel = changeUserForShow(userModel);

JSONObject token = TokenUtil.generateTokenByQrCodeLogin(userid, host);

JSONObject object = new JSONObject();

object.put("code", 10086);

object.put("uuid", uuid);

object.put("userinfo", userModel);

object.put("token", token);

object.put("msg", "登录成功");

//this.webSocketHandler.forwardQrCode(object.toString());

jmsSender.sendMessage(object.toString()); //采用ActiveMQ进行推送,也可以直接注入websocket进行发送

}

//处理用户登录信息

private UserModel changeUserForShow(UserModel userModel) {

UserModel user = new UserModel();

user.setId(userModel.getId());

user.setUserName(userModel.getUserName());

user.setUserSex(userModel.getUserSex());

user.setUserPortrait(userModel.getUserPortrait());

return user;

}

/**

* 描述:唯一标识符

* @return

* @author songfayuan

* 2017年8月11日上午9:06:12

*/

public static String generateUUID() {

String uuid = UUID.randomUUID().toString();

uuid = uuid.replace("-", "");

Long currentTime = System.currentTimeMillis();

String currentDate = String.valueOf(currentTime);

return uuid + currentDate;

}

}


websocket实现(本案例采用Spring自带的websocket)

[java] view
plain copy

package org.fore.sms.qrcode.websocket;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import org.springframework.web.socket.config.annotation.EnableWebSocket;

import org.springframework.web.socket.config.annotation.WebSocketConfigurer;

import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;

@Configuration

@EnableWebMvc

@EnableWebSocket

public class QrCodeLoginWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

@Autowired

private QrCodeLoginWebSocketEndPoint endPoint;

@Autowired

private QrCodeLoginHandshakeInterceptor interceptor;

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

registry.addHandler(endPoint, "/qrcodelogin.do").addInterceptors(interceptor).setAllowedOrigins("*");

// registry.addHandler(endPoint,

// "/sockjs.do").addInterceptors(interceptor).setAllowedOrigins("*")

// .withSockJS();

}

/**

* Each underlying WebSocket engine exposes configuration properties that

* control runtime characteristics such as the size of message buffer sizes,

* idle timeout, and others.

*/

/**

* For Tomcat, WildFly, and GlassFish add a

* ServletServerContainerFactoryBean to your WebSocket Java config:

*/

@Bean

public ServletServerContainerFactoryBean createWebSocketContainer() {

ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();

container.setMaxTextMessageBufferSize(8192);

container.setMaxBinaryMessageBufferSize(8192);

return container;

}

/**

* For Jetty, you’ll need to supply a pre-configured Jetty

* WebSocketServerFactory and plug that into Spring’s

* DefaultHandshakeHandler through your WebSocket Java config:

*/

// @Bean

// public DefaultHandshakeHandler handshakeHandler() {

//

// WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);

// policy.setInputBufferSize(8192); /* 设置消息缓冲大小 */

// policy.setIdleTimeout(600000); /* 10分钟read不到数据的话,则断开该客户端 */

//

// return new DefaultHandshakeHandler(new JettyRequestUpgradeStrategy(new

// WebSocketServerFactory(policy)));

// }

}

[java] view
plain copy

package org.fore.sms.qrcode.websocket;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.http.server.ServerHttpRequest;

import org.springframework.http.server.ServerHttpResponse;

import org.springframework.stereotype.Component;

import org.springframework.web.socket.WebSocketHandler;

import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

@Component

public class QrCodeLoginHandshakeInterceptor extends HttpSessionHandshakeInterceptor {

private Logger logger = LoggerFactory.getLogger(QrCodeLoginHandshakeInterceptor.class);

@Override

public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,

Map<String, Object> attributes) throws Exception {

return super.beforeHandshake(request, response, wsHandler, attributes);

}

@Override

public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,

Exception ex) {

super.afterHandshake(request, response, wsHandler, ex);

}

}

[java] view
plain copy

package org.fore.sms.qrcode.websocket;

import java.io.IOException;

import java.util.Map;

import java.util.UUID;

import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.fore.model.quota.tcp.ReqCode;

import org.springframework.stereotype.Component;

import org.springframework.web.socket.CloseStatus;

import org.springframework.web.socket.TextMessage;

import org.springframework.web.socket.WebSocketSession;

import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.JSONObject;

@Component

public class QrCodeLoginWebSocketEndPoint extends TextWebSocketHandler {

private Logger logger = LoggerFactory.getLogger(QrCodeLoginWebSocketEndPoint.class);

private static Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();

private static Map<WebSocketSession,String > sessionMap2 = new ConcurrentHashMap<>();

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

logger.info("WebSocketHandler:客户端{}上线", session.getRemoteAddress());

String uuid = generateUUID();

sessionMap.put(uuid,session);

sessionMap2.put(session,uuid);

}

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

String msg = message.getPayload();

String ipAddress = session.getRemoteAddress().toString();

JSONObject requestData = JSON.parseObject(msg);

Integer code = requestData.getInteger("code");

JSONObject result = new JSONObject();

String uuid = sessionMap2.get(session);

result.put("code", 200);

result.put("uuid", uuid);

switch (code) {

case ReqCode.REQ_QR_CODE:

logger.info("WebSocketHandler:客户端{}发送消息{}...", ipAddress, msg);

if(session.isOpen())

session.sendMessage(new TextMessage(result.toString()));

logger.info("WebSocketHandler:客户端{}发送消息{}完成", ipAddress, msg);

break;

default:

break;

}

}

@Override

public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

String ipAddress = session.getRemoteAddress().toString();

logger.info("WebSocketHandler:客户端{}下线", ipAddress);

logger.info("WebSocketHandler:删除客户端{}的session...", ipAddress);

logger.info("WebSocketHandler:删除sessionMap的客户端{}连接...", ipAddress);

String uuid = sessionMap2.get(session);

sessionMap.remove(uuid);

sessionMap2.remove(session);

logger.info("WebSocketHandler:删除sessionMap的客户端{}连接完成", ipAddress);

logger.info("WebSocketHandler:删除WebSocket客户端{}连接...", ipAddress);

// logger.info("{}", sessionMap);

sessionMap.remove(session);

// logger.info("{}", sessionMap);

logger.info("WebSocketHandler:删除WebSocket客户端{}连接完成", ipAddress);

logger.info("WebSocketHandler:删除客户端{}的session完成", ipAddress);

if(session.isOpen())

session.close();

}

@Override

public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {

logger.info("WebSocketHandler:客户端{}异常", session.getRemoteAddress(), exception);

}

//发送消息

public void sendMessage(String userInfo) throws Exception {

JSONObject json = JSONObject.parseObject(userInfo);

String uuid = json.getString("uuid");

WebSocketSession session = sessionMap.get(uuid);

if (session == null) {

logger.info("app发送给PC的登录信息:{}参数不正确!",userInfo);

}else {

logger.info("app发送给PC的登录信息:{}",userInfo);

session.sendMessage(new TextMessage(userInfo));

}

}

//唯一标识符

public static String generateUUID() {

String uuid = UUID.randomUUID().toString();

uuid = uuid.replace("-", "");

Long currentTime = System.currentTimeMillis();

String currentDate = String.valueOf(currentTime);

return uuid + currentDate;

}

}


JMS实现

[java] view
plain copy

package org.fore.sms.qrcode.jms;

import org.fore.utils.jms.Listener;

import org.fore.sms.qrcode.websocket.QrCodeLoginWebSocketEndPoint;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;

@Component

public class QrCodeLoginListener implements Listener {

private Logger logger = LoggerFactory.getLogger(QrCodeLoginListener.class);

@Autowired

private QrCodeLoginWebSocketEndPoint qrCodeLoginWebSocketEndPoint;

@Override

public void onMessage(String message) {

logger.info("app确认登录信息:接收app推送的确定PC登录消息{}", message);

JSONObject object = JSONObject.parseObject(message);

try {

qrCodeLoginWebSocketEndPoint.sendMessage(object.toJSONString());

} catch (Exception e) {

logger.info("app确认登录信息:接收app推送的确定PC登录消息异常", e);

}

}

}

核心代码就酱......简短项目就是这些了,好了到了关键发布和部署到服务器环节####


如何对 Websocket 应用进行负载均衡?

#nginx websocket 负载均衡配置(用1.3以后版本的nginx,原生支持websocket反向代理;压力测试可以用jmeter+第三方websocket插件,具体可以到github上搜一下。)

#回传消息 需要 uid+serverip+fd 绑定关系 来实现

#压测 可以用jmeter 或者 swoole作者写的
swoole-src/run.php at master · swoole/swoole-src · GitHub

#add 2017 1126

upstream websocket{

server serverip01;

server serverip02

}

map $http_upgrade $connection_upgrade {

default upgrade;

'' close;

}

server {

listen 8020;

location / {

proxy_pass http://websocket;
proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection "Upgrade";

}

}

客户端链接时负载…就是公布出无数个WS链接点,客户端获取“通过一个计算策略分配的链接点”地址,客户端链接…
nginx负载没用,代理链接数在那儿放着的…

我的简单方案:我后台用PHP跑了6个进程监听六个端口(12322〜12327),然后Nginx部署安装了yaoweibin的ngx_tcp_proxy_module实现了tcp
upstream,目前运行良好。

之前项目有过类似实践,一百万个并发连接。

第一层:DNS轮训 ,域名可指向多个同等配置的NGINX

第二层:NGINX,后端配置多个无状态应用进程

DNS轮训主要解决单机NGINX并发连接数瓶颈,不高的话直接NGINX亦可。

NGINX WEBSOCKET和负载均衡可参考:

Note: 基本的 WebSocket 的 Nginx 配置

NGINX负载均衡配置

觉得很容易用到.. Nginx
从 1.3 开始支持 WebSocket, 现在已经是 1.4.4 了

相对 HTTP, 看过例子发现配置其实比较简单,

先用
ws
模块写一个简单的 WebSocket 服务器:
Server = require('ws').Server

wss = new Server port: 3000

wss.on 'connection', (ws) ->
console.log 'a connection'
ws.send 'started'

console.log 'server started'


然后修改 Hosts, 添加, 比如
ws.repo
, 指向
127.0.0.1


然后是 Nginx 配置:
server {
listen 80;
server_name ws.repo;

location / {
proxy_pass http://127.0.0.1:3000/; proxy_redirect off;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}


Reload Nginx 然后从浏览器控制台尝试链接, OK
new WebSocket('ws://ws.repo/')


或者通过 Upstream 的写法:

upstream ws_server {
server 127.0.0.1:3000;
}

server {
listen 80;
server_name ws.repo;

location / {
proxy_pass http://ws_server/; proxy_redirect off;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}


WebSocket 先是通过 HTTP 建立连接,

然后通过 101 状态码, 表示切换协议,, 在配置里是
Upgrade


【博主推荐两个比较常用的WS负载组件】


1、Swoole - 面向生产环境的 PHP 异步网络通信引擎 https://www.swoole.com/[/code] 
2、Java丨PHP丨C#丨Websocket丨Asp.net Web实时消息服务器推送 - GoEasy http://goeasy.io/cn/[/code] 希望对大家有帮助,如有理解错误的地方,还请大家斧正。
                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: