最近在做一个扫码登录功能,为此我还在网上搜了一下关于微信的扫描登录的实现方式。当这个功能完成了后,我决定将整个实现思路整理出来,方便自己以后查看也方便其他有类似需求的程序猿些。
要实现扫码登录我们需要解决两个问题:
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进行推送。
生成二维码部分引入依赖文件
- <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
-
-
-
-
-
-
-
- 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;
-
-
-
-
-
-
-
- @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;
-
-
-
-
-
-
-
-
-
-
-
- @RequestMapping("/getLoginQrCode")
- @ResponseBody
- @LimitLess
- public void getLoginQrCode(String uuid, HttpServletRequest request,
- HttpServletResponse response) throws ServletException, IOException {
-
-
- 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) {
-
- e.printStackTrace();
- }
- }
-
-
-
-
-
-
-
-
-
- @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", "登录成功");
-
- jmsSender.sendMessage(object.toString());
- }
-
- 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;
- }
-
-
-
-
-
-
-
- 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)
- 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("*");
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
- @Bean
- public ServletServerContainerFactoryBean createWebSocketContainer() {
- ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
- container.setMaxTextMessageBufferSize(8192);
- container.setMaxBinaryMessageBufferSize(8192);
- return container;
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- 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);
- }
- }
- 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);
-
- sessionMap.remove(session);
-
- 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实现
- 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);
- }
- }
-
- }
核心代码就酱......简短项目就是这些了,好了到了关键发布和部署到服务器环节####
#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,目前运行良好。
觉得很容易用到.. 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/
2、Java丨PHP丨C#丨Websocket丨Asp.net Web实时消息服务器推送 - GoEasy http://goeasy.io/cn/
希望对大家有帮助,如有理解错误的地方,还请大家斧正。