头图

一、需求描述

用户在PC端用微信扫描二维码实现后台登录
图示:
image.png
e97764478c9bca0905cc82d04af690e.jpg
image.png

二、实现原理

微信扫码登录.jpg
此处采用socket实现,当然也可以通过轮询去监测微信扫码状态

三、实现步骤

1. 微信公众平台小程序后台申请跳转链接

开发管理->开发设置->扫普通链接二维码打开小程序

image.png

2. PC请求websocket连接获取二维码key

客户端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--登录界面-->
<img id="qrCode" src="" alt="扫码">
</body>
</html>
<script>
    let socket;
    let timer;
    if (typeof WebSocket === "undefined") {
        alert("您的浏览器不支持socket");
    } else {
        // 实例化socket
        socket = new WebSocket(
            "wss://ip:port/platform-system/socket/wx/qrLogin");
        // 监听socket连接
        socket.onopen = () => {
            console.log('连接成功.......')
            //模拟心跳
            timer = setInterval(() => {
                socket.send(1)
            }, 30000)
        };
        // 监听socket错误信息
        socket.onerror = () => {
            console.log('连接发生错误.......')
        };
        // 监听socket消息
        socket.onmessage = ({data}) => {
            //判断状态
            //此处0代表刚建立连接,返回来的是二维码key
            if (data.code === 0) {
                //请求服务端获取二维码
                document.querySelector('#qrCode').src = `https://ip:port/platform-system/auth/wx/miniprogram/qrCode?pcKey=${data.data}`
            } else if (data.code === 1) {
                //关闭定时器
                clearInterval(timer)
                //关闭socket
                socket.close()
                //此处1代表登录成功
                location.href = '后台主页'
            } else {
                //其他情况代表小程序登录出现问题
                console.log(`登录出错:${data.msg}`)
                //关闭定时器
                clearInterval(timer)
                //关闭socket
                socket.close()
            }
        };
    }
</script>

服务端:

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
public class JsonWebSocketEncoder implements Encoder.Text<Result> {

    @Override
    public String encode(Result object) throws EncodeException {
        try {
            return JSONUtil.toJsonStr(object);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}
@Slf4j
@Component
@ServerEndpoint(value = "/socket/wx/qrLogin", encoders = JsonWebSocketEncoder.class)
public class WxQrLoginWebSocket {

    public static final ConcurrentMap<String, Session> sockets = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        log.info("客户端连接成功:{}", session.getId());
        //将连接结果告诉客户端用于携带进入小程序(此处直接用的雪花算法,可使用其他方式生成key)
        String key = IdUtil.getSnowflake().nextIdStr();
        //存放session
        sockets.put(key, session);
        log.info("当前在线客户端:{}个", sockets.size());
        //通知客户端
        sendMessage(key, Result.result("0", null, key));
    }

    /**
     * 链接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        try {
            sockets.entrySet().removeIf(entry -> session.getId().equals(entry.getValue().getId()));
            log.info("【websocket消息】连接断开,sessionId:" + session.getId());
            log.info("当前在线客户端:{}个", sockets.size());
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("【websocket消息】收到客户端心跳:{},{}", message, session.getId());
        log.info("当前在线客户端:{}个", sockets.size());
    }

    /**
     * 发送错误时的处理
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        try {
            //打印错误
            log.error("用户错误,原因:" + error.getMessage());
            error.printStackTrace();
            //关闭链接
            session.close();
            sockets.entrySet().removeIf(entry -> session.getId().equals(entry.getValue().getId()));
            log.info("当前在线客户端:{}个", sockets.size());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 消息
     *
     * @param key  key
     * @param data 数据
     **/
    public static void sendMessage(String key, Object data) {
        sockets.get(key).getAsyncRemote().sendObject(data);
    }
}

3. 小程序获取key并授权登录

客户端:

  1. 获取key

     onLoad(option) {
      if(option.q){
          //微信扫描扫描二维码进来的
          let url = decodeURIComponent(option.q)
          let params = this.getUrlParam(url)
          console.log('从微信扫码过来params')
          //此处需要存储这个pcKey后面会用到
          console.log(params.pcKey)
      }
     }
     getUrlParam(url){
        let params = url.split("?")[1].split("&");
        let obj = {};
        params.map(v => (obj[v.split("=")[0]] = v.split("=")[1]));
        return obj
     }
  2. 请求服务端登录并通知PC端

    wx.request({
     url: 'https://ip:port/...', //仅为示例,并非真实的接口地址
     method: 'POST',
     header: {
         'content-type': 'application/x-www-form-urlencoded'
     },
     data: {
         username: 'username',
         password: 'password'
     },
     success: function (res) {
         //获取token
         var token = res.data.accessToken;
         //通知服务端告诉PC登录
         wx.request({
             url: 'https://ip:port/auth/notifyPcLogin', //仅为示例,并非真实的接口地址
             method: 'GET',
             header: {
                 'Authorization': token
             },
             data: {
                 pcKey: '之前存储的key'
             },
             success: function (res) {
                 console.log(res.data)
             }
         })
     }
    })

    服务端:

    @RestController
    @RequestMapping("/auth")
    @RequiredArgsConstructor
    public class LoginController {
    
     private final WxLoginService wxLoginService;
    
     @ApiOperation(value = "通知PC登录接口")
     @GetMapping("/notifyPcLogin")
     public Result<Boolean> notifyPcLogin(
             @ApiParam(name = "pcKey", value = "PC端后端给的唯一标识", required = true)
             @NotBlank(message = "唯一标识不能为空") @RequestParam(value = "pcKey") String pcKey
     ) {
         return Result.success(wxLoginService.notifyPcLogin(pcKey));
     }
    }
    @Service
    @RequiredArgsConstructor
    public class WxLoginService {
    
     private final ISysUserService sysUserService;
    
     /**
      * 唤醒PC登录
      *
      * @param pcKey pc唯一key
      * @return java.lang.Boolean
      * @author Guo Shuai
      * @since 1.0
      **/
     public Boolean notifyPcLogin(String pcKey) {
         //获取socket session
         Session session = WxQrLoginWebSocket.sockets.get(pcKey);
         //判断是否存在
         if (ObjectUtil.isNull(session)) {
             return Boolean.FALSE;
         }
         //获取当前登录用户手机号
         String phone = sessionService.getLoginUser().getPhone();
         //通过手机号查找用户
         SysUser sysUser = sysUserService.lambdaQuery().eq(SysUser::getMobile, phone).one();
         //获取用户pc端token
         LoginUserVO loginUserVO = sysUserService.getUserToken(sysUser, sysUser.getMobile(), sessionService.getTenantId(), "10");
         //通知pc登录
         WxQrLoginWebSocket.sendMessage(pcKey, Result.result("1", null, loginUserVO));
         //返回
         return Boolean.FALSE;
     }
    }

    至此整个流程结束

    四、注意事项

    小程序申请的跳转链接只有正式版可以携带自定义的动态参数,否则只能用配置的测试地址,测试地址可以配置最多5个


Pursuer丶
7 声望4 粉丝

对技术抱有热情,对工作保持严谨!