使用SSE实现移动端扫码登录PC端

网上搜索的扫码登录大部分采用的都是PC端轮训获取二维码状态的方式实现扫码登录,需要写定时任务,服务端还需要保存二维码主键的扫码状态,感觉比较麻烦,感觉用服务器通知的方法比较简单,所以考虑只用这种方式,服务端通知的方式可以使用websocket或者SSE,websocket是双向通讯,搭建起来需要引入插件,二维码刷新还是需要使用定时任务,但是使用SSE可以根据SSE的超时来触发二维码超时刷新的功能,而且不需要额外引入其他插件.完美解决,废话不说,上代码.

服务端

controler类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/sse")
public class SSEControler {

    @Autowired private UserService userService;

    //获取uuid
    @GetMapping("/createQrCode")
    public SseEmitter createQrCode (String uuid){
        SseEmitter emitter = SseServer.createConnect(uuid);
        SseServer.sendMessage(uuid,"ing");
        return emitter;
    }


    //第3步:扫码
    @GetMapping("/scanQrCode")
    public Result<Object> scanQrCode (String uuid,String userId){
        try{
            User u = userService.getById(userId);
            SseServer.sendMessage(uuid,"ing",u.getUsername());
        }catch (Exception e){
            throw new Exception("二维码已过期");
        }

        return ResultUtil.data("成功");
    }


    @GetMapping("/scanQrCodeCencel")
    public Result<Object> scanQrCodeCencel (String uuid){
        try{
        SseServer.sendMessage(uuid,"cencel","取消授权");
    }catch (Exception e){
        throw new Exception("二维码已过期");
    }
        return ResultUtil.data("成功");
    }

    @GetMapping("/qrCodeConfirm")
    public void payback (String uuid,String userId){
        try{
         User u = userService.getById(userId);
        if (u == null) {
            throw new Exception("用户不存在");
        }
        //拿到用户后自己去鉴权获取token并返回
        String accessToken =  onAuthentication( u);
        SseServer.sendMessage(uuid,"finish",accessToken);
    }catch (Exception e){
        throw new Exception("二维码已过期");
        }
    }
}

SSE工具类


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * SseServer业务封装类来操作SEE
 */
@Slf4j
public class SseServer {

    /**
     * 当前连接总数
     */
    private static AtomicInteger currentConnectTotal = new AtomicInteger(0);

    /**
     * messageId的 SseEmitter对象映射集
     */
    private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    /**
     * 创建sse连接
     *
     * @param messageId - 消息id(唯一)
     * @return
     */
    public static SseEmitter createConnect(String messageId) {
        /**
         * 设置连接超时时间。0表示不过期,默认是30秒,超过时间未完成会抛出异常
         */
        SseEmitter sseEmitter = new SseEmitter(60*1000L);
        // 注册回调
        sseEmitter.onCompletion(completionCallBack(messageId));
        sseEmitter.onTimeout(timeOutCallBack(messageId));
        sseEmitter.onError(errorCallBack(messageId));
        sseEmitterMap.put(messageId, sseEmitter);

        //记录一下连接总数。数量+1
        int count = currentConnectTotal.incrementAndGet();
        log.info("创建sse连接成功 ==> 当前连接总数={}, messageId={}", count, messageId);
        return sseEmitter;
    }

    /**
     * 给指定 messageId发消息
     *
     * @param messageId - 消息id(唯一)
     * @param message   - 消息文本
     */
    public static void sendMessage(String messageId, String message) {
        if (sseEmitterMap.containsKey(messageId)) {
            try {
                sseEmitterMap.get(messageId).send(message);
            } catch (IOException e) {
                log.error("发送消息异常 ==> messageId={}, 异常信息:", messageId, e.getMessage());
                e.printStackTrace();
            }
        } else {
            throw new RuntimeException("连接不存在或者超时, messageId=" + messageId);
        }
    }

    public static void sendMessage(String messageId, String name,String message) {
        if (sseEmitterMap.containsKey(messageId)) {
            try {
                SseEmitter sseEmitter = sseEmitterMap.get(messageId);
                sseEmitter.send(SseEmitter.event().name(name).id(messageId).data(message));
            } catch (IOException e) {
                log.error("发送消息异常 ==> messageId={}, 异常信息:", messageId, e.getMessage());
                e.printStackTrace();
            }
        } else {
            throw new RuntimeException("连接不存在或者超时, messageId=" + messageId);
        }
    }

