前言
本打算用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
如果擷取到認證資訊,則跳轉進入内部系統。
如圖所示
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CMzgDN3ADMyMTL3kDN3ADO1kDM1IzMwgTMwITLxQjM4czLcNDM4EDMy8CXxQjM4czLcd2bsJ2Lc12bj5ycn9Gbi52YugTMwIzcldWYtl2Lc9CX6MHc0RHaiojIsJye.png)
2.微信的實作方式
每隔1分鐘調用一次 https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=【UUID】&tip=0&r=-1524754438&_=1521943100181
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>
pom.xml
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);
}
}
App.java
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>
index.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;
}
}
}
WebSecurityConfig
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一分鐘,然後使用消息訂閱+廣播喚醒線程的方式來實作此功能。如果有懂原理的朋友可以給我留言。
代碼下載下傳
如果你覺得我的部落格對你有幫助,可以給我點兒打賞,左側微信,右側支付寶。
有可能就是你的一點打賞會讓我的部落格寫的更好:)
傳回玩轉spring boot系列目錄
作者:劉冬.NET 部落格位址:http://www.cnblogs.com/GoodHelper/ 歡迎轉載,但須保留版權
作者:劉冬.NET
部落格位址:http://www.cnblogs.com/GoodHelper/
歡迎轉載,但須保留版權