网站通过手机微信扫码关注直接登录系统--java版

一、场景

1、有自己的一个网站;

2、点击登录按钮后,弹出公司公众号二维码;

3、用户扫码后,若之前没有关注公众号,需要点击关注公众号,然后直接登录系统;

4、若用户扫码后,之前已经关注过公众号,则直接登录系统。

二、实现逻辑

网站通过手机微信扫码关注直接登录系统--java版_第1张图片
 

1、用户请求登录,服务器通过微信接口,首先调用可用的接口access_token;

2、服务器拿到微信的接口调用凭据access_token后,再调用微信接口凭凭据去获得带参数的二维码的ticket,即买票。

3、服务器凭票,再次请求微信服务器,获得带参场景二维码,将之直接返回给前端,这里直接就是一个图片,前端不需要做生成二维码处理。

4、前端将二维码展示给用户,用户进行扫码关注或者直接进入公众号。

5、在用户进行关注或者直接进入公众号的时候,微信会将用户的操作事件推送给之前配置好的服务器回调函数(这是重点,会在后面详细说明)。

6、服务器的回调函数在被微信回调后,根据自己的业务逻辑,通过之前二维码中的参数进行唯一定位,进行业务逻辑处理。

7、在完成第3步之后,服务器前端就会进行轮询,每3秒访问一次服务器,查询回调函数是否被微信回调了,即,用户扫码后,事件有无被触发,若触发了,根据事件状态,进行业务操作,并停止轮询(当然,为了避免服务器不断被访问,轮询这里定义了最长轮询事件90秒)。

 三、代码前的准备工作

1、有自己企业的公众号,当然,前期为了不停的测试,可以申请微信测试账号

申请网址:微信测试公众号申请网址

网站通过手机微信扫码关注直接登录系统--java版_第2张图片

网站通过手机微信扫码关注直接登录系统--java版_第3张图片 

 这里有两点需要注意:

就是进行接口配置信息,上面是我已经配置好的截图,刚进入时,需要自己进行这两项的配置。

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版_第4张图片

上面的代码也是基于这个安全模式加密搞的

加密部分的代码是基于微信官网文档上稍微封装的:

微信消息加密源码下载地址

网站通过手机微信扫码关注直接登录系统--java版_第5张图片

网站通过手机微信扫码关注直接登录系统--java版_第6张图片

将其中的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

网站通过手机微信扫码关注直接登录系统--java版_第7张图片

 返回给微信的格式:

https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html

网站通过手机微信扫码关注直接登录系统--java版_第8张图片

 

填个坑:在回复微信消息时,里面的fromUser是指的开发者(即公司公众号)的微信号,而不是appid,这个和微信推给我们的是用户的appid还不一样。

网站通过手机微信扫码关注直接登录系统--java版_第9张图片

 后端代码基本上就是这些了,剩下的就是前段的登录轮询,这里就不说了,主要是个轮询:

/**
 * 登录注册入口
 */
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是微信号,这点要谨记。

 

 

你可能感兴趣的:(微信,openid)