    /**
     * 给所有 messageId广播发送消息
     *
     * @param message
     */
    public static void batchAllSendMessage(String message) {
        sseEmitterMap.forEach((messageId, sseEmitter) -> {
            try {
                sseEmitter.send(message, MediaType.APPLICATION_JSON);
            } catch (IOException e) {
                log.error("广播发送消息异常 ==> messageId={}, 异常信息:", messageId, e.getMessage());
                removeMessageId(messageId);
            }
        });
    }

    /**
     * 给指定 messageId集合群发消息
     *
     * @param messageIds
     * @param message
     */
    public static void batchSendMessage(List<String> messageIds, String message) {
        if (CollectionUtils.isEmpty(messageIds)) {
            return;
        }
        // 去重
        messageIds = messageIds.stream().distinct().collect(Collectors.toList());
        messageIds.forEach(userId -> sendMessage(userId, message));
    }


    /**
     * 给指定组群发消息(即组播,我们让 messageId满足我们的组命名确定即可)
     *
     * @param groupId
     * @param message
     */
    public static void groupSendMessage(String groupId, String message) {
        if (MapUtils.isEmpty(sseEmitterMap)) {
            return;
        }
        sseEmitterMap.forEach((messageId, sseEmitter) -> {
            try {
                // 这里 groupId作为前缀
                if (messageId.startsWith(groupId)) {
                    sseEmitter.send(message, MediaType.APPLICATION_JSON);
                }
            } catch (IOException e) {
                log.error("组播发送消息异常 ==> groupId={}, 异常信息:", groupId, e.getMessage());
                removeMessageId(messageId);
            }
        });
    }

    /**
     * 移除 MessageId
     *
     * @param messageId
     */
    public static void removeMessageId(String messageId) {
        sseEmitterMap.remove(messageId);
        //数量-1
        currentConnectTotal.getAndDecrement();
        log.info("remove messageId={}", messageId);
    }

    /**
     * 获取所有的 MessageId集合
     *
     * @return
     */
    public static List<String> getMessageIds() {
        return new ArrayList<>(sseEmitterMap.keySet());
    }

    /**
     * 获取当前连接总数
     *
     * @return
     */
    public static int getConnectTotal() {
        return currentConnectTotal.intValue();
    }

    /**
     * 断开SSE连接时的回调
     *
     * @param messageId
     * @return
     */
    private static Runnable completionCallBack(String messageId) {
        return () -> {
            SseServer.sendMessage(messageId,"timeOut","二维码失效");
            log.info("结束连接 ==> messageId={}", messageId);
            removeMessageId(messageId);
        };
    }

    /**
     * 连接超时时回调触发
     *
     * @param messageId
     * @return
     */
    private static Runnable timeOutCallBack(String messageId) {
        return () -> {
            log.info("连接超时 ==> messageId={}", messageId);
            removeMessageId(messageId);
        };
    }

    /**
     * 连接报错时回调触发。
     *
     * @param messageId
     * @return
     */
    private static Consumer<Throwable> errorCallBack(String messageId) {
        return throwable -> {
            log.error("连接异常 ==> messageId={}", messageId);
            removeMessageId(messageId);
        };
    }
}

此处工具类是直接使用别人写好的,连接找不到了,等回头找到了再补

PC端

样式自己调,这里只实现功能,图片什么的可以自己找合适的

  1. 安装二维码生成插件
