spring boot高性能实现二维码扫码登录(上)——单服务器版

前言


 

  目前网页的主流登录方式是通过手机扫码二维码登录。我看了网上很多关于扫码登录博客后,发现基本思路大致是:打开网页,生成uuid,然后长连接请求后端并等待登录认证相应结果,而后端每个几百毫秒会循环查询数据库或redis,当查询到登录信息后则响应长连接的请求。

然而,如果是小型应用则没问题,如果用户量,并发大则会出现非常严重的性能瓶颈。而问题的关键是使用了循环查询数据库或redis的方案。假设要优化这个方案可以使用java多线程的同步集合+CountDownLatch来解决。

 

一、环境


 

1.java 8(jdk1.8)

2.maven 3.3.9

3.spring boot 2.0

 

二、知识点


 

1.同步集合使用

2.CountDownLatch使用

3.http ajax

4.zxing二维码生成

 

三、流程及实现原理


 

1.打开网页,通过ajax请求获取二维码图片地址

2.页面渲染二维码图片,并通过长连接请求,获取后端的登录认证信息

3.事先登录过APP的手机扫码二维码,然后APP请求服务器端的API接口,把用户认证信息传递到服务器中。

4.后端收到APP的请求后,唤醒长连接的等待线程,并把用户认证信息写入session。

5.页面得到长连接的响应,并跳转到首页。

整个流程图下图所示

 

 

 四、代码编写


 

 

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>
pom.xml

 

 

首先,参照《玩转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";@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

