微信公众号开发(4)-实现PC扫码登录

一、PC微信扫码登录原理简介
PC端调用微信服务端的ticket接口,微信服务端获取ticket,PC端拿到ticket之后,生成带参数登录二维码,用户扫码之后会发送扫码事件消息到微信服务端,这个消息中会带上用户微信的openId,根据openId调用获取用户接口拿到用户信息,包含unionId、昵称、头像、性别等字段,这里可以将用户信息存入redis,在PC获取登录二维码之后我们要加一个轮询获取用户扫码状态,原理就是查询redis是否存入的用户信息,如果存入,那么开始做登录流程。

二、代码实现
1、微信服务端代码
首先写一个常量类,包括一些微信接口地址的常量和需要用到的一些字符串常量,因为本地项目已经开发完成,这里可能有一些在本章节不需要的多余常量。

public class WechatConstant {
     

    private WechatConstant(){
     }

    /**
     * 微信的TOKEN标识
     */
    public static final String WEIXIN_ACCESS_TOKEN = "YY_WEIXIN_ACCESS_TOKEN";

    /**
     * 小程序的TOKEN标识
     */
    public static final String XCX_ACCESS_TOKEN = "XCX_ACCESS_TOKEN";

    /**
     * 微信的 jsapi_ticket 标示
     */
    public static final String YY_WEIXIN_JSAPI_TICKET = "YY_WEIXIN_JSAPI_TICKET";

    /**
     * 获取token的微信URL地址
     */
    public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={?}&secret={?}";

    /**
     * 通过code换取网页授权access_token和openid
     */
    public static final String OPENID_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={?}&secret={?}&code={?}&grant_type=authorization_code";

    /**
     * 获取 jsapi_ticket 的微信URL地址
     */
    public static final String JS_API_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={?}&type=jsapi";

    /**
     * 获取用户信息地址
     */
    public static final String WX_USER_INFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={?}&openid={?}&lang=zh_CN";

    /**
     * 发送微信消息地址
     */
    public static final String SEND_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={?}";

    /**
     * 带参二维码地址
     */
    public static final String QRCODE_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={?}";

    /**
     * 小程二维码地址
     */
    public static final String XCX_QRCODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={?}";

    /**
     * 模板消息发送
     */
    public static final String SEND_TEMPLATE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={?}";

    public static final String OAUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=";

    public static final String MATERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token={?}";

    /**
     * 请求微信接口返回值字段
     */
    public static final String TICKET = "ticket";

    /**
     * 请求微信接口返回值字段
     */
    public static final String EXPIRES_IN = "expires_in";

    public static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

    public static final String OPENID = "openid";
    public static final String UNIONID = "unionid";

    public static final String ERRCODE = "errcode";

    public static final String ERRMSG = "errmsg";

    public static final String MINIPROGRAM = "miniprogram";

    public static final String ACCESS_TOKEN = "access_token";

    public static final String TOUSER = "touser";

    public static final String MSGTYPE = "msgtype";

    public static final String TEXT = "text";

    public static final String CONTENT = "content";

    public static final String TEMPLATE_ID = "template_id";
    public static final String XIN_TASK_ = "xin_task_";
    public static final String XIN_LOGIN_ = "xin_login_";

    public static final String NICKNAME = "nickname";
    public static final String HEADIMGURL = "headimgurl";
    public static final String HD_CODE = "HD_CODE";

    public static final String URL = "url";

    public static final String SUCCESS = "success";
    public static final String DATA = "data";
    public static final String VALUE = "value";

    public static final String AUTH_URL = "auth_url";
    public static final String PAGEPATH = "pagepath";
    public static final String APPID = "appId";
    public static final String TIMESTAMP = "timestamp";
    public static final String NONCESTR = "nonceStr";
    public static final String SIGNATURE = "signature";

    public static final String SCENE = "scene";
    public static final String PAGE = "page";
    public static final String WIDTH = "width";

    /**
     * 绑定微信前缀
     */
    public static final String WEIXIN_BIND_PREFIX = "bind_key_";

    public static final String WECHAT_BIND_ALLOW_NO_USER = "200";
    public static final String WECHAT_BIND_ALLOW_HAS_USER = "201";
    public static final String WECHAT_BIND_NOT_ALLOW_CODE = "202";

    /**
     * 论文搜索点击事件
     */
    public static final String WECHAT_SEARCH_EVENT = "SEARCH_EVENT";

    public static final String WECHAT_COOPERATION_EVENT = "COOPERATION_EVENT";

}

我们需要一个微信公共的service,这个service主要提供调用微信接口的实现,例如获取access_token、获取ticket、获取用户信息等实现方法。

整个微信服务端,需要使用同一的access_token,access_token的有效期为7200秒,我们获取access_token之后可存入redis,缓存有效期必需小于7200秒。具体代码如下:

