Java实现二维码扫描登录

操作步骤

  • PC端生成二维码
  • 手机端登录后,才能扫描PC端二维码。
  • 手机端扫描二维码发送通知给PC端。
  • 手机端确认登录,发送通知给PC端,你已经登录成功。

截取图

PC端生成二维码

1pc端生成二维码.png

手机端登录

2手机端登录.png

手机端扫描二维码

3扫描电脑二维码.png

PC接收到登录通知

4PC端提示已扫描.png

手机端确认登录

5确认PC端登录.png

PC端接收到确认登录通知

6登录成功.png

使用技术

  • springboot

  • webflux

  • SSE(Server-sent Events)是WebSocket的一种轻量代替方案,使用 HTTP 协议。

  • vue

  • uniapp

  • springJpa

  • mysql

  • hutool

后端业务定义

  • 手机端登录接口

  • 生成PC端二维码接口

  • PC端监听二维码session状态接口,目前定义状态:0 二维码生成成功 ,1 手机端扫码成功 2手机端确认登录 -1 sessionId过期失效

  • 二维码扫描通知,手机端扫描成功会调用此接口,发送session通知

  • 手机端确认通知,手机端确认登录会调用此接口,发送确认登录通知

PC端业务定义

  • 显示登录扫描二维码,使用base64编码显示二维码
  • 二维码显示成功后,使用SSE方式开启二维码session监听状态,状态:0 二维码生成成功 ,1 手机端扫码成功 2手机端确认登录 -1 sessionId过期失效

手机端业务定义

  • 调用登录接口,跳转到扫描二维码界面
  • 扫描二维码,发送扫描通知
  • 二维码有效,跳转到确认登录界面
  • 在确认登录界面点击确认登录,发送确认登录通知

后端接口代码

package com.xl.controller;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import com.xl.domain.entity.UserEntity;
import com.xl.domain.pojo.SessionPojo;
import com.xl.domain.result.AjaxResult;
import com.xl.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
 * 用户登录
 */
