最近在做一個掃碼登入功能,為此我還在網上搜了一下關於微信的掃描登入的實現方式。當這個功能完成了後,我決定將整個實現思路整理出來,方便自己以後檢視也方便其他有類似需求的程式猿些。
要實現掃碼登入我們需要解決兩個問題:
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/
希望對大家有幫助,如有理解錯誤的地方,還請大家斧正。