欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于websocket实现扫描小程序码登录web页面和微信小程序(双端登录)

程序员文章站 2024-03-24 14:38:58
...

SpringBoot+WebSocket+Redis控制二维码失效时间

类似于腾讯文档网页版扫描小程序码登录的模式

基于websocket实现扫描小程序码登录web页面和微信小程序(双端登录)
腾讯文档采用的是轮询的方式,我在这里采用websocket的方式.
这里运用了github上基于微信SDK的更易用的SDK weixin-java-miniapp

第一步:导入需要的maven依赖

     <!--springboot整合了websocket-->
	   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-miniapp</artifactId>
            <version>3.3.0</version>
        </dependency>

第二步:配置websocket

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

这里的socketKey对应@PathParam定义的名字,用来识别唯一的socket连接

@Component
@ServerEndpoint(value = "/socketLogin/{socketKey}")
public class LoginSocket {

    private static Logger log= LoggerFactory.getLogger(LoginSocket.class);

    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();

    private Session session;

    /**
     * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen(Session session, @PathParam("socketKey")String socketKey) {
        this.session=session;
        log.info("[微信小程序websocket]socketKey:{}",socketKey +"-->建立连接");
        sessionMap.put(socketKey,session);
    }
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("socketKey") String socketKey) {
        log.info("[微信小程序websocket]socketKey:{}",socketKey +"-->断开连接");
        sessionMap.remove(socketKey);
    }

    public ConcurrentHashMap<String, Session> getSessionMap() {

        return sessionMap;
    }
}

第三步:完成socket配置后,正式开始微信小程序开发

开发之前需要准备好申请的小程序APPID SECRET
编写工具类(整合weixin-java-miniapp),这个工具类根据自身需求,灵活设置WxMaInMemoryConfig里面的内容

@Configuration
public class WxMaConfiguration {
    private static String appId;

    private static String secret;

    @Value("${weixin.applet_appid}")
    public void setAppId(String appId) {
        WxMaConfiguration.appId = appId;
    }

    @Value("${weixin.applet_secret}")
    public void setSecret(String secret) {
        WxMaConfiguration.secret = secret;
    }

    private static WxMaService wxMaService=null;

    @Bean
    public Object services(){
        WxMaInMemoryConfig config = new WxMaInMemoryConfig();
        config.setAppid(appId);
        config.setSecret(secret);
        wxMaService = new WxMaServiceImpl();
        wxMaService.setWxMaConfig(config);
        return Boolean.TRUE;
    }
    public static WxMaService getWxMaService(){
        return wxMaService;
    }
}

第四步:网页端获取小程序二维码接口

建议小程序二维码里面的scene参数和建立websocket连接的key保持一致
pathStr:小程序登录成功后微信小程序的跳转地址,而不是网页的跳转地址.如:pages/index/index
注意此处:3.3.0之前版本的 weixin-java-miniapp无法通过createWxaCodeUnlimitBytes返回byte字节,返回的都是File文件

 /**
     * 获取微信小程序带参数的二维码
     *
     * @return
     */
    @RequestMapping("/getAppletQrCode")
    public void getAppletCode(@RequestParam("sceneStr") String sceneStr, @RequestParam("pathStr") String pathStr, HttpServletResponse response) {
        logger.info("[微信小程序]获取微信小程序二维码,参数->sceneStr:"+sceneStr+", pathStr:"+pathStr);
        WxMaService wxMaService = WxMaConfiguration.getWxMaService();
        // 获取小程序二维码生成实例
        WxMaQrcodeService wxMaQrcodeService = wxMaService.getQrcodeService();
        // 设置小程序二维码线条颜色为黑色
        WxMaCodeLineColor lineColor = new WxMaCodeLineColor("0", "0", "0");
        // 生成二维码图片字节流  
        byte[] qrCodeBytes = null;
        try {
            qrCodeBytes = wxMaQrcodeService.createWxaCodeUnlimitBytes(sceneStr, pathStr, 430, false, lineColor, false);
            //设置二维码180s失效
            redisUtils.setex("qrcode:"+sceneStr,3*60,sceneStr);
        } catch (Exception e) {
            logger.error("[微信小程序]生成小程序码出现异常:{}",e);
        }
        response.setContentType("image/png");
        //写入response的输出流中
        try {
            OutputStream outputStream = response.getOutputStream();
            outputStream.write(Base64.encodeBase64(qrCodeBytes));
            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            logger.error("[微信小程序]输出流写出小程序码出现异常:{}",e);
        }
    }

第五步:小程序登录接口,对于前端来说以上接口都在网页端调取,而此接口在小程序端调取,要做好兼容

/**
     * 初次未授权登录
     * 微信小程序授权登录
     *
     * @param code
     * @return https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
     */
    @RequestMapping("/weChatAppletLogin")
    @ResponseBody
    public ResultBean weChatAuthLogin(@RequestParam("code") String code, @RequestParam("encryptedData") String encryptedData, @RequestParam("ivStr") String ivStr, @RequestParam("nickname") String nickname, @RequestParam("headurl") String headurl, @RequestParam("socketKey") String socketKey,HttpServletResponse response) {
        logger.info("[微信小程序]微信小程序和网页端登录,参数->code:"+code+", encryptedData:"+encryptedData+", nickname:"+nickname+", headurl:"+headurl+", socketKey:"+socketKey);
        if (StringUtils.isBlank(code)) {
            return ResultBean.setError(1, "code码为空");
        }
        //微信接口参数
        Map<String, String> params = new HashMap<>();
        params.put("appid", APPLET_APPID);
        params.put("secret", APPLET_SECRET);
        params.put("js_code", code);
        params.put("grant_type", "authorization_code");
        try {
            String wxResult = HttpUtil.doGet(APPLET_URL, params);
            JSONObject jsonObject = JSONObject.parseObject(wxResult);

            if (jsonObject.get("errcode") != null && StringUtils.equalsIgnoreCase(jsonObject.get("errcode").toString(), "40163")) {
                return ResultBean.setError(1, "code码失效");
            }
            String session_key = jsonObject.get("session_key").toString();
            //第一次登录没有unionid
            Object unionidObj = jsonObject.get("unionid");
            Map<String,String> userInfo=null;
            String unionid=null;
            //第一次授权
            if (unionidObj == null) {
                userInfo = getAuthUnionId(session_key, encryptedData, ivStr);
                unionid = userInfo.get("unionid");
            }else{
                unionid=jsonObject.get("unionid").toString();
            }
            WXUser wxUser = wxService.findWXUserByunionid(unionid);
            WXUser user = new WXUser();
            user.setUnionid(unionid);
            user.setNickname(nickname);
            user.setHeadimgurl(headurl);
            //微信信息存在
            if (wxUser != null) {
                wxService.updateCloudWXUser(user);
            } else {
                //第一次登陆
                wxService.addWXUser(user);
            }
            Map<String, String> result = new HashMap<>();
            result.put("unionid", unionid);
            //设置小程序的登录状态缓存(若不设置,微信默认30天内没有操作就重新登录)
            //redisUtils.setex("applet:"+unionid,30*24*60*60,unionid);
            //这是网页端的socket返回数据
            ConcurrentHashMap<String, Session> sessionMap = loginSocket.getSessionMap();
            if (StringUtils.isNotBlank(socketKey)) {
                Session currentSession = sessionMap.get(socketKey);
                if (currentSession != null) {
                    String codeKey = redisUtils.getString("qrcode:" + socketKey);
                    ResultBean resultBean=null;
                    if(StringUtils.isBlank(codeKey)){
                        resultBean = ResultBean.setOk(1, "二维码已失效");
                    }else{
                        //扫描后删除缓存的二维码
                        redisUtils.delString("qrcode:" + socketKey);
                        //设置网页端登录有效时长为6小时
                        redisUtils.setex(unionid, 3600 * 6, unionid);
                        resultBean = ResultBean.setOk(0, "扫码登录成功",unionid);
                    }
                    String res = JSON.toJSONString(resultBean);
                    currentSession.getAsyncRemote().sendText(res);
                }
            }
            return ResultBean.setOk(0, "授权登录小程序成功", result);
        } catch (Exception e) {
            logger.error("[微信小程序]小程序授权登录出现异常:{}",e);
            return ResultBean.setError(1, "授权登录小程序失败");
        }
    }

