前言
本打算用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的依赖:
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.session
spring-session-data-redis
完整的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.0modelVersion> <groupId>com.demogroupId> <artifactId>authartifactId> <version>0.0.1-SNAPSHOTversion> <packaging>jarpackaging> <name>authname> <description>二维码登录description> <parent> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-parentartifactId> <version>2.0.0.RELEASEversion> <relativePath /> parent> <properties> <project.build.sourceEncoding>UTF-8project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding> <java.version>1.8java.version> properties> <dependencies> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-thymeleafartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-devtoolsartifactId> <scope>runtimescope> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-testartifactId> <scope>testscope> dependency> <dependency> <groupId>com.google.zxinggroupId> <artifactId>coreartifactId> <version>3.3.0version> dependency> <dependency> <groupId>com.google.zxinggroupId> <artifactId>javaseartifactId> <version>3.3.0version> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-redisartifactId> dependency> <dependency> <groupId>org.springframework.sessiongroupId> <artifactId>spring-session-data-redisartifactId> dependency> <dependency> <groupId>commons-codecgroupId> <artifactId>commons-codecartifactId> dependency> dependencies> <build> <plugins> <plugin> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-maven-pluginartifactId> 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">
/**/
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 MapgetQrCode() throws Exception { Map 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 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 setUser(@PathVariable String loginId, @PathVariable String user) { ValueOperations opsForValue = redisTemplate.opsForValue(); String value = opsForValue.get(LOGIN_KEY + loginId); if (value != null) { opsForValue.set(LOGIN_KEY + loginId, user, 1, TimeUnit.MINUTES); } Map 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 getResponse(@PathVariable String loginId, HttpSession session) { Map result = new HashMap<>(); result.put("loginId", loginId); ValueOperations 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 hints = new Hashtable (); 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系列目录