一、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./cgi-bin/token?grant_type=client_credential&appid={?}&secret={?}";/*** 通过code换取网页授权access_token和openid*/public static final String OPENID_URL = "https://api./sns/oauth2/access_token?appid={?}&secret={?}&code={?}&grant_type=authorization_code";/*** 获取 jsapi_ticket 的微信URL地址*/public static final String JS_API_TICKET_URL = "https://api./cgi-bin/ticket/getticket?access_token={?}&type=jsapi";/*** 获取用户信息地址*/public static final String WX_USER_INFO_URL = "https://api./cgi-bin/user/info?access_token={?}&openid={?}&lang=zh_CN";/*** 发送微信消息地址*/public static final String SEND_MESSAGE_URL = "https://api./cgi-bin/message/custom/send?access_token={?}";/*** 带参二维码地址*/public static final String QRCODE_URL = "https://api./cgi-bin/qrcode/create?access_token={?}";/*** 小程二维码地址*/public static final String XCX_QRCODE_URL = "https://api./wxa/getwxacodeunlimit?access_token={?}";/*** 模板消息发送*/public static final String SEND_TEMPLATE_URL = "https://api./cgi-bin/message/template/send?access_token={?}";public static final String OAUTH_URL = "https://open./connect/oauth2/authorize?appid=";public static final String MATERIAL_URL = "https://api./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方法,添加事件消息类型的处理:
@Transactionalpublic 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_"前缀,我们需要将它截取掉:
@Transactionalpublic 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 {// 搜索公众号关注 TODOresult = 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代码在同一个项目中
@Transactionalprivate 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()){//会员登录消息 TODOwechatMsg = wechatMsgService.findByCode(WechatMsgTypes.QRCODE_LOGIN_VIP);}else{//非会员登录消息 TODOwechatMsg = 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("扫码登录失败");}}@Transactionalprivate 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")@ResponseBodypublic 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*/@Overridepublic 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*/@Overridepublic 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./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./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);}});}
如果觉得《微信公众号开发(4)-实现PC扫码登录》对你有帮助,请点赞、收藏,并留下你的观点哦!