@Slf4j
@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserService userService;


    /**
     * 缓存放二维码会话,设置1分钟过期
     */
    private TimedCache qrCodeSession= CacheUtil.newTimedCache(1000*60);

    /**
     * 存储token信息
     */
    private TimedCache mobileTokenSession=CacheUtil.newTimedCache(1000*60);


    /**
     * 用户手机端登录方法
     * @return
     */
    @PostMapping(value = "/loginMobile")
    public AjaxResult<Map<String,String>> loginMobile(@RequestBody UserEntity userForm){
         UserEntity userEntity=userService.login(userForm);
         //登录成功
         if(ObjectUtil.isNotNull(userEntity)){
             AjaxResult ajaxResult = AjaxResult.SUCCESS;
             Map<String,String> resultMap=new HashMap<>();
             resultMap.put("username",userEntity.getUsername());
             String tokenId=IdUtil.objectId();
             resultMap.put("token",tokenId);
             log.debug("token={}",tokenId);
             ajaxResult.setData(resultMap);
             //存储到缓存中
             mobileTokenSession.put(tokenId,userEntity);
             return ajaxResult;
         }else{ //登录失败
             return AjaxResult.Fail;
         }
    }


    /**
     * 生成PC端登录二维码
     * @return
     */
    @GetMapping(value = "/pcQrCode")
    public Mono<AjaxResult<Map<String,String>>> pcQrCode(){
        return Mono.create((sink)->{
            //生成会话编号
            String sessionId= IdUtil.objectId();
            log.debug("sessionId={}",sessionId);
            SessionPojo sessionPojo=new SessionPojo();
            sessionPojo.setSessionId(sessionId);
            //0 二维码 生成状态 ,1 扫码状态 2 登录状态
            sessionPojo.setStatus(0);

            //生成base64二维码
            QrConfig qrConfig=new QrConfig();
            qrConfig.setWidth(300);
            qrConfig.setHeight(300);
            String base64Code=QrCodeUtil.generateAsBase64(sessionId, qrConfig,"jpeg");

            //绑定返回体数据
            AjaxResult<Map<String,String>> ajaxResult=AjaxResult.SUCCESS;
            Map<String,String> resultData=new HashMap<>();
            resultData.put("sessionId",sessionId);
            resultData.put("base64Code",base64Code);
            ajaxResult.setData(resultData);

            //存入缓存
            qrCodeSession.put(sessionId,sessionPojo);

            sink.success(ajaxResult);
        });
    }


    /**
     * 监听二维码 session 状态
     * @param sessionId
     * @return
     */
    @GetMapping(value = "/getSessionStatus",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Mono<AjaxResult<SessionPojo>> getSessionStatus(String sessionId){
        return Mono.create((sink)->{
            SessionPojo sessionPojo =(SessionPojo)qrCodeSession.get(sessionId);
            AjaxResult ajaxResult=AjaxResult.SUCCESS;
            if(ObjectUtil.isNull(sessionPojo)){
                sessionPojo=new SessionPojo();
                sessionPojo.setSessionId(sessionId);
                //0 二维码 生成状态 ,1 扫码成功 2 登录成功  -1 sessionId过期失效
                sessionPojo.setStatus(-1);
            }
            ajaxResult.setData(sessionPojo);
            sink.success(ajaxResult);
        });
    }


    /**
     * 扫码二维码成功
     * @return
     */
    @PostMapping(value = "/mobileScanOk")
    public Mono<AjaxResult> mobileScanOk(ServerWebExchange exchange)
    {

       return exchange.getFormData().flatMap((formData)->{
            //判断二维码session是否有效
            String sessionId = formData.getFirst("sessionId");
            String token=formData.getFirst("token");

            SessionPojo sessionPojo = (SessionPojo) qrCodeSession.get(sessionId);
            if(ObjectUtil.isNull(sessionPojo)){
                AjaxResult ajaxResult=AjaxResult.Fail;
                ajaxResult.setMsg("二维码已失效");
                return Mono.just(ajaxResult);
            }

           //判断token是否有效
           UserEntity userEntity = (UserEntity) mobileTokenSession.get(token);

            //0 二维码 生成状态 ,1 扫码成功 2 登录成功  -1 sessionId过期失效
            mobileTokenSession.get(token);
            sessionPojo.setStatus(1);
            sessionPojo.setUsername(userEntity.getUsername());
            AjaxResult ajaxResult=AjaxResult.SUCCESS;
            ajaxResult.setMsg("扫描成功,等待手机端确认操作");
            return Mono.just(ajaxResult);
        });


    }

    /**
     * 手机端确认登录
     * @return
     */
    @PostMapping(value = "/mobileOkPcLogin")
    public Mono<AjaxResult> mobileOkPcLogin(ServerWebExchange exchange){

        return exchange.getFormData().flatMap(formData -> {
            String sessionId = formData.getFirst("sessionId");
            String token=formData.getFirst("token");

            //判断二维码session是否有效
            SessionPojo sessionPojo = (SessionPojo) qrCodeSession.get(sessionId);
            if (ObjectUtil.isNull(sessionPojo)) {
                AjaxResult ajaxResult = AjaxResult.Fail;
                ajaxResult.setMsg("二维码已失效");
                return Mono.just(ajaxResult);
            }

            //判断token是否有效
            UserEntity userEntity = (UserEntity) mobileTokenSession.get(token);
            if (ObjectUtil.isNull(userEntity)) {
                AjaxResult ajaxResult = AjaxResult.Fail;
                ajaxResult.setMsg("用户信息验证失效");
                return Mono.just(ajaxResult);
            }

            //修改二维码session对象状态
            //0 二维码 生成状态 ,1 扫码成功 2 登录成功  -1 sessionId过期失效
            sessionPojo.setUsername(userEntity.getUsername());
            sessionPojo.setStatus(2);

            AjaxResult ajaxResult = AjaxResult.SUCCESS;
            ajaxResult.setMsg("PC登录成功");
            return Mono.just(ajaxResult);
        });

    }
}

PC端代码

<template>
  <div id="qcode-box" >
      <div class="q-login">
         <div class="title">扫一扫登录</div>
         <div class="success-login" v-if="sessionStatus==2">
                √&nbsp;登录成功,{{username}}
         </div>  
         <div class="qcode-img" v-else>
              <template v-if="sessionStatus==-1">
                  <div class="qr-expire" @click="resetRefresh()">
                     二维码过期,重新刷新
                  </div> 
              </template>
              <template v-else>
                <img :src="base64Code"/>
                <div  v-if="sessionStatus==1" class="qcode-tips">手机已扫描,等待确认</div>
              </template>  
             
         </div>
         <div class="qcode-desc">无需关注 立刻登录</div>
      </div>
  </div>
</template>

<script>
import axios from 'axios'
let sseObj;

