spring boot高性能实现二维码扫码登录(上)——单服务器版
前言
?
目前網(wǎng)頁的主流登錄方式是通過手機掃碼二維碼登錄。我看了網(wǎng)上很多關(guān)于掃碼登錄博客后,發(fā)現(xiàn)基本思路大致是:打開網(wǎng)頁,生成uuid,然后長連接請求后端并等待登錄認證相應(yīng)結(jié)果,而后端每個幾百毫秒會循環(huán)查詢數(shù)據(jù)庫或redis,當查詢到登錄信息后則響應(yīng)長連接的請求。
然而,如果是小型應(yīng)用則沒問題,如果用戶量,并發(fā)大則會出現(xiàn)非常嚴重的性能瓶頸。而問題的關(guān)鍵是使用了循環(huán)查詢數(shù)據(jù)庫或redis的方案。假設(shè)要優(yōu)化這個方案可以使用java多線程的同步集合+CountDownLatch來解決。
?
一、環(huán)境
?
1.java 8(jdk1.8)
2.maven 3.3.9
3.spring boot 2.0
?
二、知識點
?
1.同步集合使用
2.CountDownLatch使用
3.http ajax
4.zxing二維碼生成
?
三、流程及實現(xiàn)原理
?
1.打開網(wǎng)頁,通過ajax請求獲取二維碼圖片地址
2.頁面渲染二維碼圖片,并通過長連接請求,獲取后端的登錄認證信息
3.事先登錄過APP的手機掃碼二維碼,然后APP請求服務(wù)器端的API接口,把用戶認證信息傳遞到服務(wù)器中。
4.后端收到APP的請求后,喚醒長連接的等待線程,并把用戶認證信息寫入session。
5.頁面得到長連接的響應(yīng),并跳轉(zhuǎn)到首頁。
整個流程圖下圖所示
?
?
?四、代碼編寫
?
?
pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.demo</groupId><artifactId>auth</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>auth</name><description>二維碼登錄</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.0.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-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- zxing --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.0</version></dependency><dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.3.0</version></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project> pom.xml?
?
首先,參照《玩轉(zhuǎn)spring boot——簡單登錄認證》完成簡單登錄認證。在瀏覽器中輸入http://localhost:8080頁面時,由于未登錄認證,則重定向到http://localhost:8080/login頁面
代碼如下:
package com.demo.auth;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;/*** 登錄配置 博客出處:http://www.cnblogs.com/GoodHelper/**/ @Configuration public class WebSecurityConfig implements WebMvcConfigurer {/*** 登錄session key*/public final static String SESSION_KEY = "user";@Beanpublic SecurityInterceptor getSecurityInterceptor() {return new SecurityInterceptor();}public void addInterceptors(InterceptorRegistry registry) {InterceptorRegistration addInterceptor = registry.addInterceptor(getSecurityInterceptor());// 排除配置addInterceptor.excludePathPatterns("/error");addInterceptor.excludePathPatterns("/login");addInterceptor.excludePathPatterns("/login/**");// 攔截配置addInterceptor.addPathPatterns("/**");}private class SecurityInterceptor extends HandlerInterceptorAdapter {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {HttpSession session = request.getSession();if (session.getAttribute(SESSION_KEY) != null)return true;// 跳轉(zhuǎn)登錄String url = "/login";response.sendRedirect(url);return false;}} }?
?
其次,新建控制器類:MainController
/*** 控制器* * @author 劉冬博客http://www.cnblogs.com/GoodHelper**/ @Controller public class MainController {@GetMapping({ "/", "index" })public String index(Model model, @SessionAttribute(WebSecurityConfig.SESSION_KEY) String user) {model.addAttribute("user", user);return "index";}@GetMapping("login")public String login() {return "login";} }?
新建兩個html頁面:index.html和login.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>二維碼登錄</title> </head> <body><h1>二維碼登錄</h1><h4><a target="_blank" href="http://www.cnblogs.com/GoodHelper/">from劉冬的博客</a></h4><h3 th:text="'登錄用戶:' + ${user}"></h3> </body> </html>?
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>二維碼登錄</title> <script src="//cdn.bootcss.com/angular.js/1.5.6/angular.min.js"></script> <script type="text/javascript">/*<![CDATA[*/var app = angular.module('app', []);app.controller('MainController', function($rootScope, $scope, $http) {//二維碼圖片src $scope.src = null;//獲取二維碼 $scope.getQrCode = function() {$http.get('/login/getQrCode').success(function(data) {if (!data || !data.loginId || !data.image)return;$scope.src = 'data:image/png;base64,' + data.image$scope.getResponse(data.loginId)});}//獲取登錄響應(yīng) $scope.getResponse = function(loginId) {$http.get('/login/getResponse/' + loginId).success(function(data) {//一秒后,重新獲取登錄二維碼if (!data || !data.success) {setTimeout($scope.getQrCode(), 1000);return;}//登錄成功,進去首頁 location.href = '/'}).error(function(data, status) {console.log(data)console.log(status)//一秒后,重新獲取登錄二維碼 setTimeout($scope.getQrCode(), 1000);})}$scope.getQrCode();});/*]]>*/ </script> </head> <body ng-app="app" ng-controller="MainController"><h1>掃碼登錄</h1><h4><a target="_blank" href="http://www.cnblogs.com/GoodHelper/">from劉冬的博客</a></h4><img ng-show="src" ng-src="{{src}}" /> </body> </html>?
login.html頁面先請求后端服務(wù)器,獲取登錄uuid,然后獲取到服務(wù)器的二維碼后在頁面渲染二維碼。接著使用長連接請求并等待服務(wù)器的相應(yīng)。
?
然后新建一個承載登錄信息的類:LoginResponse
package com.demo.auth;import java.util.concurrent.CountDownLatch;/*** 登錄信息承載類* * @author 劉冬博客http://www.cnblogs.com/GoodHelper**/ public class LoginResponse {public CountDownLatch latch;public String user;// 省略 get set }?
?
最后修改MainController類,最終的代碼如下:
package com.demo.auth;import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit;import javax.imageio.ImageIO; import javax.servlet.http.HttpSession;import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.SessionAttribute;import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;/*** 控制器* * @author 劉冬博客http://www.cnblogs.com/GoodHelper**/ @Controller public class MainController {/*** 存儲登錄狀態(tài)*/private Map<String, LoginResponse> loginMap = new ConcurrentHashMap<>();@GetMapping({ "/", "index" })public String index(Model model, @SessionAttribute(WebSecurityConfig.SESSION_KEY) String user) {model.addAttribute("user", user);return "index";}@GetMapping("login")public String login() {return "login";}/*** 獲取二維碼* * @return*/@GetMapping("login/getQrCode")public @ResponseBody Map<String, Object> getQrCode() throws Exception {Map<String, Object> result = new HashMap<>();result.put("loginId", UUID.randomUUID());// app端登錄地址String loginUrl = "http://localhost:8080/login/setUser/loginId/";result.put("loginUrl", loginUrl);result.put("image", createQrCode(loginUrl));return result;}/*** app二維碼登錄地址,這里為了測試才傳{user},實際項目中user是通過其他方式傳值* * @param loginId* @param user* @return*/@GetMapping("login/setUser/{loginId}/{user}")public @ResponseBody Map<String, Object> setUser(@PathVariable String loginId, @PathVariable String user) {if (loginMap.containsKey(loginId)) {LoginResponse loginResponse = loginMap.get(loginId);// 賦值登錄用戶loginResponse.user = user;// 喚醒登錄等待線程 loginResponse.latch.countDown();}Map<String, Object> result = new HashMap<>();result.put("loginId", loginId);result.put("user", user);return result;}/*** 等待二維碼掃碼結(jié)果的長連接* * @param loginId* @param session* @return*/@GetMapping("login/getResponse/{loginId}")public @ResponseBody Map<String, Object> getResponse(@PathVariable String loginId, HttpSession session) {Map<String, Object> result = new HashMap<>();result.put("loginId", loginId);try {LoginResponse loginResponse = null;if (!loginMap.containsKey(loginId)) {loginResponse = new LoginResponse();loginMap.put(loginId, loginResponse);} elseloginResponse = loginMap.get(loginId);// 第一次判斷// 判斷是否登錄,如果已登錄則寫入sessionif (loginResponse.user != null) {session.setAttribute(WebSecurityConfig.SESSION_KEY, loginResponse.user);result.put("success", true);return result;}if (loginResponse.latch == null) {loginResponse.latch = new CountDownLatch(1);}try {// 線程等待loginResponse.latch.await(5, TimeUnit.MINUTES);} catch (Exception e) {e.printStackTrace();}// 再次判斷// 判斷是否登錄,如果已登錄則寫入sessionif (loginResponse.user != null) {session.setAttribute(WebSecurityConfig.SESSION_KEY, loginResponse.user);result.put("success", true);return result;}result.put("success", false);return result;} finally {// 移除登錄請求if (loginMap.containsKey(loginId))loginMap.remove(loginId);}}/*** 生成base64二維碼* * @param content* @return* @throws Exception*/private String createQrCode(String content) throws Exception {try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);hints.put(EncodeHintType.CHARACTER_SET, "utf-8");hints.put(EncodeHintType.MARGIN, 1);BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, 400, 400, hints);int width = bitMatrix.getWidth();int height = bitMatrix.getHeight();BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);for (int x = 0; x < width; x++) {for (int y = 0; y < height; y++) {image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);}}ImageIO.write(image, "JPG", out);return Base64.encodeBase64String(out.toByteArray());}}}?
其中,使用 ?Map<String, LoginResponse>?loginMap類存儲登錄請求信息
createQrCode方法是用于生成二維碼
getQrCode方法是給頁面返回登錄uuid和二維碼,前端頁面拿到登錄uuid后請求長連接等待二維碼的掃碼登錄結(jié)果。
setUser方法是提供給APP端調(diào)用的,在此過程中通過uuid找到對應(yīng)的CountDownLatch,并喚醒長連接的線程。而這里是為了做演示才把這個方法放到這個類里,在實際項目中,此方法不一定在這個類里或未必在同一個后端中。另外我把用戶信息的傳遞也寫在這個方法中了,而實際項目是通過其他的方式來傳遞用戶信息,這里僅僅是為了演示方便。
getResponse方法是處理ajax的長連接,并使用CountDownLatch等待APP端來喚醒這個線程,然后把用戶信息寫入session。
?
?入口類App.java
package com.demo.auth;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);} }?
?項目結(jié)構(gòu)如下圖所示:
?
?五、總結(jié)
?
打開瀏覽器輸入http://localhost:8080。運行效果如下圖所以:
?
?
使用CountDownLatch則避免了每隔500毫秒讀一次數(shù)據(jù)庫或redis的頻繁查詢性能問題。因為操作的是內(nèi)存數(shù)據(jù),所以性能非常高。
而CountDownLatch是java多線程中非常實用的類,二維碼掃碼登錄就是一個具有代表意義的應(yīng)用場景。當然,如果你不嫌代碼量大也可以用wait+notify來實現(xiàn)。另在java.util.concurrent包下,也有很多的多線程類能到達同樣的目的,我這里就不一一例舉了。
?
根據(jù)園友的建議,我發(fā)現(xiàn)本篇文章里的線程阻塞是設(shè)計缺陷,所以不循環(huán)查詢數(shù)據(jù)庫或redis里,但一臺服務(wù)器的線程數(shù)是有限的。在下篇我會改進這個設(shè)計
?
代碼下載
?
如果你覺得我的博客對你有幫助,可以給我點兒打賞,左側(cè)微信,右側(cè)支付寶。
有可能就是你的一點打賞會讓我的博客寫的更好:)
?
返回玩轉(zhuǎn)spring boot系列目錄
?
作者:劉冬.NET 博客地址:http://www.cnblogs.com/GoodHelper/ 歡迎轉(zhuǎn)載,但須保留版權(quán)轉(zhuǎn)載于:https://www.cnblogs.com/GoodHelper/p/8641905.html
總結(jié)
以上是生活随笔為你收集整理的spring boot高性能实现二维码扫码登录(上)——单服务器版的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 课程学习:程序设计与算法
- 下一篇: mybatis一对一联表查询的两种常见方