 /**
     * 授权过后的再次免授权登录
     *
     * @param sessionKey
     * @param encryptedData
     * @param ivStr
     * @return 利用微信工具sdk对获取的用户信息解密
     */

    public Map<String,String> getAuthUnionId(String sessionKey, String encryptedData,String ivStr) {
        try {
            WxMaService wxMaService = WxMaConfiguration.getWxMaService();
            WxMaUserService userService = new WxMaUserServiceImpl(wxMaService);
            WxMaUserInfo userInfo = userService.getUserInfo(sessionKey, encryptedData, ivStr);
            String unionId = userInfo.getUnionId();
            String nickName = userInfo.getNickName();
            String avatarUrl = userInfo.getAvatarUrl();
            Map<String, String> userInfoMap = new HashMap<>();
            userInfoMap.put("unionid", unionId);
            userInfoMap.put("nickname", nickName);
            userInfoMap.put("headurl",avatarUrl);
            return userInfoMap;
        } catch (Exception e) {
            logger.error("[微信小程序]已授权登录出现异常:{}",e);
            return null;
        }
    }

前端demo

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://libs.baidu.com/jquery/1.9.1/jquery.js"></script>

    <script type="text/javascript">
        $(function () {
            var ip="xxx.xxx.xxx.xxx";
            var sceneStr = "scend-" + new Date().getTime() + Math.ceil(Math.random() * 888888 + 1000000);
            //建立websocket192.168.101.123
            //var s = encodeURIComponent(sceneStr);
            var ws = new WebSocket("ws://"+ip+":端口号/项目名(没有不写)/websocket路径/" + sceneStr)
            ws.onopen = function () {
                console.log("websocket建立连接")
            }
            ws.onmessage = function (ev) {
                var parse = JSON.parse(ev.data);
                console.log(parse)
                var code1 = parse.code;
                if(code1==0){
                    ws.onclose=function () { console.log("websocket连接关闭")}
                    window.location.href="http://www.baidu.com";
                }
            }

            var url="小程序码接口地址";
            var pathStr="pages/index/index";
            $.post(url,{
                sceneStr: sceneStr,
                pathStr: pathStr
            },function(result){
                $("#qrcode").attr("src","data:image/png;base64,"+result);
            })


        })

