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

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

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

腾讯文档采用的是轮询的方式,我在这里采用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();

          })

        }, () => {})

      }

    })

  },

微信小程序调试


这里的启动参数就是页面上小程序二维码里面的参数scene=(你的参数)

这样调试的时候,就不用了手机扫码了


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容