前言
本打算用CountDownLatch來實現,但有個問題我沒有考慮,就是當使用者APP沒有掃二維碼的時候,執行緒會阻塞5分鐘,這反而造成效能的下降。好吧,現在迴歸傳統方式:前端ajax每隔1秒或2秒發一次請求,去查詢後端的登入狀態。
一、支付寶和微信的實現方式
1.支付寶的實現方式
每隔1秒會發起一次http請求,呼叫https://securitycore.alipay.com/barcode/barcodeProcessStatus.json?securityId=web%7Cauthcenter_qrcode_login%7C【UUID】&_callback=light.request._callbacks.callback3
如果獲取到認證資訊,則跳轉進入內部系統。
如圖所示
2.微信的實現方式
每隔1分鐘呼叫一次 https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=【UUID】&tip=0&r=-1524754438&_=1521943100181
而請求一次的時間預計是1分鐘,如果沒有查到到認證資訊,則會返回
window.code=408;
沒有掃碼就會一直等待。當一定時間不掃碼二維碼,頁面就會強制重新整理。
我猜想後端的機制和我上篇《spring boot高效能實現二維碼掃碼登入(上)——單伺服器版》類似。
那麼如果使用者長時間不掃二維碼,伺服器的執行緒將不會被喚醒,微信是怎麼做到高效能的。如果有園友知道,可以給我留言。
3.我的實現方式
好了,我這裡選用支付寶的實現方式。因為簡單粗暴,還高效。
流程如下:
1.前端發起成二維碼的請求,並得到登入UUID
2.後端生成UUID後寫入Redis。
3.前端每隔1秒發起一次請求,從Redis中獲取認證資訊,如果沒有認證資訊則返回waiting狀態,如果查詢到認證資訊,則將認證資訊寫入seesion。
二、程式碼編寫
pom.xml引入Redis及Session的依賴:
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- session --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
完整的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> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- session --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </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>
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); } }
resources/application.properties 中配置使用redis儲存session
# session
spring.session.store-type=redis
前端頁面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> <br /> <a href="/logout">登出</a> </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) { setTimeout($scope.getQrCode(), 1000); return; } //一秒後,重新獲取登入二維碼 if (!data.success) { if (data.stats == 'waiting') { //一秒後再次呼叫 setTimeout(function() { $scope.getResponse(loginId); }, 1000); } else { //重新獲取二維碼 setTimeout(function() { $scope.getQrCode(loginId); }, 1000); } return; } //登入成功,進去首頁 location.href = '/' }).error(function(data, status) { //一秒後,重新獲取登入二維碼 setTimeout(function() { $scope.getQrCode(loginId); }, 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>
bean配置類BeanConfig.java:
package com.demo.auth; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; @Configuration public class BeanConfig { @Bean public StringRedisTemplate template(RedisConnectionFactory connectionFactory) { return new StringRedisTemplate(connectionFactory); } }
登入處理類:
/** * 登入配置 部落格出處: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類修改為:
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.TimeUnit; import javax.imageio.ImageIO; import javax.servlet.http.HttpSession; import org.apache.commons.codec.binary.Base64; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; 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 static final String LOGIN_KEY = "key.value.login."; @Autowired private StringRedisTemplate redisTemplate; @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<>(); String loginId = UUID.randomUUID().toString(); result.put("loginId", loginId); // app端登入地址 String loginUrl = "http://localhost:8080/login/setUser/loginId/"; result.put("loginUrl", loginUrl); result.put("image", createQrCode(loginUrl)); ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); opsForValue.set(LOGIN_KEY + loginId, loginId, 5, TimeUnit.MINUTES); 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) { ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); String value = opsForValue.get(LOGIN_KEY + loginId); if (value != null) { opsForValue.set(LOGIN_KEY + loginId, user, 1, TimeUnit.MINUTES); } 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); ValueOperations<String, String> opsForValue = redisTemplate.opsForValue(); String user = opsForValue.get(LOGIN_KEY + loginId); // 長時間不掃碼,二維碼失效。需重新獲二維碼 if (user == null) { result.put("success", false); result.put("stats", "refresh"); return result; } // 登入掃碼二維碼 if (user.equals(loginId)) { result.put("success", false); result.put("stats", "waiting"); return result; } // 登入成,認證資訊寫入session session.setAttribute(WebSecurityConfig.SESSION_KEY, user); result.put("success", true); result.put("stats", "ok"); return result; } /** * 生成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()); } } @GetMapping("/logout") public String logout(HttpSession session) { // 移除session session.removeAttribute(WebSecurityConfig.SESSION_KEY); return "redirect:/login"; } }
三、執行效果:
如圖所示,效果與上篇一樣。
目前我在考慮微信的方式。我打算採用 CountDownLatch await一分鐘,然後使用訊息訂閱+廣播喚醒執行緒的方式來實現此功能。如果有懂原理的朋友可以給我留言。
如果你覺得我的部落格對你有幫助,可以給我點兒打賞,左側微信,右側支付寶。
有可能就是你的一點打賞會讓我的部落格寫的更好:)