代码编织梦想

一、概述

1、扫码登录介绍

二维码扫描登录原理

二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情:告诉系统我是谁,以及向系统证明我是谁。

比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;

2、扫码登录原理

  • PC 端发送 “扫码登录” 请求,服务端生成二维码 id,并存储二维码的过期时间、状态等信息
  • PC 端获取二维码并显示
  • PC 端开始轮询检查二维码的状态,二维码最初为 "待扫描"状态
  • 手机端扫描二维码,获取二维码 id
  • 手机端向服务端发送 “扫码” 请求,请求中携带二维码 id、手机端 token 以及设备信息
  • 服务端验证手机端用户的合法性,验证通过后将二维码状态置为 “待确认”,并将用户信息与二维码关联在一起,之后为手机端生成一个一次性 token,该 token 用作确认登录的凭证
  • PC 端轮询时检测到二维码状态为 “待确认”
  • 手机端向服务端发送 “确认登录” 请求,请求中携带着二维码 id、一次性 token 以及设备信息
  • 服务端验证一次性 token,验证通过后将二维码状态置为 “已确认”,并为 PC 端生成 PC 端 token
  • PC 端轮询时检测到二维码状态为 “已确认”,并获取到了 PC 端 token,之后 PC 端不再轮询
  • PC 端通过 PC 端 token 访问服务端

二、扫码登录实战(轮询版)

1、环境准备

  • SpringBoot
  • Lombok
  • Redis

2、RedisTemplate序列化

//序列化RedisTemplate
@Configuration
public class RedisConfig {
    // 编写自己的RedisTemplate
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        // 序列化时会自动增加类类型,否则无法反序列化
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash采用String序列方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

3、Token工具类

/**
 * token的工具类
 * 使用jwt生成/验证token(jwt JSON Web Token)
 * jwt由三部分组成: 头部(header).载荷(payload).签证(signature)
 * <p>
 * 1.header头部承载两部分信息:
 * {
 *   “type”: “JWT”, 声明类型,这里是jwt
 *   “alg”: “HS256” 声明加密的算法 通常直接使用 HMAC SHA256
 * }
 * 将头部进行base64加密, 构成了第一部分
 * <p>
 * 2.payload载荷就是存放有效信息的地方
 *  (1).标准中注册的声明
 *  (2).公共的声明 (一般不建议存放敏感信息)
 *  (3).私有的声明 (一般不建议存放敏感信息)
 * 将其进行base64加密,得到Jwt的第二部分
 * <p>
 * 3.signature签证信息由三部分组成:
 * (1).header (base64后的)
 * (2).payload (base64后的)
 * (3).secret
 * 需要base64加密后的header和base64加密后的payload连接组成的字符串,
 * 然后通过header中声明的加密方式进行加盐secret组合加密,构成了jwt的第三部分
 */
@Slf4j
public class TokenUtil {
    /**
     * token的失效时间:25天
     */
    private final static long TIME_OUT = 25 * 24 * 60 * 60 *1000L;

    /**
     * token的密钥
     */
    private final static String SECRET = "shawn222";

    /**
     * 生成token
     *
     * @return String
     */
    public static String token(String userId) {
        String token = null;
        try {
            Date date = new Date(System.currentTimeMillis() + TIME_OUT);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            Map<String, Object> headers = new HashMap<>();
            headers.put("type", "jwt");
            headers.put("alg", "HS256");
            token = JWT.create()
                    .withClaim("account", userId)
                    .withExpiresAt(date)
                    .withHeader(headers)
                    .sign(algorithm);
        } catch (IllegalArgumentException | JWTCreationException e) {
            e.printStackTrace();
        }
        return token;
    }

    /**
     * token验证
     *
     * @param token token
     * @return String
     */
    public static boolean verify(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier jwtVerifier = JWT.require(algorithm).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            // 客户端可以解密 所以一般不建议存放敏感信息
            log.info("account:" + decodedJWT.getClaim("account").asString());
            return true;
        } catch (IllegalArgumentException | JWTVerificationException e) {
            e.printStackTrace();
            return false;
        }

    }

4、定义扫码状态

public enum CodeStatus {

    /**
     * 过期
     */
    EXPIRE,

    /**
     * 未使用的二维码
     */
    UNUSED,

    /**
     * 已扫码, 等待确认
     */
    CONFIRMING,

    /**
     * 确认登录成功
     */
    CONFIRMED

}

5、定义返回类

@Data
@NoArgsConstructor
public class CodeVO<T> {

    /**
     * 二维码状态
     */
    private CodeStatus codeStatus;

    /**
     * 提示消息
     */
    private String message;

    /**
     * 正式 token
     */
    private T token;

    public CodeVO(CodeStatus codeStatus) {
        this.codeStatus = codeStatus;
    }

    public CodeVO(CodeStatus codeStatus,String message) {
        this.codeStatus = codeStatus;
        this.message = message;
    }

