1、有自己的一个网站;
2、点击登录按钮后,弹出公司公众号二维码;
3、用户扫码后,若之前没有关注公众号,需要点击关注公众号,然后直接登录系统;
4、若用户扫码后,之前已经关注过公众号,则直接登录系统。
1、用户请求登录,服务器通过微信接口,首先调用可用的接口access_token;
2、服务器拿到微信的接口调用凭据access_token后,再调用微信接口凭凭据去获得带参数的二维码的ticket,即买票。
3、服务器凭票,再次请求微信服务器,获得带参场景二维码,将之直接返回给前端,这里直接就是一个图片,前端不需要做生成二维码处理。
4、前端将二维码展示给用户,用户进行扫码关注或者直接进入公众号。
5、在用户进行关注或者直接进入公众号的时候,微信会将用户的操作事件推送给之前配置好的服务器回调函数(这是重点,会在后面详细说明)。
6、服务器的回调函数在被微信回调后,根据自己的业务逻辑,通过之前二维码中的参数进行唯一定位,进行业务逻辑处理。
7、在完成第3步之后,服务器前端就会进行轮询,每3秒访问一次服务器,查询回调函数是否被微信回调了,即,用户扫码后,事件有无被触发,若触发了,根据事件状态,进行业务操作,并停止轮询(当然,为了避免服务器不断被访问,轮询这里定义了最长轮询事件90秒)。
1、有自己企业的公众号,当然,前期为了不停的测试,可以申请微信测试账号
申请网址:微信测试公众号申请网址
这里有两点需要注意:
就是进行接口配置信息,上面是我已经配置好的截图,刚进入时,需要自己进行这两项的配置。
1、URL:这个需要有自己的外网服务器,就是这里定义的是回调函数,那么微信服务器要能通过外网访问到你的这个回调接口,(这里我心中也一直有疑惑,因为我们一直使用内网机子开发,没有部署的代码在自己本地,微信服务器是访问不到的,这就导致了在接口调试的过程中很痛苦,一直没看到微信有没有一个好的测试接口。。。)
2、Token:这是加密用的,这里你可以自己随便定义,但是在服务器配置进行服务器接口认证及消息传递时,都会带这个,所以,必须和自己服务器端的后端代码中的token要一致。
3、很关键的一点,这里把两个参数配置好之后,有个提交按钮进行提交,提交的过程中,微信是要到你的服务器上进行认证的,所以,这个时候,你的回调接口就要写好了
(这里要特别注意:这里的回调接口有两个作用:a,在接口进行配置好参数提交时,微信需要到你的服务器上进行认证,就是通过这个接口的;b:后面的用户扫码进入公众号或关注公众号,也是通过这个接口进行认证的)。所以这个接口既是服务器配置接口认证接口,也是微信事件推送接口,我在这踩了半天坑,网上很少有人提到。
这里把这个后台的回调函数实现放这里:
/**
* 微信公众号平台接口服务器回调token验证
*
* @param request
* @return
*/
@RequestMapping("/verify_wx_token")
@ResponseBody
public String verifyWXToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("call verify_wx_token success!!!!!!");
// 微信加密签名
String signature = request.getParameter("signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echostr = request.getParameter("echostr");
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (signature != null && echostr != null && CheckoutUtils.checkSignature(signature, timestamp, nonce)) {
//服务器接口提交时认证,一般就调用一次
return echostr;
//WeiXinUtils.responsePrint(response, echostr);
} else {
//用户进行关注、取关等事件时的处理逻辑,根据事件类型,与自己的业务逻辑进行挂钩
return callBackFromEventEncry(request);//这里是你自己的业务实现了,后面会贴出来
}
}
用到的一个验证工具类:
package com.datahui.excelhui.common.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class CheckoutUtils {
// 与接口配置信息中的Token要一致
private static String token = "xxxx";
/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] { token, timestamp, nonce };
// 将token、timestamp、nonce三个参数进行字典序排序
// Arrays.sort(arr);
sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为十六进制字符串
*
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串
*
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
/**
* @param a
*/
public static void sort(String a[]) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[j].compareTo(a[i]) < 0) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
}
至此,准备工作都差不多了。
首先:我的后端是基于springboot架子
配置常量 application.yml中增加如下配置
#微信公众号相关配置
weixin:
app:
#公司公众号id或测试公众号id
app_id: wx3xxxxxxxxxxx50b
#公司公众号秘钥或测试秘钥
app_secret: xxxxxxxxxxxxxxxxxxxxx
几个关键的后端接口:
核心的cotroller层 :WechatController.java
@Controller
@RequestMapping("/wechat")
public class WechatController {
@Value("${weixin.app.app_id}")
private String app_id;
@Value("${weixin.app.app_secret}")
private String app_secret;
@Autowired
private WxAccessTokenService wxAccessTokenService;
@Autowired
private UserInfoService userInfoService;
@Autowired
private WeChatService weChatService;
/**
* 生成带参数的二维码,扫描关注微信公众号,自动登录网站
*
* @return
* @throws Exception
*/
@Log("公众号二维码获取")
@RequestMapping(value = "/qrCodeUrl")
@ResponseBody
public ResultVO wechatMpLogin() throws Exception {
long start = new Date().getTime();
System.out.println(start);
long end = start + 1000 * 60;
String ss = DateUtils.format(new Date(end), "yyyy-MM-dd HH:mm:ss");
ResultVO resultVO = new ResultVO();
resultVO.setState(true);
String access_token = this.wxAccessTokenService.findAccessTokenStr();
String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + access_token;
String scene_str = "xxxx" + UUID.randomUUID().toString();
String params = "{\"expire_seconds\":1200, \"action_name\":\"QR_STR_SCENE\", \"action_info\":{\"scene\":{\"scene_str\":\"" + scene_str + "\"}}}";
Map resultMap = HttpUtils.doPost(url, params, 60000);
if (resultMap.get("ticket") != null) {
String qrcodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" + resultMap.get("ticket");
resultVO.setData(qrcodeUrl);
resultVO.setCode(scene_str);
//此处获得二维码之后,将scene_str放在map中,用户存放对应的用户微信id,方便后面查询使用
//在微信回调函数推送信息后,将用户的微信id存放在此map中对应的value中
WeiXinUtils.getUserOpenid().put(scene_str, "");
WeiXinUtils.getUserNickname().put(scene_str,"");
} else {
resultVO.setState(false);
}
return resultVO;
}
// 轮询调用,检测登录状态
@RequestMapping("/checkLogin")
@ResponseBody
public ResultVO wechatMpCheckLogin(HttpServletRequest request) {
String scene_str = request.getParameter("scene_str");
ResultVO resultVO = new ResultVO();
resultVO.setState(true);
String openId = WeiXinUtils.getUserOpenid().get(scene_str);
String nickName = WeiXinUtils.getUserNickname().get(scene_str);
String[] dataArray = {openId,nickName};
if (StringUtils.isNotBlank(openId)) {
resultVO.setCode("1");
resultVO.setData(dataArray);
} else {
resultVO.setCode("0");
}
return resultVO;
}
/**
* 微信公众号平台接口服务器回调token验证
*
* @param request
* @return
*/
@RequestMapping("/verify_wx_token")
@ResponseBody
public String verifyWXToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("call verify_wx_token success!!!!!!");
// 微信加密签名
String signature = request.getParameter("signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echostr = request.getParameter("echostr");
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (signature != null && echostr != null && CheckoutUtils.checkSignature(signature, timestamp, nonce)) {
//服务器接口提交时认证,一般就调用一次
return echostr;
//WeiXinUtils.responsePrint(response, echostr);
} else {
//用户进行关注、取关等事件时的处理逻辑,根据事件类型,与自己的业务逻辑进行挂钩
return callBackFromEventEncry(request);
}
}
/**
* 微信用户关注、进入、取消关注公众号时,微信服务器推送消息解读
* 消息经过加密,处理时需要进行解密;即消息的传递采用密文方式
* @param request
* @return
*/
public String callBackFromEventEncry (HttpServletRequest request) {
//加密消息处理
InputStream inputStream = null;
String encrypt_type =request.getParameter("encrypt_type");//加密类型
if (StringUtils.isBlank(encrypt_type)|| encrypt_type.equals("raw")) {//不用加密
// 正常的微信处理流程
} else {//需走加解密流程
//解密请求消息体
try {
inputStream = request.getInputStream();
// BufferedReader是包装设计模式,性能更高
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuffer xmlBuffer = new StringBuffer();
String line;
while ((line = bufferedReader.readLine()) != null) {
xmlBuffer.append(line);
}
bufferedReader.close();
inputStream.close();
String nXmlString = AuthProcess.decryptMsg(request, xmlBuffer.toString());//对xml内容进行解密
Map callbackMap = WeiXinUtils.xmlToMap(nXmlString); //xml转map
if (null != callbackMap) {
String fromUserName = callbackMap.get("FromUserName");//用户微信id
/**
* 1. 用户未关注时,进行关注后的事件推送的事件类型为 subscribe
* 2.用户已关注时的事件推送 事件类型为 SCAN
*/
String event = callbackMap.get("Event");//事件类型
String eventKey = callbackMap.get("EventKey");
String scene_str = "";
if (StringUtils.isNotBlank(event)) {
Map wechatUserInfoMap = null;
String nickname = "";//用户的昵称
try {
WeiXinUserVO weiXinUserVO = this.weChatService.findWeiXinUserFromWeiXin(fromUserName, null);
if (null != weiXinUserVO) {
nickname = weiXinUserVO.getNickname();//获得微信用户昵称
}
} catch (Exception e) {
e.printStackTrace();
}
if (event.equals("subscribe")) {
// 事件KEY值,qrscene_为前缀,后面为二维码的参数值;如qrscene_123
scene_str = eventKey.split("qrscene_")[1];//关注时的eventKey是qrscene_为前缀,后面为二维码的参数值
// System.out.println("this is subscribe>>>>>>>> the scene_str is " + scene_str);
//用户关注公众号
if (StringUtils.isNotBlank(scene_str)) {
//场景值存在
WeiXinUtils.getUserOpenid().put(scene_str, fromUserName);
WeiXinUtils.getUserNickname().put(scene_str,nickname);
}
} else if (event.equals("SCAN")) {
//用户之前已经关注过公众号,扫码后直接进入了公众号
scene_str = eventKey;//已关注时的eventKey就是二维码参数
// System.out.println("this is SCAN>>>>>>>> the scene_str is " + scene_str);
WeiXinUtils.getUserOpenid().put(scene_str, fromUserName);
WeiXinUtils.getUserNickname().put(scene_str,nickname);
} else if (event.equals("unsubscribe")) {
//用户取消订阅,不做任何处理
}
if(StringUtils.isNotBlank(scene_str) && WeiXinUtils.getUserOpenid().containsKey(scene_str)){
//针对扫码登录的自动回复
String replayContent = "Hi~~\n欢迎您,您已成功登录。\n";//回复消息内容
String timestamp = String.valueOf(System.currentTimeMillis()/1000);
String replyMsg = "" +
" " + //这里是发送给对方的openId
" " + //注意这里是开发者的微信号,不是app_id
""+Integer.valueOf(timestamp)+" " +
" " +
" " +
" ";
// System.out.println("into replyMsg=============="+replyMsg);
return AuthProcess.encryptMsg(request, replyMsg);//将消息加密后进行返回
}else{
return "success";
}
}
}
} catch (IOException ex) {
ex.printStackTrace();
return "";
} catch (Exception exception) {
exception.printStackTrace();
return "";
}
// result = AuthProcess.encryptMsg(request, originalResult);
}
//通知微信平台处理成功
return "success";
}
}
两个主要的service层的实现,这里只贴实现类了:
/**
* 微信接口Service实现层
*/
@Service
public class WeChatServiceImpl implements WeChatService {
@Autowired
private WxAccessTokenService wxAccessTokenService;
@Override
public WeiXinUserVO findWeiXinUserFromWeiXin(String openId, String accessToken) throws Exception {
if (StringUtils.isBlank(accessToken)) {
accessToken = this.wxAccessTokenService.findAccessTokenStr();
if (StringUtils.isBlank(accessToken)) {
return null;
}
}
String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=" + accessToken + "&openid=" + openId;
Map map = HttpUtils.doGet(url);
//===========================================================================//
if (null != map && map.size() > 0) {
if (null != map.get("errcode")) {
return null;
}else{
WeiXinUserVO weiXinUserVO = new WeiXinUserVO();
weiXinUserVO.setOpenid(map.get("openid") == null? null:map.get("openid").toString());
weiXinUserVO.setSubscribe(map.get("subscribe") == null? null:map.get("subscribe").toString());
weiXinUserVO.setNickname(map.get("nickname") == null? null:map.get("nickname").toString());
weiXinUserVO.setSex(map.get("sex") == null? null: map.get("sex").toString());
weiXinUserVO.setLanguage(map.get("language") == null? null:map.get("language").toString());
weiXinUserVO.setCity(map.get("city") == null? null:map.get("city").toString());
weiXinUserVO.setProvince(map.get("province") == null? null:map.get("province").toString());
weiXinUserVO.setCountry(map.get("country") == null? null:map.get("country").toString());
weiXinUserVO.setSubscribeTime(map.get("subscribe_time") == null? null:map.get("subscribe_time").toString());
weiXinUserVO.setRemark(map.get("remark") == null? null:map.get("remark").toString());
weiXinUserVO.setSubscribeScene(map.get("subscribe_scene") == null? null:map.get("subscribe_scene").toString());
weiXinUserVO.setQrScene(map.get("qr_scene") == null? null:map.get("qr_scene").toString());
weiXinUserVO.setQrSceneStr(map.get("qr_scene_str") == null? null:map.get("qr_scene_str").toString());
return weiXinUserVO;
}
}
return null;
}
}
@Service
public class WxAccessTokenServiceImpl implements WxAccessTokenService {
@Value("${weixin.app.app_id}")
private String app_id;
@Value("${weixin.app.app_secret}")
private String app_secret;
@Autowired
private WxAccessTokenRepository wxAccessTokenRepository;
@Override
public WxAccessToken saveOrUpdate(WxAccessToken wxAccessToken) throws Exception {
if(StringUtils.isBlank(wxAccessToken.getId())){
wxAccessToken.setId(UUID.randomUUID().toString());
}
if (null == wxAccessToken.getCreateTime()) {
wxAccessToken.setCreateTime(new Date().getTime());
}
return wxAccessTokenRepository.save(wxAccessToken);
}
@Override
public WxAccessToken findAccessToken() throws Exception{
// List wxAccessTokenList = wxAccessTokenRepository.findAll();
WxAccessToken currentToken = wxAccessTokenRepository.findByAppId(this.app_id);
if (null == currentToken ) {
//数据库中没有对应的凭据
//调用微信接口获得access_token 并存储
WxAccessToken newToken = new WxAccessToken();
String accessToken = this.getAccessToken();
if (StringUtils.isNotBlank(accessToken)) {
newToken.setAppId(this.app_id);
newToken.setAccessToken(accessToken);//更新token
newToken.setCreateTime(new Date().getTime());//更新创建时间
return this.saveOrUpdate(newToken);
}
}else{
//数据库中存在凭据,则需要查看凭据的有效时间
// WxAccessToken wxAccessToken = wxAccessTokenList.get(0);
if ((new Date().getTime() - currentToken.getCreateTime()) / (1000 * 60) > 90) {
//存储时间超过了90分钟,则需要重新从微信方获得新的凭据
String accessToken = this.getAccessToken();
if (StringUtils.isNotBlank(accessToken)) {
currentToken.setAccessToken(accessToken);//更新token
currentToken.setCreateTime(new Date().getTime());//更新创建时间
return this.saveOrUpdate(currentToken);
}
}else{
return currentToken;
}
}
return null;
}
@Override
public String findAccessTokenStr() throws Exception {
String accessToken = "";
WxAccessToken wxAccessToken = this.findAccessToken();
if (null != wxAccessToken) {
accessToken = wxAccessToken.getAccessToken();
}
return accessToken;
}
/**
* 调用微信公众号接口获得公众号接口调用凭据
* @return 返回凭据
* @throws Exception
*/
private String getAccessToken() throws Exception{
//调用微信接口获取access_token :access_token 的有效期是2个小时,不要重复调用,否则会将之前的老的刷新
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + app_id + "&secret=" + app_secret;
Map accessTokenMap = HttpUtils.doGet(url);
if(null != accessTokenMap.get("access_token")){
return accessTokenMap.get("access_token").toString();
}else{
return null;
}
}
}
用到的实体类:
import lombok.Data;
import java.io.Serializable;
/**
* 对应微信方定义的用户信息
*/
@Data
public class WeiXinUserVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户是否订阅该公众号标识,值为0时,代表此用户没有关注该公众号,拉取不到其余信息。
*/
private String subscribe;
/**
*用户的标识,对当前公众号唯一
*/
private String openid;
/**
*用户的昵称
*/
private String nickname;
/**
*用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
*/
private String sex;
/**
*用户的语言,简体中文为zh_CN
*/
private String language;
/**
*用户所在城市
*/
private String city;
/**
*用户所在省份
*/
private String province;
/**
*用户所在国家
*/
private String country;
/**
*用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
*/
private String headimgurl;
/**
*用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间
*/
private String subscribeTime;
/**
*公众号运营者对粉丝的备注,公众号运营者可在微信公众平台用户管理界面对粉丝添加备注
*/
private String remark;
/**
*返回用户关注的渠道来源,
* ADD_SCENE_SEARCH 公众号搜索,
* ADD_SCENE_ACCOUNT_MIGRATION 公众号迁移,
* ADD_SCENE_PROFILE_CARD 名片分享,
* ADD_SCENE_QR_CODE 扫描二维码,
* ADD_SCENE_PROFILE_LINK 图文页内名称点击,
* ADD_SCENE_PROFILE_ITEM 图文页右上角菜单,
* ADD_SCENE_PAID 支付后关注,
* ADD_SCENE_WECHAT_ADVERTISEMENT 微信广告,
* ADD_SCENE_OTHERS 其他
*/
private String subscribeScene;
/**
*二维码扫码场景(开发者自定义)
*/
private String qrScene;
/**
*二维码扫码场景描述(开发者自定义)
*/
private String qrSceneStr;
}
几个工具类:
/**
* 自定义微信API接口工具类
*/
public class WeiXinUtils {
/**
* 微信扫码状态记录
* key = scene_str;value = openId
*/
private static Map USER_OPENID = new HashMap<>();
/**
* 微信扫码用户昵称记录
* key = scene_str;value = nickname
*/
private static Map USER_NICKNAME = new HashMap<>();
public static Map getUserOpenid() {
return USER_OPENID;
}
public static void setUserOpenid(Map userOpenid) {
USER_OPENID = userOpenid;
}
public static Map getUserNickname() {
return USER_NICKNAME;
}
public static void setUserNickname(Map userNickname) {
USER_NICKNAME = userNickname;
}
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map xmlToMap(String strXML) throws Exception {
try {
Map data = new HashMap();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}
/**
* 返回信息给微信 商户已经接收到回调
*
* @param response
* @param content 内容
* @throws Exception
*/
public static void responsePrint(HttpServletResponse response, String content) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/xml");
response.getWriter().print(content);
response.getWriter().flush();
response.getWriter().close();
}
}
/**
* 封装http get post
*/
public class HttpUtils {
private static final Gson gson = new Gson();
/**
* get方法
* @param url
* @return
*/
public static Map doGet(String url){
Map map = new HashMap<>();
CloseableHttpClient httpClient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000) //连接超时
.setConnectionRequestTimeout(5000)//请求超时
.setSocketTimeout(5000)
.setRedirectsEnabled(true) //允许自动重定向
.build();
HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);
try{
HttpResponse httpResponse = httpClient.execute(httpGet);
if(httpResponse.getStatusLine().getStatusCode() == 200){
String jsonResult = EntityUtils.toString( httpResponse.getEntity());
map = gson.fromJson(jsonResult,map.getClass());
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return map;
}
/**
* 封装post
* @return
*/
public static Map doPost(String url, String data,int timeout){
CloseableHttpClient httpClient = HttpClients.createDefault();
//超时设置
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(timeout) //连接超时
.setConnectionRequestTimeout(timeout)//请求超时
.setSocketTimeout(timeout)
.setRedirectsEnabled(true) //允许自动重定向
.build();
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
httpPost.addHeader("Content-Type","text/html; chartset=UTF-8");
if(data != null && data instanceof String){ //使用字符串传参
StringEntity stringEntity = new StringEntity(data,"UTF-8");
httpPost.setEntity(stringEntity);
}
try{
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpResponse.getStatusLine().getStatusCode() == 200){
String result = EntityUtils.toString(httpEntity);
System.out.println("doPost result is :"+result);
Map map = gson.fromJson(result, Map.class);
return map;
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
httpClient.close();
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
这里要说明的是:
回调函数在测试公众号中是不用加密的,也就是说传输的是明文,而在正式的公众号中是经过加密的(公众号==》基本配置==》服务器配置)
上面的代码也是基于这个安全模式加密搞的
加密部分的代码是基于微信官网文档上稍微封装的:
微信消息加密源码下载地址
将其中的java部分源码放入自己的代码中,主要是参考网上的自己封装了AuthProcess.java类,提供加解密的接口
这块的原理其实就是,在微信回调我们自己的函数时,传递的数据是加密过的,然后我们处理过后,返回给微信也要按照他的格式进行加密返回,当然,你要是想用明文传递的话,就在服务器配置中选择明文或者兼容模式都可以
public class AuthProcess {
public final static String Token = "xxxxx";//公众平台上面自己填写的Token
public final static String EncodingAESKey = "xxxxxxxxxxxxxxxxxxxx";//公众平台上面自己填写的43位EncodingAESKey
public final static String AppID = "wxxxxxxxxx";//应用的appid(微信生成的)
/**
* 将加密后的原文进行解密重新封装
* @param request
* @param originalXml 原xml
* @return 重新解密后的xml
*/
public static String decryptMsg(HttpServletRequest request, String originalXml) {
// 微信加密签名
//String sVerifyMsgSig = request.getParameter("signature");
String msgSignature = request.getParameter("msg_signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(Token, EncodingAESKey, AppID);
return pc.decryptMsg(msgSignature, timestamp, nonce, originalXml);
} catch (AesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/**
* 对需要回复的原文进行加密重新封装
* @param request
* @param replyXml 需要回复的xml
* @return 重新加密后的xml
*/
public static String encryptMsg(HttpServletRequest request,String replyXml) {
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(Token, EncodingAESKey, AppID);
return pc.encryptMsg(replyXml, timestamp, nonce);
} catch (AesException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
这里要说明的是微信回调我们的函数后,我们必须要有响应,而且是按照他的格式给的,格式说明如下:
微信传递过来的格式:
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
返回给微信的格式:
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
填个坑:在回复微信消息时,里面的fromUser是指的开发者(即公司公众号)的微信号,而不是appid,这个和微信推给我们的是用户的appid还不一样。
后端代码基本上就是这些了,剩下的就是前段的登录轮询,这里就不说了,主要是个轮询:
/**
* 登录注册入口
*/
var _time;//轮询号
function loginAndRigister() {
$.ajax({
type: "get",
url: baseUrl() + "/wechat/qrCodeUrl",
contentType: false,
processData: false,
dataType: "json",
async: false,
success: function (data) {
$('#loginModal_winxin').modal('show');//弹出扫码界面
if (data && data.state) {
$("#wechat-qrcode").attr("src", data.data);//设置二维码
$("#scene_str_id").val(data.code);//将后端自定义的scene_str获得,保存到前段,方便传参调用
var invalNum = 0;//轮询次数
_time = setInterval(function() {
invalNum++;
console.log(invalNum);
//轮询,校验是否已扫码关注
$.ajax({
type: "get",
url: baseUrl() + "/wechat/checkLogin?scene_str=" + data.code,
contentType: false,
processData: false,
dataType: "json",
async: false,
success: function (data) {
if (data.state) {
if (data.code == "1") {
//已关注,返回用户微信id
clearInterval(_time);
var resData = data.data;
var openId = resData[0];
var nickName = resData[1];
$.ajax({
type: "post",
url: baseUrl() + "/userInfo/findUserByOpenId?openId=" + openId + "&nickName=" + nickName,
// contentType: false,
// processData: false,
dataType: "json",
async: false,
success: function (res) {
if(res.code == '1'){
if(res.state){
//用户存在,直接登录
}else {
//用户不存在,注册
}
}else{
toastr.error('查询用户失败!');
}
},
error: function (data) {
console.log("错误了")
console.log(data);
}
});
} else {
//未关注,不做任何处理
}
}
},
error: function (data) {
console.log("错误了")
console.log(data);
}
});
if (invalNum > 29) {
//设置最大轮询30次,即90秒
invalNum = 0;
$('#loginModal_winxin').modal('hide');//关闭扫码界面
}
}, 3000);
}else{
toastr.error("获取微信二维码失败,请刷新页面重试!");
}
},
error: function (data) {
toastr.error("系统出现错误,请刷新页面重试!");
}
});
}
1、回调函数的配置地方和服务器验证的配置是一个地方
2、在获取二维码的时候的凭证是2小时过期,这个自己拿到一次后,要自己存储起来,并在指定的时间重新获取。
3、服务器验证的回调函数的返回可以直接返回空串或者success;
4、带参二维码通过用户的微信id获得用户信息后,可在eventKey中获得每次扫码的参数,但是关注事件的eventKey是加了前缀的,这点要区别对待(上面代码中有说明);
5、回调函数中返回给微信的格式要正确,尤其是返回去的fromUserName是微信号,这点要谨记。