前言javascript
目前網頁的主流登陸方式是經過手機掃碼二維碼登陸。我看了網上不少關於掃碼登陸博客後,發現基本思路大體是:打開網頁,生成uuid,而後長鏈接請求後端並等待登陸認證相應結果,然後端每一個幾百毫秒會循環查詢數據庫或redis,當查詢到登陸信息後則響應長鏈接的請求。css
然而,若是是小型應用則沒問題,若是用戶量,併發大則會出現很是嚴重的性能瓶頸。而問題的關鍵是使用了循環查詢數據庫或redis的方案。假設要優化這個方案可使用java多線程的同步集合+CountDownLatch來解決。html
1、環境前端
1.java 8(jdk1.8)java
2.maven 3.3.9git
3.spring boot 2.0github
2、知識點web
1.同步集合使用ajax
2.CountDownLatch使用redis
3.http ajax
4.zxing二維碼生成
3、流程及實現原理
1.打開網頁,經過ajax請求獲取二維碼圖片地址
2.頁面渲染二維碼圖片,並經過長鏈接請求,獲取後端的登陸認證信息
3.事先登陸過APP的手機掃碼二維碼,而後APP請求服務器端的API接口,把用戶認證信息傳遞到服務器中。
4.後端收到APP的請求後,喚醒長鏈接的等待線程,並把用戶認證信息寫入session。
5.頁面獲得長鏈接的響應,並跳轉到首頁。
整個流程圖下圖所示
4、代碼編寫
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>
首先,參照《玩轉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"; @Bean public 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 { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute(SESSION_KEY) != null) return true; // 跳轉登陸 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) }); } //獲取登陸響應 $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頁面先請求後端服務器,獲取登陸uuid,而後獲取到服務器的二維碼後在頁面渲染二維碼。接着使用長鏈接請求並等待服務器的相應。
而後新建一個承載登陸信息的類: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 { /** * 存儲登陸狀態 */ 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; } /** * 等待二維碼掃碼結果的長鏈接 * * @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); } else loginResponse = loginMap.get(loginId); // 第一次判斷 // 判斷是否登陸,若是已登陸則寫入session if (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(); } // 再次判斷 // 判斷是否登陸,若是已登陸則寫入session if (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後請求長鏈接等待二維碼的掃碼登陸結果。
setUser方法是提供給APP端調用的,在此過程當中經過uuid找到對應的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); } }
項目結構以下圖所示:
5、總結
打開瀏覽器輸入http://localhost:8080。運行效果以下圖因此:
使用CountDownLatch則避免了每隔500毫秒讀一次數據庫或redis的頻繁查詢性能問題。由於操做的是內存數據,因此性能很是高。
而CountDownLatch是java多線程中很是實用的類,二維碼掃碼登陸就是一個具備表明意義的應用場景。固然,若是你不嫌代碼量大也能夠用wait+notify來實現。另在java.util.concurrent包下,也有不少的多線程類能到達一樣的目的,我這裏就不一一例舉了。
根據園友的建議,我發現本篇文章裏的線程阻塞是設計缺陷,因此不循環查詢數據庫或redis裏,但一臺服務器的線程數是有限的。在下篇我會改進這個設計
若是你以爲個人博客對你有幫助,能夠給我點兒打賞,左側微信,右側支付寶。
有可能就是你的一點打賞會讓個人博客寫的更好:)