    public CodeVO(CodeStatus codeStatus,String message,T token) {
        this.codeStatus = codeStatus;
        this.message = message;
        this.token=token;
    }

}

6、定义二维码工具类

/**
 * 二维码工具类
 */
public class CodeUtil {

    /**
     * 获取过期二维码存储信息
     *
     * @return 二维码值对象
     */
    public static CodeVO getExpireCodeInfo() {
        return new CodeVO(CodeStatus.EXPIRE,"二维码已更新");
    }

    /**
     * 获取未使用二维码存储信息
     *
     * @return 二维码值对象
     */
    public static CodeVO getUnusedCodeInfo() {
        return new CodeVO(CodeStatus.UNUSED,"二维码等待扫描");
    }

    /**
     * 获取已扫码二维码存储信息
     */
    public static CodeVO getConfirmingCodeInfo() {
        return new CodeVO(CodeStatus.CONFIRMING,"二维码扫描成功,等待确认");
    }

    /**
     * 获取已扫码确认二维码存储信息
     * @return 二维码值对象
     */
    public static CodeVO getConfirmedCodeInfo(String token) {
        return new CodeVO(CodeStatus.CONFIRMED, "二维码已确认",token);
    }

}

7、编写相应方法

@Slf4j
@Service
public class LoginService {

    @Resource
    RedisTemplate<String, Object> redisTemplate;

     /**
     * 生成uuid
     */
    public CommonResult<String> generateUUID(){
        try{
            String uuid = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(RedisKeyUtil.getScanUUID(uuid),
                    CodeUtil.getUnusedCodeInfo(),RedisKeyUtil.getTimeOut(), TimeUnit.MINUTES);
            return new CommonResult<>(uuid);
        }catch (Exception e){
            log.warn("redis二维码生成异常{}",e.getMessage());
        }

        return new CommonResult("二维码异常,请重新扫描",400);

    }
    
    /**
     * uuid状态信息
     */
    public CommonResult<CodeVO> getInfoUUID(String uuid) {

        Object object = redisTemplate.opsForValue().get(RedisKeyUtil.getScanUUID(uuid));
        if(object==null){
            return new CommonResult("二维码不存在或者已过期",400);
        }
        return new CommonResult<>((CodeVO)object);
    }
    
    
    /**
     * 扫描登录,去确认二维码
     */
    public CommonResult scanQrLogin(String uuid, String account) {
        try {
            Object o = redisTemplate.opsForValue().get(RedisKeyUtil.getScanUUID(uuid));
            if(null==o){
                return new CommonResult<>("二维码异常,请重新扫描",400);
            }
            CodeVO codeVO = (CodeVO) o;
            //获取状态
            CodeStatus codeStatus = codeVO.getCodeStatus();
            // 如果未使用
            if(codeStatus==CodeStatus.UNUSED){
                redisTemplate.opsForValue().set(RedisKeyUtil.getScanUUID(uuid),
                        CodeUtil.getConfirmingCodeInfo(),RedisKeyUtil.getTimeOut(), TimeUnit.MINUTES);
                //你的逻辑
                
                return new CommonResult<>("请确认登录",200,null);
            }
        }catch (Exception e){
            log.warn("二维码异常{}",e.getMessage());
            return new CommonResult<>("内部错误",500);
        }
        return new CommonResult<>("二维码异常,请重新扫描",400);
    }
    