/**
     * 获取微信的Token
     */
    public synchronized String getToken() throws Exception {
     
        String accessToken = wechatRedisService.get(WechatConstant.WEIXIN_ACCESS_TOKEN);
        if (StringUtils.isBlank(accessToken)) {
     
            String rest = restTemplate.getForObject(WechatConstant.ACCESS_TOKEN_URL, String.class, mp.getPub().getAppid(), mp.getPub().getAppSecret());
            logger.info("wx get token return:{}", rest);
            JSONObject jo = JSONObject.parseObject(rest);
            if (jo.containsKey(WechatConstant.ACCESS_TOKEN)) {
     
                accessToken = jo.getString(WechatConstant.ACCESS_TOKEN);
                int expires = jo.getInteger(WechatConstant.EXPIRES_IN) - 5 * 60;
                wechatRedisService.put(WechatConstant.WEIXIN_ACCESS_TOKEN, accessToken, expires);
            } else {
     
                logger.error("wx get token error:{}", rest);
                throw new WechatException("获取token失败,请稍后再试");
            }
        }
        return accessToken;
    }

获取带参数二维码的ticket:

/**
     * 获取带参二维码
     *
     * @param data
     * @return
     */
    public String getQrTicket(String data) throws Exception {
     
        if (StringUtils.isBlank(data)) {
     
            throw new WechatException("data is null");
        }
        logger.info("wx get QrTicket data:{}", data);
        String rest = restTemplate.postForObject(WechatConstant.QRCODE_URL, data, String.class, getToken());
        logger.info("wx get QrTicket rerutn:{}", rest);
        JSONObject jo = JSONObject.parseObject(rest);
        if (jo.containsKey(WechatConstant.TICKET)) {
     
            return jo.getString(WechatConstant.TICKET);
        }
        logger.error("wx get qrticket error:{}", rest);
        throw new WechatException(jo.getString(WechatConstant.ERRMSG));
    }

获取用户信息:

/**
     * 通过openid获取用户信息
     *
     * @param openId
     * @return
     * @throws Exception
     */
    public JSONObject getWxUserInfo(String openId) throws Exception {
     
        if (StringUtils.isBlank(openId)) {
     
            throw new WechatException("openid参数为空");
        }
        String rest = restTemplate.getForObject(WechatConstant.WX_USER_INFO_URL, String.class, getToken(), openId);
        logger.info("wx get userInfo return:{}", rest);
        JSONObject jo = JSONObject.parseObject(rest);
        if (jo.containsKey(WechatConstant.ERRCODE)) {
     
            logger.error("wx get userinfo error:{}", rest);
            throw new WechatException(jo.getString(WechatConstant.ERRMSG));
        }
        return jo;
    }

以上,需要用到的微信公共服务的实现已经完成,现在开始做扫码事件消息的处理。

在上一个章节中的handleReceiveMessage方法,添加事件消息类型的处理:

@Transactional
    public String handleReceiveMessage(String xml) {
     
        String response = null;
        try {
     
            String result = null;
            WechatMessage msg = parseMessage(xml);
            try {
     
                if (msg != null) {
     
                    String msgType = msg.getMsgType();
                    if (WeChatMsgType.REQ_MESSAGE_TYPE_TEXT.equals(msgType)) {
     
                        // 文本消息

                            msg = handleTextMessage(msg);
                            result = msg.getContent();
                    }  else if (msgType.equals(WeChatMsgType.REQ_MESSAGE_TYPE_EVENT)) {
      // 事件推送
                        result = handleEvent(msg);
                        msg.setMsgType(WechatConstant.TEXT);
                    }
                }
            } catch (Exception e) {
     
                logger.error("--handleReceiveMessage 处理微信消息异常: ", e);
                return WechatConstant.SUCCESS;
            }
            if (msg != null && StringUtils.isNotBlank(result)) {
     
                msg.setContent(result);
                response = msg.toXml();
                logger.info("wx response xml:{}", response);
            }
        } catch (Exception ex) {
     
            logger.error("--handleReceiveMessage 解析微信消息异常: ", ex);
            return WechatConstant.SUCCESS;
        }
        return response;
    }

事件消息处理handleEvent方法,这里要注意的点是,如果用户没有关注公众号,扫码之后的eventKey会加上"qrscene_"前缀,我们需要将它截取掉:

@Transactional
    public String handleEvent(WechatMessage msg) throws Exception {
     

        String result = null;
        // 事件类型
        String eventType = msg.getEvent();
        if (WeChatMsgType.EVENT_TYPE_SUBSCRIBE.equals(eventType)) {
     
            // 关注
            String eventKey = msg.getEventKey();
            if (StringUtils.isNotBlank(eventKey) && eventKey.startsWith("qrscene_")) {
     
                // 扫描带参数二维码关注
                String scene = eventKey.substring(8);
                if (scene.startsWith(WechatConstant.XIN_LOGIN_)) {
     
                    try {
     
                        result = processXinLogin(scene, msg);
                    } catch (Exception e) {
     
                        logger.error("扫描二维码登录异常, 异常消息:", e);
                        return "";
                    }
                }
            } else {
     
                // 搜索公众号关注 TODO
                result = wechatMsgService.findByCode(WechatMsgTypes.SEARCH_ATTENTION);
            }
        } else if (eventType.equals(WeChatMsgType.EVENT_TYPE_SCAN)) {
     
            // 扫描带参数二维码(已关注)
            String eventKey = msg.getEventKey();
            if (eventKey.startsWith(WechatConstant.XIN_LOGIN_)) {
     
                // 扫码登陆
                try {
     
                    result = processXinLogin(eventKey, msg);
                } catch (Exception e) {
     
                    logger.error("扫描二维码登录异常, 异常消息:", e);
                    return "";
                }
            } else {
     
                //用户注册【关注时】
                try {
     
                    result = processXinLogin(eventKey, msg);
                } catch (Exception e) {
     
                    logger.error("扫描二维码登录异常, 异常消息:", e);
                    return "";
                }
            }
        }
        return result;
    }

processXinLogin这个方法里面我做了注册逻辑,因为我的微信服务端代码和PC代码在同一个项目中

@Transactional
    private String processXinLogin(String scene, WechatMessage msg) {
     
        try {
     
            // 扫码登陆
            String loginKey = scene.substring(10);
            String loginStr = loginKey;

            Map<String, Object> map = addOrGetUserInfo(msg);
            OauthUser oauthUser = (OauthUser)map.get("oauthUser");
            String unionId = (String) map.get("unionId");
            wechatRedisService.put(loginStr, unionId,180);

            String wechatMsg = "";
            if(oauthUser.getIsVip()){
     
                //会员登录消息 TODO
                wechatMsg = wechatMsgService.findByCode(WechatMsgTypes.QRCODE_LOGIN_VIP);
            }else{
     
                //非会员登录消息 TODO
                wechatMsg = wechatMsgService.findByCode(WechatMsgTypes.QRCODE_LOGIN_NORMAL);
            }
            return String.format(wechatMsg, new SimpleDateFormat(WechatConstant.TIME_FORMAT).format(new Date()));
        }catch (Exception e){
     
            logger.error("processXinLogin failed,Error:",e);
            throw new WechatException("扫码登录失败");
        }

    }

    @Transactional
    private Map<String, Object> addOrGetUserInfo(WechatMessage msg){
     
        try{
     
            Map<String, Object> map = new HashedMap();
            JSONObject wxUserInfo = wechatService.getWxUserInfo(msg.getFrom());
            String unionid = (String) wxUserInfo.getOrDefault(WechatConstant.UNIONID, null);
            OauthUser oauthUser = oauthService.wechat_login(unionid,wxUserInfo);
            map.put("unionId",unionid);
            map.put("oauthUser",oauthUser);
            return map;
        }catch(Exception e){
     
            logger.error("addOrGetUserInfo failed,Error:",e);
            throw new WechatException("addOrGetUserInfo failed");
        }

    }

到此,微信服务端的代码已经完成,下面开始PC端代码。

获取带参数二维码tickect和轮询扫码登录状态接口:

@RequestMapping("/login-ajax")
    @ResponseBody
    public ApiResult weixinAjax(@RequestParam String oper,
                                @RequestParam(defaultValue = "", value = "login_s") String loginStr,
                                HttpServletRequest request, HttpServletResponse response) {
     
        ApiResult vo = null;
        Map<String, Object> map = new HashedMap();
        try {
     
            if ("login_ticket".equals(oper)) {
     
                map = oauthWechatService.getLoginTicket();
                vo = new ApiResult(BaseApiCode.SUCCESS, "success", map);
            } else if ("login_verify".equals(oper)) {
     
                map = oauthWechatService.loginVerify(loginStr, request, response);
                vo = new ApiResult(BaseApiCode.SUCCESS, "success", map);
            } else {
     
                vo = new ApiResult(BaseApiCode.FAILED, "未知操作");
            }
        } catch (WechatException e) {
     
            vo = new ApiResult(BaseApiCode.FAILED, e.getMessage());
        } catch (Exception ex) {
     
            logger.error("服务器错误", ex);
            vo = new ApiResult(BaseApiCode.FAILED, "服务器出错");
        }
        return vo;
    }