npm install qrcode
  1. 代码
   <div>
         <div v-if="!socialLogining">
           <div class="loginContent">
             <div class="smallTit">登录</div>
             <div class="scanQw" style="width: 30px;height: 30px;position: absolute;right: 8px;top:8px">
               <img v-if="!showQrCodeFlag" src="/scanQr.png" class="scanQrImg" @click="showQrCode" >
               <img v-if="showQrCodeFlag" src="/accLogin.png" class="scanQrImg" @click="showPwd" >
             </div>
             <div v-if="showQrCodeFlag" class="loginMain">
               <img v-show="!scanQrCodeIng" :src="qrCodeUrl" style="width: 300px;height: 300px">
               <div v-show="scanQrCodeIng"  style="width: 495px;height: 300px;color:#ffffff">
                 <div style="font-size:30px;color: #cc0000;text-align: center">{{scanQrCodeName}}</div>
                 <span style="font-size:30px;margin-top: 20px">已扫码</span>
                 <div style="font-size:30px;margin-top: 20px">请在手机上确认登录</div>
               </div>
             </div>
             <div v-else>
             <div  class="loginMain">
               <Form
                 ref="usernameLoginForm"
                 :model="form"
                 :rules="rules"
                 class="form"
               >
                 <FormItem class="username" prop="username">
                   <i class="userIcon"></i>
                   <Input
                     v-model="form.username"
                     size="large"
                     placeholder="请输入用户名"
                     autocomplete="off"
                   />
                 </FormItem>
                 <FormItem class="pwd" prop="password">
                   <i class="pwdIcon"></i>
                   <Input
                     type="password"
                     v-model="form.password"
                     size="large"
                     placeholder="请输入密码"
                     autocomplete="off"
                   />
                 </FormItem>
               </Form>
             </div>

             <Button
               class="login-btn"
               type="primary"
               size="large"
               :loading="loading"
               @click="submitLogin"
               long
             >
   
               <span >登录</span>
             </Button>
             </div>
           </div>
         </div>
         <div v-if="socialLogining">
           <RectLoading />
         </div>
       </div>
     <script>
       import QRCode from 'qrcode'
       export default {
   		  data() {
   		    return {
   		      scanQrCodeUuid:"",
   		      scanQrCodeIng:false,
   		      showQrCodeFlag:false,
   		      scanQrCodeName:"",
   		      source:null,
   		      qrCodeUrl:"",
   		      };
		 },
		  methods: {
   createQrCode(data){
     var qrUrl = ""
     QRCode.toDataURL(data, function (err, url) {
       qrUrl = url
     })
      this.qrCodeUrl = qrUrl;
   },
   showPwd(){
     if(this.showQrCodeFlag){
       this.showQrCodeFlag = !this.showQrCodeFlag;
       return;
     }
   },
  async showQrCode(){
   var that = this;
    this.scanQrCodeIng = false;
    var url = "http://"+window.location.host
    var uuid = this.uuid();
    this.scanQrCodeUuid = uuid
    this.source = new EventSource(
        url+'/xboot/sse/createQrCode?uuid='+uuid);
    await this.sseWatch();
    this.createQrCode(uuid)
       this.showQrCodeFlag = true;
   },

   sseWatch(){
     var that = this;
     this.source.onopen = function(e) {
     };
     //自定义finish事件,主动关闭EventSource
     this.source.addEventListener('ing', function(e) {
       that.scanQrCodeIng = true;
       that.scanQrCodeName = e.data
     }, false);

     this.source.addEventListener('finish', function(e) {
     //e中携带后端登录后的token
       that.$Message.success("登录中");
       //登录方法,调用自己的登录方法,下面的方法是我系统获取到token的方法
       that.afterLogin({result:e.data}) 
     }, false);
     this.source.addEventListener('cencel', function(e) {
       that.$Message.success("取消授权");
       that.scanQrCodeIng = false;
     }, false);
     this.source.addEventListener('timeOut', function(e) {
     }, false);
     //监听服务器端发来的事件:error
     this.source.onerror = function(e) {
       that.source.close();
       that.source=null;
       if(that.showQrCodeFlag){
         that.showQrCode()
       }
     };
   },
   uuid() {
     var s = [];
     var hexDigits = "0123456789abcdef";
     for (var i = 0; i < 36; i++) {
       s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
     }
     s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
     s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
     s[8] = s[13] = s[18] = s[23] = "-";

     var uuid = s.join("");
     return uuid;
   },
   }};
</script>
<style lang="less">
.scanQw {
 font-size: 16px;
 font-weight: bold;
 color: #333;
 cursor: pointer;
}
.scanQrImg  {
 display: none;
 width: 40px;
 height: 40px;
}
.scanQw:hover > .scanQrImg {
 display: block;
 width: 30px;
 height: 30px;
}
</style>

移动端

移动端使用uni-app开发,由于H5不支持扫码,所以需要使用uni的条件编译,里面提示请求后台的方法请改为使用自己的
注意:H5端使用摄像头必须使用https,本地调试可以使用花生壳做穿透(自带https)
页面