/*** 控制器* * @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);} elseloginResponse = loginMap.get(loginId);// 第一次判断// 判断是否登录,如果已登录则写入sessionif (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();}// 再次判断// 判断是否登录,如果已登录则写入sessionif (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);}
}

 

 项目结构如下图所示:

 

 五、总结


 

打开浏览器输入http://localhost:8080。运行效果如下图所以:

 

 

使用CountDownLatch则避免了每隔500毫秒读一次数据库或redis的频繁查询性能问题。因为操作的是内存数据,所以性能非常高。

而CountDownLatch是java多线程中非常实用的类,二维码扫码登录就是一个具有代表意义的应用场景。当然,如果你不嫌代码量大也可以用wait+notify来实现。另在java.util.concurrent包下,也有很多的多线程类能到达同样的目的,我这里就不一一例举了。

 

根据园友的建议,我发现本篇文章里的线程阻塞是设计缺陷,所以不循环查询数据库或redis里,但一台服务器的线程数是有限的。在下篇我会改进这个设计

 

代码下载

 

如果你觉得我的博客对你有帮助,可以给我点儿打赏,左侧微信,右侧支付宝。

有可能就是你的一点打赏会让我的博客写的更好:)

 

返回玩转spring boot系列目录

 

作者:刘冬.NET 博客地址:http://www.cnblogs.com/GoodHelper/ 欢迎转载,但须保留版权

转载于:https://www.cnblogs.com/GoodHelper/p/8641905.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/369261.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

查看 固态硬盘位置_3米防摔+人脸/指纹解锁:西数Armorlock移动固态硬盘

要求快速而又安全的数据拷贝工具&#xff1f;指纹识别移动SSD大家应该都见过了&#xff0c;今天西数推出了一个更为特别的人脸/指纹识别加密移动SSD。G-Technology Armorlock使用AES256全盘加密固态硬盘中的数据&#xff0c;解锁方式不是常见的密码或自带指纹传感器&#xff0c…

CCF - 201403-2 - 窗口

问题描述 试题编号&#xff1a;201403-2试题名称&#xff1a;窗口时间限制&#xff1a;1.0s内存限制&#xff1a;256.0MB问题描述&#xff1a; 问题描述在某图形操作系统中,有 N 个窗口,每个窗口都是一个两边与坐标轴分别平行的矩形区域。窗口的边界上的点也属于该窗口。窗口之…

通过Spring集成从Hibernate 3迁移到4

本周是时候将我们的代码库升级到最新的Hibernate 4.x了。 我们推迟了迁移&#xff08;仍在Hibernate 3.3上&#xff09;&#xff0c;因为3.x分支的较新维护版本需要对API进行一些更改&#xff0c;这些更改显然仍在不断变化中。 一个示例是UserType API&#xff0c;该API仍然存在…

web前端工程师全套教程免费分享

这是我自己早前听课时整理的前端全套知识点&#xff0c;适用于初学者&#xff0c;也可以适用于中级的程序员&#xff0c;你们可以下载下来。我自认为还是比较系统全面的&#xff0c;可以抵得上市场上90%的学习资料。讨厌那些随便乱写的资料还有拿出来卖钱的人&#xff01;在这里…

vb在服务器上新建文件夹,vb.net-如果不存在,如何在VB中创建文件夹?

vb.net-如果不存在&#xff0c;如何在VB中创建文件夹&#xff1f;我为自己编写了一个小小的下载应用程序&#xff0c;以便我可以轻松地从服务器上获取一组文件&#xff0c;然后将它们全部放入带有全新安装的Windows的新PC上&#xff0c;而无需实际运行网络。 不幸的是&#xff…

mybatis一对一联表查询的两种常见方式

1.一条语句执行查询&#xff08;代码如下图&#xff09; 注释&#xff1a;class表&#xff08;c别名&#xff09;&#xff0c;teacher表&#xff08;t别名&#xff09;teacher_id为class表的字段t_id为teacher表的字段&#xff0c;因为两者有主键关联的原因&#xff0c;c_id为c…

在Windows 7中设置Java开发环境

一段时间以来&#xff0c;我收到了很多愿意尝试Java语言的学生和人们的要求&#xff0c;它们提供了关于如何设置Java开发环境的简单指南&#xff0c;类似于我一年前写的那样。 Mac用户。 看到这里和这里 。 因此&#xff0c;本文主要针对Java开发新手&#xff0c;他们寻求有关使…

写给想成为前端工程师的同学们―前端工程师是做什么的?

前端工程师是做什么的&#xff1f; 前端工程师是互联网时代软件产品研发中不可缺少的一种专业研发角色。从狭义上讲&#xff0c;前端工程师使用 HTML、CSS、JavaScript 等专业技能和工具将产品UI设计稿实现成网站产品&#xff0c;涵盖用户PC端、移动端网页&#xff0c;处理视觉…

逆水寒服务器维护7.5,逆水寒7.26日维护到什么时候 逆水寒7.26日游戏改动汇总介绍...

逆水寒7.26日维护到什么时候 逆水寒7.26日游戏改动汇总介绍2018-07-26 10:08:08来源&#xff1a;游戏下载编辑&#xff1a;苦力趴评论(0)《逆水寒》官方发布微博&#xff0c;称为了保证服务器的运行稳定和服务质量&#xff0c;将于7月26日上午7:00-上午10:00进行停服维护。此次…

是否可以限制蓝牙传输距离_技术文章—关于蓝牙传输范围的常见误解

蓝牙技术在耳机、手机、手表及汽车领域的普及为人们带来了许多便利&#xff0c;却也引发了一些人们对于蓝牙的误解。目前&#xff0c;蓝牙可为多种重要的解决方案提供支持&#xff0c;其中包括家庭自动化、室内导航以及商业和工业创新等。误解一&#xff1a;蓝牙稳定传输的最远…

shell 统计行数

语法&#xff1a;wc [选项] 文件… 说明&#xff1a;该命令统计给定文件中的字节数、字数、行数。如果没有给出文件名&#xff0c;则从标准输入读取。wc同时也给出所有指定文件的总统计数。字是由空格字符区分开的最大字符串。 该命令各选项含义如下&#xff1a; - c 统计字节数…

Async分析

1&#xff1a;android在新版本中不允许UI线程访问网络&#xff0c;但是如果需要访问网络又改怎么办呐&#xff1f;这里有很多解决方案&#xff0c;比如新开一个线程&#xff0c;在新线程中进行访问&#xff0c;然后访问数据&#xff0c;返回后可能会更新界面也可能不更新界面&a…

JavaFX即将推出您附近的Android或iOS设备吗?

已经有大新闻最近在世界上的JavaFX的关于JavaFX的是许多更多的组件开源&#xff0c;开源的广告在2012 JavaOne大会 。 在2月的开源更新中 &#xff0c; Richard Bair汇编了一份JavaFX项目表&#xff0c;该表在撰写本文时&#xff08;2013年2月11日&#xff0c;星期一&#xff0…

基于webpack搭建的vue element-ui框架

花了1天多的时间&#xff0c; 终于把这个框架搭建起来了。 好了&#xff0c; 不多说了&#xff0c; 直接进入主题了。前提是安装了nodejs,至于怎么安装&#xff0c; 网上都有教程。 这里就不多说了&#xff0c; 这边使用的IDE是idea。1.在E:/my-project&#xff08;我的电脑上&…

mvc怎么请求服务器错误信息,asp.net-mvc – IIS显示服务器错误而不是自定义错误...

我正在使用MVC 5,我正在使用自定义视图处理我的错误,例如(404,403 ……等)它在我的本地IIS上工作正常,但是当我在登台服务器上发布时,它显示有关这些错误代码的IIS服务器错误消息.它显示了这条消息&#xff1a;代替&#xff1a;我修改了web.config for< customErrors mode “…

编译打包vue_Vue 源码分析( 一 )

Vue 源码分析&#xff08; 一 &#xff09;目录结构、版本、入口1、Vue 源码目录结构dist&#xff1a;打包之后生成的结果目录 examples&#xff1a;代码示例 scripts&#xff1a;配置文件 src&#xff1a;源代码目录compiler: 编译相关 &#xff08;将template模板转换成rende…

List 集合转换 json 字符串 ajax前台拼接

List 集合 转换为json 字符串public object Taoshow(){var i pbll.PackShow();//list集合var lida JsonConvert.SerializeObject(i); //转换成json字符串return lida;}function Tao() {$.ajax({url: "/Wangjie/Taoshow",type: "Get",dataType: "Jso…

【数论想法题】小C的问题 @科林明伦杯哈尔滨理工大学第八届程序设计竞赛...

Time Limit: 1000 MS Memory Limit: 256000 K Description 小C是一个可爱的女孩&#xff0c;她特别喜欢世界上最稳定的图形&#xff1a;三角形。有一天她得到了n根木棍&#xff0c;她把这些木棍随意的摆放成一行。小K来和小C玩&#xff0c;他发现了这排木棍&#xff0c;突然想…

使用grep4j轻松测试分布式组件上的SLA

因此&#xff0c;您的分布式体系结构如下图所示&#xff0c;您刚刚从企业那里收到了一项要求&#xff0c;以确保生产者发送并随后传输到下游系统&#xff08;消费者&#xff09;的消息的SLA必须快且永远不会慢于此。 400毫秒。 要求说&#xff1a; 从生产者发送到任何消费者的…

16进制颜色识别和搭配规律

在CSS中&#xff0c;经常会用到16进制的颜色来设置文本、背景、边框等颜色&#xff0c;但是对于一个纯前端来讲&#xff0c;16进制颜色的识别和搭配可能会较为陌生了 ,本文简单介绍一下16进制颜色的一些规律 16进制颜色的数值意义&#xff1a; 举个例子&#xff1a;比如 #1A2B…