前言
?
本打算用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 {@Beanpublic 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";@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;// 跳轉登錄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.";@Autowiredprivate 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一分鐘,然后使用消息訂閱+廣播喚醒線程的方式來實現此功能。如果有懂原理的朋友可以給我留言。
?
?
代碼下載
?
如果你覺得我的博客對你有幫助,可以給我點兒打賞,左側微信,右側支付寶。
有可能就是你的一點打賞會讓我的博客寫的更好:)
?
返回玩轉spring boot系列目錄
?
?
?
?