export default {
  name: 'QCode',
  data () {
    return {
      requestIp:'http://localhost',
      base64Code: '',
      sessionId:'',
      sessionStatus:0,
      username:''
    }
  },
  mounted(){
    this.loadQrCode();
  },
  methods:{
    //加载
    loadQrCode(){
        axios.get(this.requestIp+'/user/pcQrCode').then((response)=>{
            let {data}=response.data;
            this.base64Code=data.base64Code;
            this.sessionId=data.sessionId;
            this.startQrCodeListener();
        })
    },
    //开启sse长连接
    startQrCodeListener(){
        //发送Http长连接
        sseObj=new EventSource(this.requestIp+"/user/getSessionStatus?sessionId="+this.sessionId);
        //回调方法
        sseObj.onmessage=(evt)=>{
             let resultJson=JSON.parse(evt.data);
            //session状态
            this.sessionStatus=resultJson.data.status;
            //等于-1表示sessionId过期
            if(this.sessionStatus==-1){
              sseObj.close();
              return;
            }else if(this.sessionStatus==2){
                this.username=resultJson.data.username;
                 sseObj.close();
            }
            console.log(evt.data);
        }

        sseObj.error=(evt)=>{
            sseObj.close();
        }
    },
    //重新刷新二维码
    resetRefresh(){
       sseObj.close();
       this.loadQrCode();
    },
  }

}
</script>

<style scoped>
  #qcode-box{
    width: 100vw;
    height: 100vh;
  }
 .q-login{
   background: #ebecef;
   width: 500px;
   height: 300px;
   margin: 0 auto;
   border-radius: 8px 8px;
   position: relative;
   top: 50%;
   transform: translateY(-50%);
 }
 .q-login >.title{
   line-height: 40px;
   font-size: 16px;
   font-weight: 600;
   color: #222226;
   text-align: center;
 }

  .q-login > .qcode-img{
    text-align: center;
    padding-top: 10px;
    position: relative;
  }
  .q-login > .qcode-img > img{
    width: 200px;
    height: 200px;
  }
  .q-login >.qcode-desc{
     text-align: center;
     font-size: 13px;
     color: #222226;
     margin-top: 10px;
  }
  .qr-expire{
    width: 200px;
    line-height: 200px;
    background: #000;
    margin: 0 auto;
    color: #fff;
    cursor: pointer;
    font-size: 13px;
    flex: 1;
  }
  .qcode-tips{
    width: 200px;
    line-height: 30px;
    position: absolute;
    background: #000;
    color: #fff;
    bottom: 0px;
    text-align: center;
    font-size: 13px;
    left: 50%;
    transform: translateX(-50%);

  }

  .success-login{
    line-height: 200px;
    background: #fff;
    color: #046590;
    text-align: center;
    font-size: 16px;
    font-weight: 800;
  }
</style>

手机端代码

登录代码

<template>
    <view class="content">  
        <view class="login-box">
            <view class="item">
                 <input type="text" v-model="httpUrl" placeholder="接口地址"/>
            </view>
            <view class="item">
                 <input type="text" v-model="username" placeholder="请输入用户名"/>
            </view>
            <view class="item">
                 <input type="password" v-model="password" placeholder="请输入密码" />
            </view>
            <view class="item">
                <button type="default" @click="login">登录</button>
            </view>
        </view>
    </view>
</template>

<script>
    export default {
        data() {
            return {
                httpUrl: 'http://192.168.1.61',
                username:'xiaoliang',
                password:'123456'
            }
        },
        onLoad() {

        },
        methods: {
            login(){
                let requestUrl=this.httpUrl+'/user/loginMobile';
                uni.request({
                    method:'POST',
                    url: requestUrl,
                    data: {
                        username: this.username,
                        password: this.password
                    },
                    header: {
                        'content-type': 'application/json' 
                    },
                    success: (res) => {
                        console.log(res.data);
                        let resultData=res.data;
                        let code=resultData.code;
                        if(code==0){ //登录成功
                             let storageInfo={username:this.username,token:resultData.data.token,httpUrl:this.httpUrl};
                             uni.setStorage({
                                 key: 'info',
                                 data: JSON.stringify(storageInfo)
                             });
                
                             uni.navigateTo({
                                 url: '../qrCode/qrCode'
                             });
                             
                        }else{
                            uni.showToast({
                                title: '登录失败',
                                duration: 2000
                            });
                        }
                    }
                });
            }
        }
    }
</script>

<style>
    .login-box{
        flex: 1;
        display: flex;
        flex-direction: column;
    }
    .login-box > .item{
        flex: 1;
    }
    .login-box > .item >input{
        height: 80rpx;
        border-bottom: solid 1px #eee;
        padding-left: 10rpx;
    }
    .login-box > .item >button{
        margin: 5rpx;
        margin-top: 10rpx;
    }
