Java實(shí)現(xiàn)二維碼掃描登錄

操作步驟

  • PC端生成二維碼
  • 手機(jī)端登錄后,才能掃描PC端二維碼。
  • 手機(jī)端掃描二維碼發(fā)送通知給PC端。
  • 手機(jī)端確認(rèn)登錄,發(fā)送通知給PC端,你已經(jīng)登錄成功。

截取圖

PC端生成二維碼

1pc端生成二維碼.png

手機(jī)端登錄

2手機(jī)端登錄.png

手機(jī)端掃描二維碼

3掃描電腦二維碼.png

PC接收到登錄通知

4PC端提示已掃描.png

手機(jī)端確認(rèn)登錄

5確認(rèn)PC端登錄.png

PC端接收到確認(rèn)登錄通知

6登錄成功.png

使用技術(shù)

  • springboot

  • webflux

  • SSE(Server-sent Events)是WebSocket的一種輕量代替方案,使用 HTTP 協(xié)議。

  • vue

  • uniapp

  • springJpa

  • mysql

  • hutool

后端業(yè)務(wù)定義

  • 手機(jī)端登錄接口

  • 生成PC端二維碼接口

  • PC端監(jiān)聽二維碼session狀態(tài)接口,目前定義狀態(tài):0 二維碼生成成功 ,1 手機(jī)端掃碼成功 2手機(jī)端確認(rèn)登錄 -1 sessionId過期失效

  • 二維碼掃描通知,手機(jī)端掃描成功會調(diào)用此接口,發(fā)送session通知

  • 手機(jī)端確認(rèn)通知,手機(jī)端確認(rèn)登錄會調(diào)用此接口,發(fā)送確認(rèn)登錄通知

PC端業(yè)務(wù)定義

  • 顯示登錄掃描二維碼,使用base64編碼顯示二維碼
  • 二維碼顯示成功后,使用SSE方式開啟二維碼session監(jiān)聽狀態(tài),狀態(tài):0 二維碼生成成功 ,1 手機(jī)端掃碼成功 2手機(jī)端確認(rèn)登錄 -1 sessionId過期失效

手機(jī)端業(yè)務(wù)定義

  • 調(diào)用登錄接口,跳轉(zhuǎn)到掃描二維碼界面
  • 掃描二維碼,發(fā)送掃描通知
  • 二維碼有效,跳轉(zhuǎn)到確認(rèn)登錄界面
  • 在確認(rèn)登錄界面點(diǎn)擊確認(rèn)登錄,發(fā)送確認(rèn)登錄通知

后端接口代碼

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;


    /**
     * 緩存放二維碼會話,設(shè)置1分鐘過期
     */
    private TimedCache qrCodeSession= CacheUtil.newTimedCache(1000*60);

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


    /**
     * 用戶手機(jī)端登錄方法
     * @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 二維碼 生成狀態(tài) ,1 掃碼狀態(tài) 2 登錄狀態(tài)
            sessionPojo.setStatus(0);

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

            //綁定返回體數(shù)據(jù)
            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);
        });
    }


    /**
     * 監(jiān)聽二維碼 session 狀態(tài)
     * @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 二維碼 生成狀態(tài) ,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 二維碼 生成狀態(tài) ,1 掃碼成功 2 登錄成功  -1 sessionId過期失效
            mobileTokenSession.get(token);
            sessionPojo.setStatus(1);
            sessionPojo.setUsername(userEntity.getUsername());
            AjaxResult ajaxResult=AjaxResult.SUCCESS;
            ajaxResult.setMsg("掃描成功,等待手機(jī)端確認(rèn)操作");
            return Mono.just(ajaxResult);
        });


    }

    /**
     * 手機(jī)端確認(rèn)登錄
     * @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("用戶信息驗(yàn)證失效");
                return Mono.just(ajaxResult);
            }

            //修改二維碼session對象狀態(tài)
            //0 二維碼 生成狀態(tài) ,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">手機(jī)已掃描,等待確認(rèn)</div>
              </template>  
             
         </div>
         <div class="qcode-desc">無需關(guān)注 立刻登錄</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(){
        //發(fā)送Http長連接
        sseObj=new EventSource(this.requestIp+"/user/getSessionStatus?sessionId="+this.sessionId);
        //回調(diào)方法
        sseObj.onmessage=(evt)=>{
             let resultJson=JSON.parse(evt.data);
            //session狀態(tài)
            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>

手機(jī)端代碼

登錄代碼

<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>

掃描二維碼發(fā)送掃描通知代碼

<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('條碼內(nèi)容:' + 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>

發(fā)送確認(rèn)登錄代碼

<template>
    <view>
        <view class="username">{{username}}</view>
        <view v-if="msg==''">
            <button @click="mobileOkPcLogin()">確認(rèn)登錄</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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 手機(jī)掃碼二維碼實(shí)現(xiàn)登錄某個網(wǎng)站的操作過程為,手機(jī)登錄某個APP,利用“掃一掃”功能掃描網(wǎng)頁上的二維碼,掃描成功后...
    mysimplebook閱讀 17,181評論 1 13
  • 序 本文主要來研究一下二維碼登錄的相關(guān)場景和原理。 場景 主要的場景有如下幾個: app掃二維碼登錄pc版系統(tǒng)比如...
    go4it閱讀 849評論 0 5
  • 二維碼登錄的本質(zhì) 二維碼登錄本質(zhì)上也是一種登錄認(rèn)證方式。既然是登錄認(rèn)證,要做的也就兩件事情! 告訴系統(tǒng)我是誰 向系...
    涅槃快樂是金閱讀 1,823評論 0 32
  • 拿二維碼掃描登錄這件事來說,其實(shí)它的本質(zhì)就是一種登錄認(rèn)證方式,二維碼在中間共有 3 個狀態(tài):待掃描、已掃描待確認(rèn)、...
    Amok校長閱讀 735評論 0 0
  • 首先介紹下自己的背景: 我11年左右入市到現(xiàn)在,也差不多有4年時間,看過一些關(guān)于股票投資的書籍,對于巴菲特等股神的...
    瞎投資閱讀 5,944評論 3 8

友情鏈接更多精彩內(nèi)容