java实现公众号扫码登录和扫码关注(详细)

目录

一、获取二维码

1.准备appid和appsecret

2.生成二维码

获取公众号二维码代码:

结果

二、Ngrok内网穿透

1.注册

2.登录

3.开通隧道

4.添加隧道

5.下载客户端

6.启动

三、公众号回调

1.公众号回调接口代码

2.公众号接口配置

四、登录


前提:

准备一个测试公众号appID和appsecret或者自己的公众号都行,服务器域名地址(没有的话用ngrok,下面会讲)。
思路:

先获取token,利用token获取ticket,公众号二维码,在公众号中启用服务器配置,配置完之后要写回调接口给微信公众号调用,这样才能获取里面的openid,前端轮询登录,后端根据openid结合自己实际的需求进行登录。总的来说步骤:扫码---回调---轮询---登录

一、获取二维码

1.准备appid和appsecret

微信公众平台测试号链接

2.生成二维码

微信开放文档生成二维码链接

根据官方文档说明的方法,自己可以进去看,大概意思是有两种二维码形式,利用token和其他参数去取ticket,然后再利用ticket取获取二维码图片。

获取公众号二维码代码:

我习惯写在yml文件,看个人习惯。

application-dev.yml

#公众号
oauth2:
  wechat:
    app-id: 自己的appid
    app-secret: 自己的sectet
    access-token-url: https://api.weixin.qq.com/cgi-bin/token
    gzh-qrcode-create-url: https://api.weixin.qq.com/cgi-bin/qrcode/create
    gzh-qrcode-url: https://mp.weixin.qq.com/cgi-bin/showqrcode
WechatController
@RestController
@RequestMapping("wx")
@Log4j2
@Api(tags = "微信端相关登录")
public class WechatController {
    private final IMpaTUserWechatService mpaTUserWechatService;

    public WechatController(MpaTUserWechatServiceImpl mpaTUserWechatService) {
        this.mpaTUserWechatService = mpaTUserWechatService;
    }

    @ApiOperation("获取公众号二维码")
    @GetMapping("getGzhQrcode")
    public WebResult<?> getGzhQrcode() {
        String uuid = EncryptUtils.UUIDLowerCase(true);
        return new WebResult<>(mpaTUserWechatService.getGzhQrcode(uuid));
    }
}
IMpaTUserWechatService
public interface IMpaTUserWechatService extends IService<MpaTUserWechat> {

    String getGzhAccessToken();
    
    Map<String, Object> getGzhQrcodeCreate(String uuid);