/**
     * 获取登录ticket
     * @return
     * @throws Exception
     */
    @Override
    public Map<String, Object> getLoginTicket() throws Exception {
     
        Map<String, Object> map = new HashedMap();
        String loginStr = getLoginRandomString();
        String sid = String.format("xin_login_%s", loginStr);
        wechatRedisService.delete(sid);
        String postData = String.format("{\"expire_seconds\": 300,\"action_name\": \"QR_STR_SCENE\", \"action_info\": {\"scene\": {\"scene_str\": \"%s\"}}}", sid);
        String rst = wechatService.getQrTicket(postData);
        if(StringUtils.isNotBlank(rst)){
     
            map.put("ticket",rst);
            map.put("loginStr",loginStr);
            return map;
        }else{
     
            throw new Exception("ticket有误");
        }
    }

    /**
     * 登录验证
     * @param loginStr
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    public Map<String, Object> loginVerify(String loginStr, HttpServletRequest request, HttpServletResponse response) throws Exception {
     
        Map<String, Object> map = new HashedMap();
        if (StringUtils.isBlank(loginStr)) {
     
            throw new Exception("无效登录");
        }
        String unionid = wechatRedisService.get(loginStr);
        if(StringUtils.isNotBlank(unionid)){
     
            OauthWechat bean = oauthWechatService.findByUnionid(unionid);
            if(bean == null){
     
                throw new Exception("unoinid无效");
            }
            OauthUser oauthUser = bean.getUser();
            String token = oauthService.getToken(oauthUser);
            map.put("token", token);
            map.put("expire", TokenRedisService.expire);
            map.put("user", userRedisService.getUserJson(oauthUser));
            Context.setCookieUser(response, oauthUser, token);
            userRedisService.put(oauthUser);
        }else{
     
            throw new WechatException("等待用户扫码");
        }
        return map;
    }

前端JS代码:

$(document).ready(function () {
     
    $("#login_mpweixin_img").click(function () {
     
        login_qrcode_get();
    });
    //login_qrcode_get();
});

var _c_t_i = null; //记录过期
var _v_t_i = null; //验证
var _l_g_s = "";  // 登录字符串
var _l_t_count = 0;
var _l_t_max = 20;
var _l_ticket = "";
function login_qrcode_get() {
     
    $("#login_mpweixin_img").attr('src','/static/_files/img/qrcode_loading.gif');
    login_ticket();
}

/**
 * 获取微信ticket, 生成微信登录二维码
 * @returns {boolean}
 */
function login_ticket() {
     

    if (_v_t_i) {
     
        clearTimeout(_v_t_i);
    }
    if (_c_t_i) {
     
        clearTimeout(_c_t_i);
    }
    var _t = new Date().getTime();
    if(_l_t_count>=_l_t_max){
     
        if(_l_ticket!=undefined&&_l_ticket!=""){
     
            $("#login_mpweixin_img").attr('src', 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + _l_ticket+'&t=' + _t);
        }
        alert("停留时间过长,请刷新后扫码登录!");
        window.location.reload();
        return false;
    }else{
     
        $.ajax({
     
            type: 'get',
            url: '/cgi-oauth/wechat/login-ajax?oper=login_ticket&t=' + _t,
            dataType: 'json',
            async: false,
            success: function (result) {
     
                if (result.code == 0) {
     
                    var data = result.data;
                    _l_g_s = data.loginStr;
                    _l_t_count++;
                    _l_ticket = data.ticket;
                    var img_url = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' + _l_ticket +'&t=' + _t;
                    $("#login_mpweixin_img").attr('src', img_url);
                    _c_t_i = setTimeout("login_core_expire();", 180000);
                    _v_t_i = setTimeout("login_verify();", 2000);
                } else {
     
                    $("#login_mpweixin_img").attr('src', '/static/_files/img/qrcode_invalid.jpg');
                }
            },
            error: function () {
     
                $("#login_mpweixin_img").attr('src', '/static/_files/img/qrcode_invalid.jpg');
            }
        });
    }
}
function login_core_expire() {
     
    if (_v_t_i) {
     
        clearTimeout(_v_t_i);
    }
    login_qrcode_get();
}
function login_core_success() {
     
    if (_v_t_i) {
     
        clearTimeout(_v_t_i);
    }
    if (_c_t_i) {
     
        clearTimeout(_c_t_i);
    }
    location.reload();
}
function login_verify() {
     
    if ($("#login_mpweixin_img").is(':hidden')) {
     
        _v_t_i = setTimeout("login_verify();", 2000);
        return;
    }
    var _t = new Date().getTime();
    $.ajax({
     
        type: 'get',
        url: '/cgi-oauth/wechat/login-ajax?oper=login_verify&t=' + _t + '&login_s=' + _l_g_s,
        dataType: 'json',
        async: false,
        success: function (data) {
     
            if (data.code == 0) {
     
                login_core_success();
            } else {
     
                _v_t_i = setTimeout("login_verify();", 2000);
            }
        },
        error: function () {
     
            _v_t_i = setTimeout("login_verify();", 2000);
        }
    });
}

你可能感兴趣的:(微信公众号,java)