<template>
	<view>
		<!-- #ifdef H5 -->
			<view class="slot-wrap" style="margin-right: 25px;" @click="$refs.sc.init()">
				<u-icon name="scan" color="white" size="50"></u-icon>
			</view>
			<!-- #endif -->
			 <!-- #ifdef APP-PLUS || MP-WEIXIN-->
			<view class="slot-wrap" style="margin-right: 25px;" @click="scanInfo">
				<u-icon name="scan" color="white" size="50"></u-icon>
			</view>
			 <!-- #endif -->
			<!-- #ifdef H5 -->
		<scanCode ref="sc" @on-success="scanResult"></scanCode>
		<!-- #endif -->
		<u-modal v-model="showPcLogin" content="确认登录PC端" :show-confirm-button="true" :show-cancel-button = "true"  @cancel="pcCancel" @confirm="pcLogin"></u-modal>
	</view>
</template>
<script>
	// #ifdef H5
	import scanCode from '@/components/scanCode/scanCode.vue';//直接调用扫码方法调用
	// #endif
	
	export default {
		data() {
			return {
				pcUUID:"",
				showPcLogin:false,		
				}
			},
		// #ifdef H5
		components: {
			scanCode
		},
		// #endif
		methods: {
			pcCancel(){
				var that = this;
				this.$u.get('/sse/scanQrCodeCencel', {uuid:this.pcUUID}).then(res => {
						if(res.success){
							that.showPcLogin = false;
							that.$refs.uToast.show({
												title: '取消登录',
												type: 'success',
											})
						}
					})
					.catch(err => {});
			},
		
			pcLogin(){
				var that = this;
				this.$u.get('/sse/qrCodeConfirm', {uuid:this.pcUUID,userId:"自己获取当前登录用户的userId"}).then(res => {
						if(res.success){
							that.showPcLogin = false;
							that.$refs.uToast.show({
												title: '登录成功',
												type: 'success',
											})
						}
					})
					.catch(err => {});
			},
			scanInfo() {
				var _this = this
				uni.scanCode({
					success: function(res) {
						_this.scanQrCodeIng(res.result)
					}
				})
			},
			scanResult(v){
				var _this = this
				_this.scanQrCodeIng(v)
			},
			scanQrCodeIng(data){
				var that = this;
				this.pcUUID = data
				this.$u.get('/sse/scanQrCode', {uuid:data,userId:"自己获取当前登录用户的userId"}).then(res => {
						if(res.success){
							that.showPcLogin = true;
						}
					})
					.catch(err => {});
			},
			}
		}
	}
</script>

扫码组件
首先安装html5扫码,如果没有H5扫码可以直接删除条件编译

npm install vue-qrcode-reader
<template>
		<view>
			<u-modal class="qrcode" v-model="show" width="100%" :show-cancel-button="true" :show-title="false" :show-confirm-button="false" cancel-color="#387BF1">
				<view  class="slot-content">
					<qrcode-stream v-if="show" @decode="onDecode" @init="onInit" />
				</view>
			</u-modal>
		</view>
</template>
<script>
import { QrcodeStream } from 'vue-qrcode-reader';
export default {
	name:"scanCode",
	components: {
		QrcodeStream,
	},
	data() {
		return {
			show:false,
			qrcode: false,
			torchActive: false,
			camera: 'off',
			result:0,
		};
	},
	methods: {
		init(){
			this.show = true
		},
		onDecode(data) {
			this.$emit("on-success",data);
			this.show = false;
		},
		async onInit(promise) {
			console.log(promise);
			try {
				await promise;
			} catch (error) {
				if (error.name === 'NotAllowedError') {
					this.error = '请获取摄像头权限';
				} else if (error.name === 'NotFoundError') {
					this.error = '无摄像头';
				} else if (error.name === 'NotSupportedError') {
					this.error = '不支持摄像头';
				} else if (error.name === 'NotReadableError') {
					this.error = '摄像头在使用';
				} else if (error.name === 'OverconstrainedError') {
					this.error = '摄像头不是套装';
				} else if (error.name === 'StreamApiNotSupportedError') {
					this.error = 'Api不支持';
				}
			}
		}
	}
};
</script>
<style lang="scss">
</style>

大功告成

  • 27
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值