    </script>
</head>
<body>

<img id="qrcode" src="">

</body>
</html>

小程序端demo(此demo仅做参考)

import {
  Base64,
  Crypto
} from './utils/ossUpload';
import {
  format
} from './utils/util.js';
App({
  onLaunch: function(option) {
    this.onLaunchArgu = option;
    console.log(option)
    this.init();
    this.globalData.os = this._getSystemOs()
    console.log(this._getSystemInfo())
  },
  requestArr:[],
  // 初始化
  init(fallback){
    wx.checkSession({
      success: () => {
        console.log('登录没过期了')
          // 获取用户信息
          wx.getSetting({
            success: res => {
              if (res.authSetting['scope.userInfo']) {
                // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
                this.getUserInfo().then(res => {
                  this.globalData.userInfo = res.userInfo
                  // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
                  // 所以此处加入 callback 以防止这种情况
                  if (this.userInfoReadyCallback) {
                    this.userInfoReadyCallback(res)
                  }
                }, () => {})
              }
            }
          })
      },
      fail(res) {
        console.log('登录过期了');
      },
      complete(res) {
        console.log(res);
      }
    })
  },
  login(callback) {
    wx.login({
      success: res => {
        const code = res.code;
        this.getUserInfo().then(res => {
          this.request({
            url: '/studyassistant/weixin/weChatAppletLogin',
            data: {
              #这里的参数是扫二维码获取到的,方便调试,直接拿取
              socketKey: this.onLaunchArgu.query.scene,  
              code,
              encryptedData: res.encryptedData,
              ivStr: res.iv,
              headurl: res.userInfo.avatarUrl,
              nickname: encodeURIComponent(res.userInfo.nickName)
            }
          }, false).then(res => {
            this.globalData.unionId = res.data.unionid;
            this._setStorageSync({
              unionId: res.data.unionid,
            })
            if (this.requestArr.length) {
              this.requestArr.map((item) => {
                this.request(item.option, item.author);
              })
              this.request()
            }
            callback();
          })
        }, () => {})
      }
    })
  },

微信小程序调试

基于websocket实现扫描小程序码登录web页面和微信小程序(双端登录)
这里的启动参数就是页面上小程序二维码里面的参数scene=(你的参数)
这样调试的时候,就不用了手机扫码了
基于websocket实现扫描小程序码登录web页面和微信小程序(双端登录)