前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Redis版】spring boot高性能实现二维码扫码登录(中)

【Redis版】spring boot高性能实现二维码扫码登录(中)

作者头像
BUG弄潮儿
发布2020-06-15 16:19:39
7150
发布2020-06-15 16:19:39
举报
文章被收录于专栏:JAVA乐园JAVA乐园

作者: 刘冬 来源:http://www.cnblogs.com/GoodHelper/p/8643071.html

前言

  本打算用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分钟,如果没有查到到认证信息,则会返回

代码语言:javascript
复制
window.code=408;

没有扫码就会一直等待。当一定时间不扫码二维码,页面就会强制刷新。

我猜想后端的机制和我上篇《spring boot高性能实现二维码扫码登录(上)——单服务器版》类似。

那么如果用户长时间不扫二维码,服务器的线程将不会被唤醒,微信是怎么做到高性能的。如果有园友知道,可以给我留言。

3.我的实现方式

好了,我这里选用支付宝的实现方式。因为简单粗暴,还高效。

流程如下:

1.前端发起成二维码的请求,并得到登录UUID

2.后端生成UUID后写入Redis。

3.前端每隔1秒发起一次请求,从Redis中获取认证信息,如果没有认证信息则返回waiting状态,如果查询到认证信息,则将认证信息写入seesion。

二、代码编写

pom.xml引入Redis及Session的依赖:

代码语言:javascript
复制
<!-- 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:

代码语言:javascript
复制
<?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入口类:

代码语言:javascript
复制
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

代码语言:javascript
复制
# session
spring.session.store-type=redis
代码语言:javascript
复制
 前端页面index.html和login.html
代码语言:javascript
复制
<!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
代码语言:javascript
复制
<!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:

代码语言:javascript
复制
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);
    }

}

登录处理类:

代码语言:javascript
复制
/**
 * 登录配置 博客出处: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类修改为:

代码语言:javascript
复制
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一分钟,然后使用消息订阅+广播唤醒线程的方式来实现此功能。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 BUG弄潮儿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档