    Map<String, Object> getGzhQrcode(String uuid);

}
MpaTUserWechatServiceImpl

    @Value("${oauth2.wechat.access-token-url}")
    private String ACCESS_TOKEN_URL;
    @Value("${oauth2.wechat.gzh-qrcode-create-url}")
    private String gzhQrcodeCreate;
    @Value("${oauth2.wechat.gzh-qrcode-url}")
    private String gzhQrcode;

    @Override
    public String getGzhAccessToken() {
        // 从redis中取
        final String redisKey = "cache:gzhAccessToken";
        String gzhAccessToken = stringRedisTemplate.opsForValue().get(redisKey);
        if (StringUtils.hasText(gzhAccessToken)) {
            return gzhAccessToken;
        }
        // 请求wx
        URI uri = UriComponentsBuilder.fromHttpUrl(ACCESS_TOKEN_URL)
                .queryParam("appid", gzhAppid).queryParam("secret", gzhAppSecret)
                .queryParam("grant_type", "client_credential")
                .build()
                .toUri();
        ResponseEntity<String> entity = restTemplate.getForEntity(uri, String.class);
        if (entity.getStatusCode().is2xxSuccessful()) {
            String body = entity.getBody();
            Map<String, Object> map;
            try {
                map = objectMapper.readValue(body, new TypeReference<Map<String, Object>>() {
                });
            } catch (IOException e) {
                throw new BusinessException("序列化微信返回值失败:" + body);
            }
            if (map.containsKey("errcode")) {
                throw new BusinessException("获取AccessToken失败:" + map.get("errcode") + " " + map.get("errmsg"));
            }
            // 缓存到redis
            String access_token = (String) map.get("access_token");
            stringRedisTemplate.opsForValue().set(redisKey, access_token, Duration.ofSeconds((int) map.get("expires_in") - 60));
            return access_token;
        } else {
            throw new BusinessException("请求公众号API失败:" + entity.getStatusCodeValue());
        }
    }

    @Override
    public Map<String, Object> getGzhQrcodeCreate(String uuid) {
        String access_token = getGzhAccessToken();
        URI uri = UriComponentsBuilder.fromHttpUrl(gzhQrcodeCreate)
                .queryParam("access_token", access_token)
                .build()
                .toUri();
        Map<String, String> sceneMap = new HashMap<>();
        Map<String, Map<String, String>> actionInfo = new HashMap<>();
        sceneMap.put("scene_str", uuid);
        actionInfo.put("scene", sceneMap);
        Map<String, Object> body = new HashMap<>();
        body.put("action_info", actionInfo);
        body.put("expire_seconds", expireTime);
        body.put("action_name", "QR_SCENE");

        RequestEntity<Map<String, Object>> requestEntity = RequestEntity.post(uri).body(body);
        ResponseEntity<String> gzhResponse = restTemplate.exchange(requestEntity, String.class);
        if (gzhResponse.getBody() != null) {
            Map<String, Object> resultMap;
            try {
                resultMap = objectMapper.readValue(gzhResponse.getBody(), new TypeReference<Map<String, Object>>() {
                });
            } catch (IOException e) {
                throw new BusinessException("序列化微信返回值失败:" + gzhResponse.getBody());
            }
            if (resultMap.containsKey("ticket") && resultMap.containsKey("url")) {
                return resultMap;
            } else {
                // 处理错误情况
                throw new BusinessException("登录凭证校验失败:" + resultMap.get("errcode") + " " + resultMap.get("errmsg"));
            }
        } else {
            throw new BusinessException("请求微信API失败:" + gzhResponse.getStatusCodeValue());
        }
    }

    @Override
    public Map<String, Object> getGzhQrcode(String uuid) {
        Map<String, Object> qrcodeCreateMap = getGzhQrcodeCreate(uuid);
        String ticket = (String) qrcodeCreateMap.get("ticket");
        Integer expireSeconds = (Integer) qrcodeCreateMap.get("expire_seconds");
        String url = (String) qrcodeCreateMap.get("url");
        try {
            ticket = URLEncoder.encode(ticket, "utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        URI uri = UriComponentsBuilder.fromHttpUrl(gzhQrcode).queryParam("ticket", ticket).build().toUri();
        ResponseEntity<Resource> entity = restTemplate.getForEntity(uri, Resource.class);
        if (entity.getStatusCode().is2xxSuccessful() && entity.getBody() != null) {
            try (InputStream inputStream = entity.getBody().getInputStream()) {
                byte[] imageBytes = StreamUtils.copyToByteArray(inputStream);
                stringRedisTemplate.opsForValue().set(RedisEnum.GZH_EWM_PREFIX + ticket, ticket, Duration.ofSeconds(expireSeconds - 10));
                Map<String, Object> resultMap = new HashMap<>();
                resultMap.put("ticket", ticket);
                resultMap.put("img", java.util.Base64.getEncoder().encodeToString(imageBytes));
                return resultMap;

            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
        return null;
    }

结果

返回两个参数,一个是img后端经过base64处理,前端也需要转码出来,一个是ticket,这个ticket很重要,轮询回调要用到的。!!!因为我是放在redis中,前端每次轮询我都会从redis中比较。而且有时间限制,我用的是临时二维码,有效期设定的是5分钟,大家也可以设置永久,还是那句话看个人习惯。

也可以不进行base64,直接返回结果,会出现二维码图片。

好了,整个获取二维码部分结束。下面就是关键的地方了,当时也困扰了好久。

二、Ngrok内网穿透

我们需要通过Ngrok来获取一个服务器域名,来转接给本地。因为公众号需要设置这些东西,必须有,如果有自己的服务器那这一部直接跳过,更换自己的服务器地址就行。

1.注册

Sunny-Ngrok内网转发内网穿透 - 国内内网映射服务器链接

2.登录

登录后的界面,我记得好像是这边首页需要你认证,然后才可以使用,花2个大洋。

3.开通隧道

认证成功后开通隧道,本着白嫖的心思,当然要选择免费啦。

4.添加隧道

我这边本地端口是19900

5.下载客户端

下载完成后解压,进入有两个文件的地方

6.启动

cmd窗口打开后输入指令

sunny.exe clientid 隧道id

就启动成功了

它这个有时候相应很慢,但不要着急,重新执行指令就行了,接下来用postman测试下,把自己本地地址换成它提供的域名,比如http://127.0.0.1:19900/xxx/xxx//xxx变成->它提供的域名地址+/xxx/xxx。如果相应成功,就说明这个内网穿透配置成功了。

三、公众号回调

1.公众号回调接口代码

WechatController

在之前的Controller继续添加,此接口是给公众号自己调用的,下面有配置。主要功能是拿取扫码人的openid。

    @GetMapping(value = "/receive")
    @ApiOperation("接收微信消息事件,判断用户是否完成扫码关注")
    public String getGzhLoginReceive(HttpServletRequest request) throws IOException {
        log.info("微信回调接口开始执行get请求/weixin/receive");
        // 获取微信请求参数
        String signature = request.getParameter("signature");
        String timestamp = request.getParameter("timestamp");
        String nonce = request.getParameter("nonce");
        String echostr = request.getParameter("echostr");
        log.info("开始校验此次消息是否来自微信服务器,param->signature:{},\ntimestamp:{},\nnonce:{},\nechostr:{}",
                signature, timestamp, nonce, echostr);
        String result = mpaTUserWechatService.receive(signature, timestamp, nonce, echostr, request);
        System.out.println(result);
        log.info("微信回调接口get请求执行结束!");
        return result;
    }

    @PostMapping(value = "/receive")
    @ApiOperation("接收微信消息事件,判断用户是否完成扫码关注")
    public String postGzhLoginReceive(HttpServletRequest request) throws IOException {
        log.info("微信回调接口开始执行post请求/weixin/receive");
        // 获取微信请求参数
        String signature = request.getParameter("signature");
        String timestamp = request.getParameter("timestamp");
        String nonce = request.getParameter("nonce");
        String echostr = request.getParameter("echostr");
        log.info("开始校验此次消息是否来自微信服务器,param->signature:{},\ntimestamp:{},\nnonce:{},\nechostr:{}",
                signature, timestamp, nonce, echostr);
        String result = mpaTUserWechatService.receive(signature, timestamp, nonce, echostr, request);
        System.out.println(result);
        log.info("微信回调接口post请求执行结束!");
        return result;
    }
IMpaTUserWechatService
    String receive(String signature, String timestamp, String nonce, String echostr, HttpServletRequest request);
MpaTUserWechatServiceImpl

这一块很重要,我是利用redis记录openid的,和是否扫码或者关注。方便后面登录接口轮询作比较。

    @Override
    public String receive(String signature, String timestamp, String nonce, String echostr, HttpServletRequest request) {
        log.info("微信回调方法开始执行");
        String token = "xxxxx自己随便设置,但是要和公众号的token一样";
        // 验证微信签名
        if (!WxMessageUtil.checkSignature(signature, timestamp, nonce, token)) {
            throw new BusinessException("微信回调参数异常!");
        }
        // 验证服务端配置
        if (echostr != null) {
            return echostr;
        }
        // 接收微信推送的消息
        String xmlString = null;
        try {
            xmlString = WxMessageUtil.readRequest(request);
        } catch (Exception e) {
            throw new BusinessException("回调接收消息失败");
        }
        try {
            Map<String, String> resXml = WxMessageUtil.ResponseXmlToMap(xmlString);
            String ticket = resXml.get("Ticket"); // 获取二维码凭证
            String gzhOpenid = resXml.get("FromUserName"); // 获取OpenId
            String eventType = resXml.get("Event"); // 获取事件类型
            String msgType = resXml.get("MsgType");
            stringRedisTemplate.delete(RedisEnum.GZH_EWM_PREFIX + ticket);
            stringRedisTemplate.opsForValue().set(RedisEnum.GZH_EWM_PREFIX + ticket, gzhOpenid, Duration.ofSeconds(280));
            if ("subscribe".equals(eventType)) { // 如果是订阅消息
                String subscribeContent = "感谢关注";
                stringRedisTemplate.opsForValue().set(RedisEnum.GZH_GUAN_ZHU + ticket, "0", Duration.ofSeconds(280));
                return WxMessageUtil.getWxReturnMsg(resXml, subscribeContent);
            }
            if ("SCAN".equals(eventType)) { // 如果是扫码消息
                String scanContent = "扫码成功";
                stringRedisTemplate.opsForValue().set(RedisEnum.GZH_SAO_MA + ticket, "1", Duration.ofSeconds(280));
                return WxMessageUtil.getWxReturnMsg(resXml, scanContent);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("微信回调方法执行结束!");
        return "";
    }
WxMessageUtil工具类
/**
 * 微信消息处理类(微信消息交互大部分就是xml格式交互)
 */
@Slf4j
public class WxMessageUtil {
    /*
     * xml转map
     */
    public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
        HashMap<String, String> map = new HashMap<String, String>();
        SAXReader reader = new SAXReader();

        InputStream ins = request.getInputStream();
        Document doc = reader.read(ins);

        Element root = doc.getRootElement();
        @SuppressWarnings("unchecked")
        List<Element> list = (List<Element>) root.elements();

        for (Element e : list) {
            map.put(e.getName(), e.getText());
        }
        ins.close();
        return map;
    }

    /**
     * 获取公众号回复信息(xml格式)
     */
    public static String getWxReturnMsg(Map<String, String> decryptMap, String content) throws UnsupportedEncodingException {
        log.info("---开始封装xml---decryptMap:" + decryptMap.toString());
        TextMessage textMessage = new TextMessage();
        textMessage.setToUserName(decryptMap.get("FromUserName"));
        textMessage.setFromUserName(decryptMap.get("ToUserName"));
        textMessage.setCreateTime(System.currentTimeMillis());
        textMessage.setMsgType("text"); // 设置回复消息类型
        textMessage.setContent(content); // 设置回复内容
        String xmlMsg = getXmlString(textMessage);
        // 设置返回信息编码,防止中文乱码
        String encodeXmlMsg = new String(xmlMsg.getBytes(), "UTF-8");
        return encodeXmlMsg;
    }

    /**
     * 设置回复消息xml格式
     */
    private static String getXmlString(TextMessage textMessage) {
        String xml = "";
        if (textMessage != null) {
            xml = "<xml>";
            xml += "<ToUserName><![CDATA[";
            xml += textMessage.getToUserName();
            xml += "]]></ToUserName>";
            xml += "<FromUserName><![CDATA[";
            xml += textMessage.getFromUserName();
            xml += "]]></FromUserName>";
            xml += "<CreateTime>";
            xml += textMessage.getCreateTime();
            xml += "</CreateTime>";
            xml += "<MsgType><![CDATA[";
            xml += textMessage.getMsgType();
            xml += "]]></MsgType>";
            xml += "<Content><![CDATA[";
            xml += textMessage.getContent();
            xml += "]]></Content>";
            xml += "</xml>";
        }
        log.info("xml封装结果=>" + xml);
        return xml;
    }

    /**
     * 读取 Request Body 内容作为字符串
     *
     * @param request HttpServletRequest
     * @return XmlString
     * @throws IOException XmlIO
     */
    public static String readRequest(HttpServletRequest request) throws IOException {
        InputStream inputStream;
        StringBuffer sb = new StringBuffer();
        inputStream = request.getInputStream();
        String str;
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        while ((str = in.readLine()) != null) {
            sb.append(str);
        }
        in.close();
        inputStream.close();
        return sb.toString();
    }

    /**
     * 将微信获取的XML结果转为Map
     *
     * @param xmlString xml
     * @return Map
     * @throws DocumentException DocumentException
     */
    public static Map<String, String> ResponseXmlToMap(String xmlString) throws DocumentException {
        // 解析 XML 字符串为 Document 对象
        Document document = DocumentHelper.parseText(xmlString);
        // 获取根元素
        Element rootElement = document.getRootElement();
        // 获取子元素
        List<Element> nodes = rootElement.elements();
        // 获取子元素的文本内容
        Map<String, String> resultMap = new HashMap<>();
        for (Node node : nodes) {
            Element element = (Element) node;
            String nodeName = element.getName();
            String nodeText = element.getTextTrim();
            resultMap.put(nodeName, nodeText);
        }
        if (resultMap.containsKey("errcode")) {
            throw new BusinessException("系统异常");
        }
        return resultMap;
    }

    /**
     * 验证微信签名,确保接收到的消息来自微信官方
     */
    public static boolean checkSignature(String signature, String timestamp, String nonce, String token) {
        // 1.将token、timestamp、nonce三个参数进行字典序排序
        String[] arr = new String[]{token, timestamp, nonce};
        Arrays.sort(arr);
        // 2. 将三个参数字符串拼接成一个字符串进行sha1加密
        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;
        // 3.将sha1加密后的字符串可与signature对比,标识该请求来源于微信
        return tmpStr != null && tmpStr.equalsIgnoreCase(signature.toUpperCase());
    }

    private static String byteToStr(byte[] byteArray) {
        StringBuilder strDigest = new StringBuilder();
        for (int i = 0; i < byteArray.length; i++) {
            strDigest.append(byteToHexStr(byteArray[i]));
        }
        return strDigest.toString();
    }

    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;
    }

}

2.公众号接口配置

URL是Ngrok的域名+你自己接口配置的路径

Token和上面代码中token要一样,随便写,无所谓。上面代码中有提示。

URL提交后会自动调用上面写的receive接口。你可以断点看看信息。

四、登录

上面所有条件都符合后,进行登录操作

WechatController

    @PostMapping("/gzhLogin")
    @ApiOperation("公众号登录,前端进行轮询")
    public WebResult<?> checkLogin(@RequestParam String ticket, HttpServletRequest request) {
        log.info("前端二维码轮询接口开始执行/weixin/check");
        Map<String, Object> resultMap = mpaTUserWechatService.checkLogin(ticket, request);
        log.info("前端二维码轮询接口执行结束!");
        return new WebResult<>(resultMap);
    }

IMpaTUserWechatService

    Map<String, Object> checkLogin(String ticket,HttpServletRequest request);

MpaTUserWechatServiceImpl

这边根据自己登录的实际逻辑进行修改,登录前先从redis中取相关数据判断,本来是想发送模板消息的,目前项目中没有需要,就没加,代码注释了。

    @Override
    public Map<String, Object> checkLogin(String ticket, HttpServletRequest request) {
        String gzhOpenid = stringRedisTemplate.opsForValue().get(RedisEnum.GZH_EWM_PREFIX + ticket);
        String guanzhu = stringRedisTemplate.opsForValue().get(RedisEnum.GZH_GUAN_ZHU + ticket); //0
        String saoMa = stringRedisTemplate.opsForValue().get(RedisEnum.GZH_SAO_MA + ticket); //1
        if (!StringUtils.hasText(saoMa) && !StringUtils.hasText(guanzhu)) {
            return null;
        }
        if (gzhOpenid == null) {
            return null;
        }
        MpaTUserWechat userWechat = lambdaQuery()
                .eq(MpaTUserWechat::getGzhopenid, gzhOpenid)
                .one();
        MpaTSysUser sysUser = null;
        if (userWechat != null) {
            sysUser = iMpaTSysUserService.lambdaQuery()
                    .eq(MpaTSysUser::getUsercode, userWechat.getUsercode())
                    .one();
            if (sysUser != null) {
                //登录成功
                SaTokenInfo saTokenInfo;
                UserDTO userDTO = authService.loadUserByUsername(sysUser.getLoginname());
                //  记录登录成功日志
                authService.saveLoginSuccessLog(userDTO, request);
                //发送模板消息
                /*Map<String, Object> data = new HashMap<>();
                data.put("first", Collections.singletonMap("value", "成功"));
                data.put("character_string8", Collections.singletonMap("value", userDTO.getLoginName()));
                sendTemplateMsg(sysUser.getUsercode(), "JqJTZQpeQSGqLaaRDPQ_B4EK3u1Vce7dUvRC9V5r1FQ", data);*/

                // 一行代码实现登录
                StpUtil.login(userDTO.getUserCode(), SaLoginConfig.setExtra("sub", userDTO.getUserCode())
                        .setExtra("usercode", userDTO.getUserCode())
                        .setExtra("username", userDTO.getUsername())
                        .setExtra("nickname", userDTO.getLoginName())
                        .setExtra("userfrom", "MPA")
                        .setExtra("loginname", userDTO.getLoginName())
                        .setExtra("companycode", userDTO.getCompanycode())
                        .setExtra("rolecodes", userDTO.getRoleList())
                        .setExtra("inOut", userDTO.getInOut() == null ? null : userDTO.getInOut())
                        // todo 时间控制
                        .setExtra("exp", LocalDateTime.now().plusHours(6).toEpochSecond(ZoneOffset.ofHours(8))).setExtra("issue", "dpspapi").setExtra("two_factor", "N").setExtra("is_inner_admin", "Y"));
                // 将用户信息存储到Session中
                StpUtil.getSession().set("userInfo", userDTO);
                // 获取当前登录用户Token信息
                saTokenInfo = StpUtil.getTokenInfo();
                Map<String, Object> tokenMap = new HashMap<>();
                tokenMap.put("token", saTokenInfo.getTokenValue());
                stringRedisTemplate.delete(RedisEnum.GZH_EWM_PREFIX + ticket);
                stringRedisTemplate.delete(RedisEnum.GZH_GUAN_ZHU + ticket);
                stringRedisTemplate.delete(RedisEnum.GZH_SAO_MA + ticket);
                return tokenMap;
            } else {
                authService.saveLoginFailLog(null, null, null, "该用户不存在", request);
                throw new BusinessException("该用户不存在");
            }
        } else {
            authService.saveLoginFailLog(null, null, null, "该用户未绑定公众号", request);
            throw new BusinessException(-1005, "该用户未绑定,请绑定公众号==>多联平台->账号绑定");
        }
    }

到此,整个后端代码流程结束。可以自己测试以下,只要能拿到openid就万事大吉了。

剩下的就交给前端调用了。

完结(如果对你有帮助,别忘了点赞哦)

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值