    /**
     * 确认登录,返回学生token以及对应信息
     * @param uuid
     * @param id 学生id
     * @return
     */
    public CommonResult confirmQrLogin(String uuid, String id) {

        try{
            CodeVO codeVO = (CodeVO) redisTemplate.opsForValue().get(RedisKeyUtil.getScanUUID(uuid));
            if(null==codeVO){
                return new CommonResult<>("二维码已经失效,请重新扫描",400);
            }
            //获取状态
            CodeStatus codeStatus = codeVO.getCodeStatus();
            // 如果正在确认中,查询学生信息
            if(codeStatus==CodeStatus.CONFIRMING){
                //你的逻辑

                // 生成token
                String token = TokenUtil.token(studentLoginVO.getAccount());
                
                //redis二维码状态修改,PC可以获取到
                redisTemplate.opsForValue().set(RedisKeyUtil.getScanUUID(uuid),
                        CodeUtil.getConfirmedCodeInfo(token),RedisKeyUtil.getTimeOut(), TimeUnit.MINUTES);
                
                
                return new CommonResult<>("登陆成功",200);
            }
            return new CommonResult<>("二维码异常,请重新扫描",400);
        }
        catch (Exception e){
            log.error("确认二维码异常{}",e);
            return new CommonResult<>("内部错误",500);
        }
    }
}

三、扫码登录(长连接版)

当然不仅仅包括短轮训,还有SSE(Server-Send Events,可以用WebFlux实现)以及WebSocket长连接实现,可以参考:Spring Boot + Web Socket 实现扫码登录


参考文章:

Java 语言实现简易版扫码登录

Java实现二维码扫描登录

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lemon_TT/article/details/124973714

springboot之扫码登录实战_github轮子工厂的博客-爱代码爱编程

springBoot实战之揭秘网站微信扫码登录 由于微信端流量比较足,所以扫码登录系统功能也受到了很多系统的青睐,本文就来详细的解开该技术的面纱 演示效果 准备工作 1.需要一个微信开放平台账号,并创建

springboot实现微信扫码登录功能让网站支持使用微信登录_yongfeng.的博客-爱代码爱编程_springsecurity微信扫码登录

此功能基于微信开放平台。实现此功能的前提是需要有微信开放平台帐号,并认证(需300¥)成功。需要有公网可以访问的域名和IP。 一、微信开放平台认证流程 进入微信开放平台。使用帐号登录后进入到帐号中心–》开发者资格认证

springboot实现微信扫码登录并且绑定-爱代码爱编程

前言:系统中若用到微信扫码登录,则要进行微信公众账号授权,所以在开发功能之前, 需要到微信公众平台申请一个服务号,当然仅仅只是作为开发者,则使用测试公众账号也行。 有了公众号后,则需登录公众后台进行一些基础配置,配置流程如下 1.点击设置–>选择公众号设置功能设置–>配置好业务域名和网页授权域名。 2.点击开发–>选择

springboot实现微信扫码登录-爱代码爱编程

首先登录微信开放平台 找到对应开发微信登录的api文档 https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.htm 在application.properties中添加 # 微信开放平台 appid wx.open.app_id=wxe

SpringBoot+webSocket实现扫码登录功能-爱代码爱编程

扫码登录。之前项目使用的是 ajax轮询的方式。感觉太low了。 所以这次用webSocket的方式进行实现 好。废话不多说!咱们开始!! 一、首先咱们需要一张表 这表是干啥的呢?就是记录一下谁扫码了。谁登录了。 User_Token表 字段如下: uuid : 用于确保唯一性userId :谁登录的loginTime :

SpringBoot实现专有钉钉扫码登录-爱代码爱编程

专有钉钉扫码登录 参考专有钉钉开发文档,链接:https://openplatform-portal.dg-work.cn/portal/#/helpdoc?docKey=kfzn&slug=engk1k 一、官方流程 二、具体实现 1.准备工作,下载官方sdk ①.JAVA 语言 新文档对应sdk: zwdd-sdk-java-1

Spring Boot 实现扫码登录-爱代码爱编程

一、首先咱们需要一张表 这表是干啥的呢?就是记录一下谁扫码了。谁登录了。 User_Token表 字段如下: uuid : 用于确保唯一性userId :谁登录的loginTime :登录时间createTime :创建时间 用于判断是否过期state:是否二维码失效 0有效 1失效二、角色都有哪些 咱们还需要分析一下子。扫码登录这

几行代码实现SpringBoot图片上传-爱代码爱编程

首先创建一个SpringBoot项目。 创建一个SpringBoot项目 创建成功后在静态志愿目录下会产生static template 目录 当我们的SpringBoot项目启动时会自动配置好静态资源访问,当我们的静态资源文件放在resources目录下的static,public,resources, /META-INF/resources目录下时

字节面试官:“这92道 spring boot 面试题都答不上来?”_java晴天过后的博客-爱代码爱编程

Spring Boot面试题 1、什么是Spring Boot? 多年来,随着新功能的增加,spring变得越来越复杂。只需访问页面,我们就会看到可以在我们的应用程序中使用的所有Spring项目的不同功能。如果必须启动一个新的Spring项目,我们必须添加构建路径或添加Maven依赖关系,配置应用程序服务器,添加spring配置。因此,开始一个新的sp

elasticsearch整合springboot&elasticsearch-爱代码爱编程

一:Elasticsearch-Rest-Client 1)9300:TCP spring-data-elasticsearch:transport-api.jar; springboot 版本不同, transport

【保姆级】springboot整合cucumber(bdd) >>> part1_springboot bdd-爱代码爱编程

最近需要搞BDD的单元测试。。。抓耳挠腮 记录下在实际项目中使用Cucumber Test的具体过程,包含多个避坑指南(刨了一个星期的坑= =),希望可以帮你节约大量时间(机智脸.gif) 1. 下载Cucumber Test的项目模版 GitHubhttps://github.com/cucumber/cucumber-java-skeleton

spring源码_spring 源码包-爱代码爱编程

搭建环境 生成的源码 本次使用的源码Spring4.3.x,JDK8,gradle4.10.2,idea2018.2.5 Spring源码使用的是Gradle 进行构建的,所以需要下载 Gradle 以及搭建 java开发环境 Java环境使用的是 JDK1.8 Gradle版本4.10.2 Gradle下载  spring-framework

springboot整合redisson实战(一)整合 redisson-爱代码爱编程

文章目录 前言引入redisson依赖redisson-spring-data与Spring Boot version的版本对应关系maven依赖 配置文件redisson.yml多节点配置