spring boot 自动跳转登录页面_徒手撸一个扫码登录示例工程
不知道是不是微信的原因,現(xiàn)在出現(xiàn)掃碼登錄的場景越來越多了,作為一個有追求、有理想新四好碼農(nóng),當(dāng)然得緊跟時代的潮流,得徒手?jǐn)]一個以儆效尤
本篇示例工程,主要用到以下技術(shù)棧
- qrcode-plugin:開源二維碼生成工具包,項目鏈接: https://github.com/liuyueyi/quick-media[1]
- SpringBoot:項目基本環(huán)境
- thymeleaf:頁面渲染引擎
- SSE/異步請求:服務(wù)端推送事件
- js: 原生 js 的基本操作
I. 原理解析
按照之前的計劃,應(yīng)該優(yōu)先寫文件下載相關(guān)的博文,然而看到了一篇說掃碼登錄原理的博文,發(fā)現(xiàn)正好可以和前面的異步請求/SSE 結(jié)合起來,搞一個應(yīng)用實戰(zhàn),所以就有了本篇博文關(guān)于掃碼登錄的原理,請查看: 聊一聊二維碼掃描登錄原理[2]
1. 場景描述
為了照顧可能對掃碼登錄不太了解的同學(xué),這里簡單的介紹一下它到底是個啥
一般來說,掃碼登錄,涉及兩端,三個步驟
- pc 端,登錄某個網(wǎng)站,這個網(wǎng)站的登錄方式和傳統(tǒng)的用戶名/密碼(手機(jī)號/驗證碼)不一樣,顯示的是一個二維碼
- app 端,用這個網(wǎng)站的 app,首先確保你是登錄的狀態(tài),然后掃描二維碼,彈出一個登錄授權(quán)的頁面,點擊授權(quán)
- pc 端登錄成功,自動跳轉(zhuǎn)到首頁
2. 原理與流程簡述
整個系統(tǒng)的設(shè)計中,最核心的一點就是手機(jī)端掃碼之后,pc 登錄成功,這個是什么原理呢?
- 我們假定 app 與后端通過 token 進(jìn)行身份標(biāo)識
- app 掃碼授權(quán),并傳遞 token 給后端,后端根據(jù) token 可以確定是誰在 pc 端發(fā)起登錄請求
- 后端將登錄成功狀態(tài)寫回給 pc 請求者并跳轉(zhuǎn)首頁(這里相當(dāng)于一般的用戶登錄成功之后的流程,可以選擇 session、cookie 或者 jwt)
借助上面的原理,進(jìn)行逐步的要點分析
- pc 登錄,生成二維碼
- 二維碼要求唯一,并綁定請求端身份(否則假定兩個人的二維碼一致,一個人掃碼登錄了,另外一個豈不是也登錄了?)
- 客戶端與服務(wù)端保持連接,以便收到后續(xù)的登錄成功并調(diào)首頁的事件(可以選擇方案比較多,如輪詢,長連接推送)
- app 掃碼,授權(quán)登錄
- 掃碼之后,跳轉(zhuǎn)授權(quán)頁面(所以二維碼對應(yīng)的應(yīng)該是一個 url)
- 授權(quán)(身份確定,將身份信息與 pc 請求端綁定,并跳轉(zhuǎn)首頁)
最終我們選定的業(yè)務(wù)流程關(guān)系如下圖:
II. 實現(xiàn)
接下來進(jìn)入項目開發(fā)階段,針對上面的流程圖進(jìn)行逐一的實現(xiàn)
1. 項目環(huán)境
首先常見一個 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>關(guān)鍵依賴說明
- qrcode-plugin: 不是我吹,這可能是 java 端最好用、最靈活、還支持生成各種酷炫二維碼的工具包,目前最新版本2.2,在引入依賴的時候,請指定倉庫地址https://raw.githubusercontent.com/liuyueyi/maven-repository/master/repository
- spring-boot-starter-thymeleaf: 我們選擇的模板渲染引擎,這里并沒有采用前后端分離,一個項目包含所有的功能點
配置文件application.yml
server:port: 8080spring:thymeleaf:mode: HTMLencoding: UTF-8servlet:content-type: text/htmlcache: false獲取本機(jī) ip
提供一個獲取本機(jī) ip 的工具類,避免硬編碼 url,導(dǎo)致不通用
import java.net.*; import java.util.Enumeration;public class IpUtils {public static final String DEFAULT_IP = "127.0.0.1";/*** 直接根據(jù)第一個網(wǎng)卡地址作為其內(nèi)網(wǎng)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;} }2. 登錄接口
@CrossOrigin注解來支持跨域,因為后續(xù)我們測試的時候用localhost來訪問登錄界面;但是 sse 注冊是用的本機(jī) ip,所以會有跨域問題,實際的項目中可能并不存在這個問題登錄頁邏輯,訪問之后返回的一張二維碼,二維碼內(nèi)容為登錄授權(quán) 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 為獲取本機(jī)ip的工具類,本機(jī)測試時,如果用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編碼// 一行完成,就這么簡單省事,強(qiáng)烈安利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";} }請注意上面的實現(xiàn),我們返回的是一個視圖,并傳遞了三個數(shù)據(jù)
- redirect: 跳轉(zhuǎn) url(app 授權(quán)之后,跳轉(zhuǎn)的頁面)
- subscribe: 訂閱 url(用戶會訪問這個 url,開啟長連接,接收服務(wù)端推送的掃碼、登錄事件)
- qrcode: base64 格式的二維碼圖片
注意:subscribe和qrcode都用到了全局唯一 id,后面的操作中,這個參數(shù)很重要
接著時候?qū)?yīng)的 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#cookievar cookie = text.substring(6);document.cookie = cookie;window.location.href = [[${redirect}]];source.close();}};source.onopen = function (evt) {console.log("開始訂閱");}</script> </div> </body> </html>請注意上面的 html 實現(xiàn),id 為 state 這個標(biāo)簽?zāi)J(rèn)是不可見的;通過EventSource來實現(xiàn) SSE(優(yōu)點是實時且自帶重試功能),并針對返回的結(jié)果進(jìn)行了格式定義
- 若接收到服務(wù)端 scan 消息,則修改 state 標(biāo)簽文案,并設(shè)置為可見
- 若接收到服務(wù)端 login#cookie 格式數(shù)據(jù),表示登錄成功,#后面的為 cookie,設(shè)置本地 cookie,然后重定向到主頁,并關(guān)閉長連接
其次在 script 標(biāo)簽中,如果需要訪問傳遞的參數(shù),請注意下面兩點
- 需要在 script 標(biāo)簽上添加th:inline="javascript"
- [[${}]] 獲取傳遞參數(shù)
3. sse 接口
前面登錄的接口中,返回了一個sse的注冊接口,客戶端在訪問登錄頁時,會訪問這個接口,按照我們前面的 sse 教程文檔,可以如下實現(xiàn)
private Map<String, SseEmitter> cache = new ConcurrentHashMap<>();@GetMapping(path = "subscribe", produces = {org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE}) public SseEmitter subscribe(String id) {// 設(shè)置五分鐘的超時時間SseEmitter sseEmitter = new SseEmitter(5 * 60 * 1000L);cache.put(id, sseEmitter);sseEmitter.onTimeout(() -> cache.remove(id));sseEmitter.onError((e) -> cache.remove(id));return sseEmitter; }4. 掃碼接口
接下來就是掃描二維碼進(jìn)入授權(quán)頁面的接口了,這個邏輯就比較簡單了
@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端,已經(jīng)掃碼了sseEmitter.send("scan");}// 授權(quán)同意的urlString url = "http://" + IpUtils.getLocalIP() + ":" + port + "/accept?id=" + id;model.addAttribute("url", url);return "scan"; }用戶掃碼訪問這個頁面之后,會根據(jù)傳過來的 id,定位對應(yīng)的 pc 客戶端,然后發(fā)送一個scan的信息
授權(quán)頁面簡單一點實現(xiàn),加一個授權(quán)的超鏈就好,然后根據(jù)實際的情況補(bǔ)上用戶 token(由于并沒有獨立的 app 和用戶體系,所以下面作為演示,就隨機(jī)生成一個 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,模擬傳遞用戶tokenfunction 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,補(bǔ)齊參數(shù),這里只是一個簡單的模擬var url = [[${url}]];document.getElementById("login").href = url + "&token=" + guid();</script></div> </body> </html>5. 授權(quán)接口
點擊上面的授權(quán)超鏈之后,就表示登錄成功了,我們后端的實現(xiàn)如下
@ResponseBody @GetMapping(path = "accept") public String accept(String id, String token) throws IOException {SseEmitter sseEmitter = cache.get(id);if (sseEmitter != null) {// 發(fā)送登錄成功事件,并攜帶上用戶的token,我們這里用cookie來保存tokensseEmitter.send("login#qrlogin=" + token);sseEmitter.complete();cache.remove(id);}return "登錄成功: " + token; }6. 首頁
用戶授權(quán)成功之后,就會自動跳轉(zhuǎn)到首頁了,我們在首頁就簡單一點,搞一個歡迎的文案即可
@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 -> "歡迎進(jìn)入首頁: " + cookie1.getValue()).orElse("未登錄!"); }7. 實測
到此一個完整的登錄授權(quán)已經(jīng)完成,可以進(jìn)行實際操作演練了,下面是一個完整的演示截圖(雖然我并沒有真的用 app 進(jìn)行掃描登錄,而是識別二維碼地址,在瀏覽器中進(jìn)行授權(quán),實際并不影響整個過程,你用二維掃一掃授權(quán)效果也是一樣的)
請注意上面截圖的幾個關(guān)鍵點
- 掃碼之后,登錄界面二維碼下面會顯示已掃描的文案
- 授權(quán)成功之后,登錄界面會主動跳轉(zhuǎn)到首頁,并顯示歡迎 xxx,而且注意用戶是一致的
8. 小結(jié)
實際的業(yè)務(wù)開發(fā)選擇的方案可能和本文提出的并不太一樣,也可能存在更優(yōu)雅的實現(xiàn)方式(請有這方面經(jīng)驗的大佬布道一下),本文僅作為一個參考,不代表標(biāo)準(zhǔn),不表示完全準(zhǔn)確,如果把大家?guī)肟恿?#xff0c;請留言(當(dāng)然我是不會負(fù)責(zé)的 )
上面演示了徒手?jǐn)]了一個二維碼登錄的示例工程,主要用到了一下技術(shù)點
- qrcode-plugin:生成二維碼,再次強(qiáng)烈安利一個私以為 java 生態(tài)下最好用二維碼生成工具包 https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin[3] (雖然吹得比較兇,但我并沒有收廣告費,因為這也是我寫的 )
- SSE: 服務(wù)端推送事件,服務(wù)端單通道通信,實現(xiàn)消息推送
- SpringBoot/Thymeleaf: 演示項目基礎(chǔ)環(huán)境
最后,覺得不錯的可以贊一下,加個好友有事沒事聊一聊,關(guān)注個微信公眾號支持一二,都是可以的嘛
III. 其他
0. 項目
相關(guān)博文
關(guān)于本篇博文,部分知識點可以查看以下幾篇進(jìn)行補(bǔ)全
- 【SpringBoot WEB 系列】SSE 服務(wù)器發(fā)送事件詳解
- 【SpringBoot WEB 系列】異步請求知識點與使用姿勢小結(jié)
- 【SpringBoot WEB 系列】Thymeleaf 環(huán)境搭建
- 工程:https://github.com/liuyueyi/spring-boot-demo[4]
- 項目源碼:https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/202-web-qrcode-login[5]
1. 一灰灰 Blog
盡信書則不如,以上內(nèi)容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人博客 https://blog.hhui.top[6]
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top[7]
http://weixin.qq.com/r/FS9waAPEg178rUcL93oH (二維碼自動識別)
參考資料
[1] https://github.com/liuyueyi/quick-media: https://github.com/liuyueyi/quick-media
[2] 聊一聊二維碼掃描登錄原理: https://juejin.im/post/5e83e716e51d4546c27bb559?utm_source=gold_browser_extension
[3] https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin: https://github.com/liuyueyi/quick-media/blob/master/plugins/qrcode-plugin
[4] https://github.com/liuyueyi/spring-boot-demo: https://github.com/liuyueyi/spring-boot-demo
[5] https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/202-web-qrcode-login: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-case/202-web-qrcode-login
[6] https://blog.hhui.top: https://blog.hhui.top
[7] http://spring.hhui.top: http://spring.hhui.top
總結(jié)
以上是生活随笔為你收集整理的spring boot 自动跳转登录页面_徒手撸一个扫码登录示例工程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 运行返回签名不正确_如果调用约定不匹配,
- 下一篇: 准确率 召回率_机器学习中F值(F-Me