一、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);
}
});
}