徒手擼一個掃碼登陸示例工程javascript
不知道是否是微信的緣由,如今出現掃碼登陸的場景愈來愈多了,做爲一個有追求、有理想新四好碼農,固然得緊跟時代的潮流,得徒手擼一個以儆效尤html
本篇示例工程,主要用到如下技術棧java
qrcode-plugin
:開源二維碼生成工具包,項目連接: https://github.com/liuyueyi/quick-mediaSpringBoot
:項目基本環境thymeleaf
:頁面渲染引擎SSE/異步請求
:服務端推送事件js
: 原生 js 的基本操做<!-- more -->git
按照以前的計劃,應該優先寫文件下載相關的博文,然而看到了一篇說掃碼登陸原理的博文,發現正好能夠和前面的異步請求/SSE 結合起來,搞一個應用實戰,因此就有了本篇博文github
關於掃碼登陸的原理,請查看: 聊一聊二維碼掃描登陸原理web
爲了照顧可能對掃碼登陸不太瞭解的同窗,這裏簡單的介紹一下它究竟是個啥spring
通常來講,掃碼登陸,涉及兩端,三個步驟後端
整個系統的設計中,最核心的一點就是手機端掃碼以後,pc 登陸成功,這個是什麼原理呢?跨域
藉助上面的原理,進行逐步的要點分析瀏覽器
最終咱們選定的業務流程關係以下圖:
接下來進入項目開發階段,針對上面的流程圖進行逐一的實現
首先常見一個 SpringBoot 工程項目,選擇版本2.2.1.RELEASE
pom 依賴以下
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.github.hui.media</groupId> <artifactId>qrcode-plugin</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </pluginManagement> </build> <repositories> <repository> <id>spring-releases</id> <name>Spring Releases</name> <url>https://repo.spring.io/libs-release-local</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>yihui-maven-repo</id> <url>https://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository</url> </repository> </repositories>
關鍵依賴說明
qrcode-plugin
: 不是我吹,這多是 java 端最好用、最靈活、還支持生成各類酷炫二維碼的工具包,目前最新版本2.2
,在引入依賴的時候,請指定倉庫地址https://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository
spring-boot-starter-thymeleaf
: 咱們選擇的模板渲染引擎,這裏並無採用先後端分離,一個項目包含全部的功能點配置文件application.yml
server: port: 8080 spring: thymeleaf: mode: HTML encoding: UTF-8 servlet: content-type: text/html cache: false
獲取本機 ip
提供一個獲取本機 ip 的工具類,避免硬編碼 url,致使不通用
import java.net.*; import java.util.Enumeration; public class IpUtils { public static final String DEFAULT_IP = "127.0.0.1"; /** * 直接根據第一個網卡地址做爲其內網ipv4地址,避免返回 127.0.0.1 * * @return */ public static String getLocalIpByNetcard() { try { for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) { NetworkInterface item = e.nextElement(); for (InterfaceAddress address : item.getInterfaceAddresses()) { if (item.isLoopback() || !item.isUp()) { continue; } if (address.getAddress() instanceof Inet4Address) { Inet4Address inet4Address = (Inet4Address) address.getAddress(); return inet4Address.getHostAddress(); } } } return InetAddress.getLocalHost().getHostAddress(); } catch (SocketException | UnknownHostException e) { return DEFAULT_IP; } } private static volatile String ip; public static String getLocalIP() { if (ip == null) { synchronized (IpUtils.class) { if (ip == null) { ip = getLocalIpByNetcard(); } } } return ip; } }
@CrossOrigin
註解來支持跨域,由於後續咱們測試的時候用localhost
來訪問登陸界面;可是 sse 註冊是用的本機 ip,因此會有跨域問題,實際的項目中可能並不存在這個問題
登陸頁邏輯,訪問以後返回的一張二維碼,二維碼內容爲登陸受權 url
@CrossOrigin @Controller public class QrLoginRest { @Value(("${server.port}")) private int port; @GetMapping(path = "login") public String qr(Map<String, Object> data) throws IOException, WriterException { String id = UUID.randomUUID().toString(); // IpUtils 爲獲取本機ip的工具類,本機測試時,若是用127.0.0.1, localhost那麼app掃碼訪問會有問題哦 String ip = IpUtils.getLocalIP(); String pref = "http://" + ip + ":" + port + "/"; data.put("redirect", pref + "home"); data.put("subscribe", pref + "subscribe?id=" + id); String qrUrl = pref + "scan?id=" + id; // 下面這一行生成一張寬高200,紅色,圓點的二維碼,並base64編碼 // 一行完成,就這麼簡單省事,強烈安利 String qrCode = QrCodeGenWrapper.of(qrUrl).setW(200).setDrawPreColor(Color.RED) .setDrawStyle(QrCodeOptions.DrawStyle.CIRCLE).asString(); data.put("qrcode", DomUtil.toDomSrc(qrCode, MediaType.ImageJpg)); return "login"; } }
請注意上面的實現,咱們返回的是一個視圖,並傳遞了三個數據
注意:subscribe
和qrcode
都用到了全局惟一 id,後面的操做中,這個參數很重要
接着時候對應的 html 頁面,在resources/templates
文件下,新增文件login.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="SpringBoot thymeleaf"/> <meta name="author" content="YiHui"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>二維碼界面</title> </head> <body> <div> <div class="title">請掃碼登陸</div> <img th:src="${qrcode}"/> <div id="state" style="display: none"></div> <script th:inline="javascript"> var stateTag = document.getElementById('state'); var subscribeUrl = [[${subscribe}]]; var source = new EventSource(subscribeUrl); source.onmessage = function (event) { text = event.data; console.log("receive: " + text); if (text == 'scan') { stateTag.innerText = '已掃描'; stateTag.style.display = 'block'; } else if (text.startsWith('login#')) { // 登陸格式爲 login#cookie var cookie = text.substring(6); document.cookie = cookie; window.location.href = [[${redirect}]]; source.close(); } }; source.onopen = function (evt) { console.log("開始訂閱"); } </script> </div> </body> </html>
請注意上面的 html 實現,id 爲 state 這個標籤默認是不可見的;經過EventSource
來實現 SSE(優勢是實時且自帶重試功能),並針對返回的結果進行了格式定義
scan
消息,則修改 state 標籤文案,並設置爲可見login#cookie
格式數據,表示登陸成功,#
後面的爲 cookie,設置本地 cookie,而後重定向到主頁,並關閉長鏈接其次在 script 標籤中,若是須要訪問傳遞的參數,請注意下面兩點
th:inline="javascript"
[[${}]]
獲取傳遞參數前面登陸的接口中,返回了一個sse
的註冊接口,客戶端在訪問登陸頁時,會訪問這個接口,按照咱們前面的 sse 教程文檔,能夠以下實現
private Map<String, SseEmitter> cache = new ConcurrentHashMap<>(); @GetMapping(path = "subscribe", produces = {org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE}) public SseEmitter subscribe(String id) { // 設置五分鐘的超時時間 SseEmitter sseEmitter = new SseEmitter(5 * 60 * 1000L); cache.put(id, sseEmitter); sseEmitter.onTimeout(() -> cache.remove(id)); sseEmitter.onError((e) -> cache.remove(id)); return sseEmitter; }
接下來就是掃描二維碼進入受權頁面的接口了,這個邏輯就比較簡單了
@GetMapping(path = "scan") public String scan(Model model, HttpServletRequest request) throws IOException { String id = request.getParameter("id"); SseEmitter sseEmitter = cache.get(request.getParameter("id")); if (sseEmitter != null) { // 告訴pc端,已經掃碼了 sseEmitter.send("scan"); } // 受權贊成的url String url = "http://" + IpUtils.getLocalIP() + ":" + port + "/accept?id=" + id; model.addAttribute("url", url); return "scan"; }
用戶掃碼訪問這個頁面以後,會根據傳過來的 id,定位對應的 pc 客戶端,而後發送一個scan
的信息
受權頁面簡單一點實現,加一個受權的超鏈就好,而後根據實際的狀況補上用戶 token(因爲並無獨立的 app 和用戶體系,因此下面做爲演示,就隨機生成一個 token 來替代)
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="SpringBoot thymeleaf"/> <meta name="author" content="YiHui"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>掃碼登陸界面</title> </head> <body> <div> <div class="title">肯定登陸嘛?</div> <div> <a id="login">登陸</a> </div> <script th:inline="javascript"> // 生成uuid,模擬傳遞用戶token function guid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // 獲取實際的token,補齊參數,這裏只是一個簡單的模擬 var url = [[${url}]]; document.getElementById("login").href = url + "&token=" + guid(); </script> </div> </body> </html>
點擊上面的受權超鏈以後,就表示登陸成功了,咱們後端的實現以下
@ResponseBody @GetMapping(path = "accept") public String accept(String id, String token) throws IOException { SseEmitter sseEmitter = cache.get(id); if (sseEmitter != null) { // 發送登陸成功事件,並攜帶上用戶的token,咱們這裏用cookie來保存token sseEmitter.send("login#qrlogin=" + token); sseEmitter.complete(); cache.remove(id); } return "登陸成功: " + token; }
用戶受權成功以後,就會自動跳轉到首頁了,咱們在首頁就簡單一點,搞一個歡迎的文案便可
@GetMapping(path = {"home", ""}) @ResponseBody public String home(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null || cookies.length == 0) { return "未登陸!"; } Optional<Cookie> cookie = Stream.of(cookies).filter(s -> s.getName().equalsIgnoreCase("qrlogin")).findFirst(); return cookie.map(cookie1 -> "歡迎進入首頁: " + cookie1.getValue()).orElse("未登陸!"); }
到此一個完整的登陸受權已經完成,能夠進行實際操做演練了,下面是一個完整的演示截圖(雖然我並無真的用 app 進行掃描登陸,而是識別二維碼地址,在瀏覽器中進行受權,實際並不影響整個過程,你用二維掃一掃受權效果也是同樣的)
請注意上面截圖的幾個關鍵點
已掃描
的文案實際的業務開發選擇的方案可能和本文提出的並不太同樣,也可能存在更優雅的實現方式(請有這方面經驗的大佬佈道一下),本文僅做爲一個參考,不表明標準,不表示徹底準確,若是把你們帶入坑了,請留言(固然我是不會負責的 🙃)
上面演示了徒手擼了一個二維碼登陸的示例工程,主要用到了一下技術點
qrcode-plugin
:生成二維碼,再次強烈安利一個私覺得 java 生態下最好用二維碼生成工具包 https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin (雖然吹得比較兇,但我並無收廣告費,由於這也是我寫的 😂)SSE
: 服務端推送事件,服務端單通道通訊,實現消息推送SpringBoot/Thymeleaf
: 演示項目基礎環境最後,以爲不錯的能夠贊一下,加個好友有事沒事聊一聊,關注個微信公衆號支持一二,都是能夠的嘛
相關博文
關於本篇博文,部分知識點能夠查看如下幾篇進行補全
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