</style>

扫描二维码发送扫描通知代码

<template>
    <view>
        <view>
            <button @click="scanCode()">扫码</button>
        </view>
    </view>
</template>

<script>
    export default {
        data() {
            return {
                sessionId:'',
                token:'',
                httpUrl:'',
                username:''
            }
        },
        methods: {
            scanCode(){
                let _this=this;
                uni.scanCode({
                    onlyFromCamera: true,
                    success: function (res) {
                        console.log('条码类型:' + res.scanType);
                        console.log('条码内容:' + res.result);
                        _this.sessionId=res.result;
                        uni.getStorage({
                            key:'info',
                            success: (resStore)=>{
                                let infoJson=JSON.parse(resStore.data);
                                console.log(infoJson);
                                _this.token=infoJson.token;
                                _this.httpUrl=infoJson.httpUrl;
                                console.log(_this.token,_this.httpUrl);
                                _this.mobileScanOk();
                            }
                        });
                    }
                });
            },
            mobileScanOk(){
                let requestUrl=this.httpUrl+'/user/mobileScanOk'
                uni.request({
                    method:'POST',
                    url: requestUrl,
                    data: {
                        sessionId: this.sessionId,
                        token: this.token
                    },
                    header:{
                        "Content-Type": "application/x-www-form-urlencoded"
                    },
                    success: (res) => {
                        let resultData=res.data;
                        console.log(resultData);
                        if(resultData.code==0){
                            
                            let status=resultData.data.status;
                            if(status==1){ //登录成功
                                this.username=resultData.data.username;
                                uni.navigateTo({
                                    url: `./confirm?token=${this.token}&sessionId=${this.sessionId}&httpUrl=${this.httpUrl}&username=${this.username}`
                                });
                            }else{
                                uni.showToast({
                                    title: resultData.msg,
                                    duration: 2000
                                });
                            }
                        }else{
                            uni.showToast({
                                title: resultData.msg,
                                duration: 2000
                            });
                        }

                    }
                });
            }
            
        }
    }
</script>

<style>

</style>

发送确认登录代码

<template>
    <view>
        <view class="username">{{username}}</view>
        <view v-if="msg==''">
            <button @click="mobileOkPcLogin()">确认登录</button>
        </view>
        <view class="msg" v-else>
            {{msg}}
        </view>
    </view>
</template>

<script>
    export default {
        data() {
            return {
                sessionId:'',
                token:'',
                httpUrl:'',
                username:'',
                msg: ''
            }
        },
        onLoad(option){
            this.sessionId=option.sessionId;
            this.token=option.token;
            this.httpUrl=option.httpUrl;
            this.username=option.username;
            console.log(option);
        },
        methods: {
            mobileOkPcLogin(){
                let _this=this;
                let requestUrl=this.httpUrl+'/user/mobileOkPcLogin'
                uni.request({
                    method:'POST',
                    url: requestUrl,
                    header:{
                        "Content-Type": "application/x-www-form-urlencoded"
                    },
                    data: {
                        sessionId: this.sessionId,
                        token:this.token
                    },
                    success: (res) => {
                        let resultData=res.data;
                        _this.msg=resultData.msg;
                        console.log(resultData);
                    }
                });
            }
        }
    }
</script>

<style>
.username{
    text-align: center;
    font-size: 32rpx;
    line-height: 50rpx;
}
.msg{
    text-align: center;
    font-size: 30rpx;
    margin-top: 10rpx;
}
</style>

完整代码

开源地址:https://gitee.com/heliang230/qrcode-auth.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 手机扫码二维码实现登录某个网站的操作过程为,手机登录某个APP,利用“扫一扫”功能扫描网页上的二维码,扫描成功后...
    mysimplebook阅读 17,184评论 1 13
  • 序 本文主要来研究一下二维码登录的相关场景和原理。 场景 主要的场景有如下几个: app扫二维码登录pc版系统比如...
    go4it阅读 851评论 0 5
  • 二维码登录的本质 二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情! 告诉系统我是谁 向系...
    涅槃快乐是金阅读 1,824评论 0 32
  • 拿二维码扫描登录这件事来说,其实它的本质就是一种登录认证方式,二维码在中间共有 3 个状态:待扫描、已扫描待确认、...
    Amok校长阅读 737评论 0 0
  • 首先介绍下自己的背景: 我11年左右入市到现在,也差不多有4年时间,看过一些关于股票投资的书籍,对于巴菲特等股神的...
    瞎投资阅读 5,966评论 3 8

友情链接更多精彩内容