Bootstrap

微信小程序00: 获取accessToken,手机号, 二维码,openId与unionId 公共配置类(核心篇)


1.前言简介

本文是微信小程序公共类 以下功能均使用 此文章 代码

  • 获取accessToken
  • 手机号
  • 小程序二维码
  • openId与UnionId

最下面有传送门, 传送到每个功能 避免多次封装
远程调用均使用restTemplate (springboot自带, 操作简单)
使用其他请随意…

1.1 专栏传送门

===> 微信小程序相关操作专栏 <===

2. 微信小程序公用功能

2.1 配置准备工作

ps: 初次搭建小程序这步骤不要忘记了
在这里插入图片描述

2.1.1 配置文件准备(单体放yml中 微服务放配置中心)

(特别注意 这里面包含大部分微信小程序配置)
appidsecret 填写上
ps: 里面部分配置可以删除

  • 过期时间
  • 启动执行
  • 登录url
  • 其他业务 如获取手机号, 获取小程序二维码等
# 微信核心参数
wechat:
  # 小程序
  mini-app:
    # 请求url
    requestUrl: /wxMiniLogin
    # 请求方法
    requestMethod: POST
    # appid
    appId: ?
    # app秘钥
    appSecret: ?
    # 微信基础请求网址
    wxBaseRequestUrl: https://api.weixin.qq.com
    # 获取access_token必备参数
    grantType: client_credential
    # 微信获取access_token的url
    aTokenUrl: ${wechat.mini-app.wxBaseRequestUrl}/cgi-bin/token?grant_type=${wechat.mini-app.grantType}&appid=%s&secret=%s
    # 要分钟(尽量少于120分钟) 一天上限2000次 这里一天最多请求24次 可增加定时刷新缓存等
    expiredTime: 70
    # 是否执行服务启动执行
    serverStartAutoRun: false
    # 微信登录提供的接口(GET)
    wxLoginUrlTemplate: ${wechat.mini-app.wxBaseRequestUrl}/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code
    # 微信获取手机号接口
    wxGetPhoneUrl: ${wechat.mini-app.wxBaseRequestUrl}/wxa/business/getuserphonenumber?access_token=%s
    #微信获取二维码(不限制)post请求
    wxACodeUnLimitUrl: ${wechat.mini-app.wxBaseRequestUrl}/wxa/getwxacodeunlimit?access_token=%s
    #微信获取二维码(限制)post请求
    wxACodeUrl: ${wechat.mini-app.wxBaseRequestUrl}/wxa/getwxacode?access_token=%s
    # 微信获取二维码(限制)post请求
    wxAQrcodeUrl: ${wechat.mini-app.wxBaseRequestUrl}/cgi-bin/wxaapp/createwxaqrcode?access_token=%s

2.1.2 获取配置文件中的小程序配置

使用@ConfigurationProperties注解对应

代码如下, 名字对应配置文件名称, 注释自己加一下吧

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 小程序获取配置类
 *
 * @author pzy
 * @version 0.1.0
 * @description TODO
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "wechat.mini-app")
public class WechatConfigProperties {

    private String requestUrl;

    private String requestMethod;

    private String appId;

    private String appSecret;

    private String wxBaseRequestUrl;

    private String grantType;

    private String aTokenUrl;

    private Integer expiredTime;

    private Boolean serverStartAutoRun;

    private String wxLoginUrlTemplate;

    /**
     * 获取手机号
     */
    private String wxGetPhoneUrl;

    /**
     * 二维码无限制url(主)
     */
    private String wxACodeUnLimitUrl;

    private String wxACodeUrl;

    private String wxAQrcodeUrl;




    /**
     * 生成微信登录请求地址
     *
     * @param code code
     * @return 请求地址
     */
    public String getWxLoginUrl(String code) {
        return String.format(wxLoginUrlTemplate, appId, appSecret, code);
    }

    /**
     * 生成微信ACCESS_TOKEN请求地址
     * <p>
     * 模板样式: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s
     *
     * @return 请求地址
     */
    public String getATokenUrl() {
        return String.format(aTokenUrl, appId, appSecret);
    }


    /**
     * 获取手机号url post请求
     *
     * @param accessToken
     * @return
     */
    public String getPhoneUrl(String accessToken) {
        return String.format(wxGetPhoneUrl, accessToken);
    }

    /**
     * 获取不限制的微信二维码url
     *
     * @param accessToken
     * @return
     */
    public String getWxACodeUnLimitUrl(String accessToken) {
        return String.format(wxACodeUnLimitUrl, accessToken);
    }

    /**
     * 获取(限制一)的微信二维码url
     *
     * @param accessToken
     * @return
     */
    public String getWxACodeUrl(String accessToken) {
        return String.format(wxACodeUrl, accessToken);
    }

    /**
     * 获取(限制二)的微信二维码url
     *
     * @param accessToken
     * @return
     */
    public String getWxAQrcodeUrl(String accessToken) {
        return String.format(wxAQrcodeUrl, accessToken);
    }
}

2.1.3 设置redis配置

spring: 
  redis:
    # 地址
    host: 192.168.1.130
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password: 123456
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 5
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池的最大数据库连接数
        max-active: 50
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 5000ms

2.2 创建不同功能工具类

2.2.1 创建微信服务工具类WechatServiceUtils

RestTemplate 直接可以使用
WechatConfigProperties 请见 2.1.2
RedisCache 请见2.2.2

/**
 * 微信服务工具类
 *
 * @author pzy
 * @version 0.1.0
 * @description: TODO
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class WechatServiceUtils {

    /**
     * 远程调用
     */
    private final RestTemplate restTemplate;

    /**
     * redis缓存
     */
    private final RedisCache redisCache;

    /**
     * 微信统一配置
     */
    private final WechatConfigProperties wechatConfigProperties;

    /**
     * 1. 获取微信登录认证信息
     *
     * @param wxCommonReqDto
     * @return
     */
    public Map<String, String> getWxMiniAuth(WxCommonReqDto wxCommonReqDto) {
        String code = wxCommonReqDto.getWxCode();
        //秘钥
        String encryptedIv = wxCommonReqDto.getIv();
        //加密数据
        String encryptedData = wxCommonReqDto.getEncryptedData();

        JSONObject wxAuthResponse = null;
        String sessionKey = "";
        String openid = "";
        try {
            //1. 想微信服务器发送请求获取用户信息
            String url = wechatConfigProperties.getWxLoginUrl(code);
            log.info("===> 请求微信url是: {}", url);

            //2. 远程调用微信接口
            String res = restTemplate.getForObject(url, String.class);
            wxAuthResponse = JSONObject.parseObject(res);

            //3. 解析返回参数 报错则进行对应处理
            /*校验1: wx请求不是null*/
            if (wxAuthResponse != null) {

                /*校验2: 响应对象是否正确*/
                CheckUtils.responseCheck(wxAuthResponse);

                //3.1 获取session_key和openid
                sessionKey = wxAuthResponse.getString("session_key");
                openid = wxAuthResponse.getString("openid");
                log.info("===> openid:  {}", openid);

                /*校验3: 响应信息是否正常*/
                if (StringUtils.isBlank(sessionKey) || StringUtils.isBlank(openid)) {
                    log.error("小程序授权失败,session_key或open_id是空!");
                    throw new ServiceException("抱歉, 小程序授权失败,缺少关键返回参数!");
                }
                log.info("===> 微信回调信息: {}", wxAuthResponse);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException("抱歉, 小程序授权失败!");
        }

        //4. 获取微信信息并制作token
        /*校验:(选用)如果获取union_id 需要进行解密*/
        Map<String, String> map = new HashMap<>();
        String token = "";
        if (StringUtils.isNotBlank(encryptedIv) && StringUtils.isNotBlank(encryptedData)) {
            String decryptResult = "";
            try {
                //如果没有绑定微信开放平台,解析结果是没有unionid的。
                decryptResult = AESUtils.decrypt(sessionKey, encryptedIv, encryptedData);

                if (StringUtils.hasText(decryptResult)) {
                    //如果解析成功,获取token
                    map.put("type", String.valueOf(1));
                    map.put("decryptResult", decryptResult);
                }
            } catch (Exception e) {
                e.printStackTrace();
                throw new ServiceException("微信登录失败!");
            }
        } else {
            //如果前端只传wxCode 没有unionId的需求 只要openId的
            map.put("type", String.valueOf(2));
            map.put("decryptResult", openid);
        }

        /*校验: 数据为空的情况*/
        if (StringUtils.isBlank(map.get("type")) || StringUtils.isBlank(map.get("decryptResult"))) {
            throw new ServiceException(ResponseEnum.A10007);
        }


        return map;
    }

    /**
     * 2. 获取缓存中的AccessToken
     * <p>
     * 没有从微信拉取[可配合定时]
     *
     * @return accessToken
     */
    public String getRedisCacheAccessToken() {
        /*校验: 缓存中有accessToken的key*/
        if (redisCache.hasKey(CacheConstants.WX_ACCESS_TOKEN)) {

            log.info("二级缓存数据取出accessToken成功!");

            return redisCache.getCacheObject(CacheConstants.WX_ACCESS_TOKEN);
        }
        //这里不用三目(不好看~~)
        return getWxMiniAccessToken();
    }


    /**
     * 3. 访问微信官方获取两小时的 accessToken
     *
     * @return accessToken
     */
    public String getWxMiniAccessToken() {
        Map<String, String> query = new HashMap<>();
        query.put("grant_type", wechatConfigProperties.getGrantType());//client_credential
        query.put("secret", wechatConfigProperties.getAppSecret());
        query.put("appid", wechatConfigProperties.getAppId());
        try {
            String aTokenUrl = wechatConfigProperties.getATokenUrl();
//            ResponseEntity<JSONObject> responseEntity = restTemplate.postForEntity(aTokenUrl, query, JSONObject.class);
            ResponseEntity<JSONObject> responseEntity = restTemplate.getForEntity(aTokenUrl, JSONObject.class, query);
            HttpStatus statusCode = responseEntity.getStatusCode(); //状态码

//            System.out.println(responseEntity.getHeaders());//获取到头信息

            /*校验: 如果接口成功 200*/
            if (Objects.equals(statusCode.value(), 200)) {
                JSONObject responseJsonBody = responseEntity.getBody();//响应体


                log.info("[请求微信小程序官方接口] => 获取accessToken请求成功返回值:{}", responseJsonBody);

                if (responseJsonBody == null) {
                    log.info("微信小程序获取accessToken请求返回result是null!");
                    throw new ServiceException(ResponseEnum.A10005);
                }
                //获取accessToken
                String accessToken = responseJsonBody.getString("access_token");
                if (StringUtils.isBlank(accessToken)) {
                    log.info("微信小程序获取accessToken请求返回access_token是null!");
                    throw new ServiceException(ResponseEnum.A10005);
                }
                //放入缓存中
                redisCache.setCacheObject(CacheConstants.WX_ACCESS_TOKEN, accessToken, wechatConfigProperties.getExpiredTime(), TimeUnit.MINUTES);

                return accessToken;
            } else {
                log.error("微信HttpStatus的StatusCode不是200 {}", statusCode.value());
                throw new ServiceException(ResponseEnum.A10005);
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.info("微信小程序获取accessToken请求异常信息 {}", e.getMessage());
            throw new ServiceException(ResponseEnum.A10005);
        }

    }

    /**
     * 错误码	错误描述	解决方案
     * -1	    system error	                                [系统繁忙,此时请开发者稍候再试]
     * 40029	code 无效	                                    [js_code无效]
     * 45011	api minute-quota reach limit mustslower retry   [next minute API 调用太频繁,请稍候再试]
     * 40013	invalid appid	                                [请求appid身份与获取code的小程序appid不匹配]
     * 错误码
     *
     * @param code js_code
     * @return phone
     */
    public String getPhoneByCode(String code) {
        String phoneUrl = wechatConfigProperties.getPhoneUrl(getRedisCacheAccessToken());

        Map<String, Object> map = new HashMap<>();
        map.put("code", code);

        JSONObject jsonObject = sendPostRestTemplate(phoneUrl, map, JSONObject.class);
        System.out.println(jsonObject);

        if (jsonObject.containsKey("errcode")) {

            /*如果异常码是0 说明正常*/
            if (!Objects.equals(String.valueOf(jsonObject.get("errcode")), "0")) {
                log.error("===> 获取手机号的异常信息 : {}", jsonObject + "");

                throw new ServiceException("获取失败: " + jsonObject.get("errmsg"), (Integer) jsonObject.get("errcode"));
            }
        }

        JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");

        return phoneInfo.getString("phoneNumber");
    }


    /**
     * 生成小程序带参数二维码
     * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html#HTTPS%20%E8%B0%83%E7%94%A8
     */
    @SneakyThrows
    public InputStream getUnlimitedWxQrCode(WxCodeUnlimitedReqDTO wxCodeUnlimitedReqDTO, String accessToken) {

        Map<String, Object> params = new HashMap<>();
        params.put("scene", wxCodeUnlimitedReqDTO.getScene());
        params.put("page", wxCodeUnlimitedReqDTO.getPage());
        params.put("path", wxCodeUnlimitedReqDTO.getPage());
        params.put("env_version", wxCodeUnlimitedReqDTO.getEnvVersion());
        params.put("width", wxCodeUnlimitedReqDTO.getWidth());
        params.put("auto_color", wxCodeUnlimitedReqDTO.getAutoColor());//自动配置线条颜色

        ResponseEntity<byte[]> response = restTemplate.postForEntity(wechatConfigProperties.getWxACodeUnLimitUrl(accessToken), JSON.toJSONString(params), byte[].class);
        System.out.println(JSON.toJSONString(params));

        byte[] buffer = response.getBody();
//        System.out.println(Base64.getEncoder().encodeToString(buffer));

        assert buffer != null;
        return new ByteArrayInputStream(buffer);
    }

    /**
     * 远程调用 restTemplate方法 post请求
     *
     * @param url
     * @param body
     * @return
     */
    public <T> T sendPostRestTemplate(String url, Map<String, Object> body, Class<T> responseType) {
        return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, null), responseType).getBody();
    }
}

2.2.2 创建RedisCache

redis的增删改查操作 记得配置序列化与反序列化
文章传送门: ===> redis高级(序列化与反序列化) <===

@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {

        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit));
    }

    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key) {

        if (StringUtils.isBlank(key)) {
            throw new IllegalArgumentException("key cannot be null");
        }

        return redisTemplate.getExpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return Boolean.TRUE.equals(redisTemplate.delete(key));
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection) {
        return redisTemplate.delete(collection) > 0;
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 删除Hash中的某条数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey) {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

    /**
     * 获得缓存的基本对象列表(全部的key)
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

2.2.3 AESUtils工具类

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import javax.validation.constraints.NotNull;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;

/**
 * @author pzy
 * @description ok
 * @version 0.1.0
 */
@Slf4j
public class AESUtils {
    // 加密模式
    private static final String ALGORITHM = "AES/CBC/PKCS7Padding";
    private static final String CHARSET_NAME = "UTF-8";
    private static final String AES_NAME = "AES";

    //解决java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/CBC/PKCS7Padding
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 解密
     *
     * @param content 目标密文
     * @param key     秘钥
     * @param iv      偏移量
     * @return 解密信息
     */
    public static String decrypt(@NotNull String content, @NotNull String key, @NotNull String iv) {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            byte[] sessionKey = java.util.Base64.getDecoder().decode(key);
            SecretKeySpec keySpec = new SecretKeySpec(sessionKey, AES_NAME);
            byte[] ivByte = java.util.Base64.getDecoder().decode(iv);
            AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
            cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
            return new String(cipher.doFinal(Base64.decodeBase64(content)), CHARSET_NAME);
        } catch (Exception e) {
            log.error("解密失败:{}", e);
            e.printStackTrace();
        }
        return StringUtils.EMPTY;
    }

    public static void main(String[] args) {

        //接口传入的参数
        String sessionKey = "";
        String encryptedData = "";
        String iv = "";

        String decrypt = AESUtils.decrypt(encryptedData, sessionKey, iv);
        System.out.println("解密后:" + decrypt);
    }
    
}

2.3 各个功能传送门

2.3.1 获取accessToken

在这里插入图片描述

官方文档地址: => 获取接口调用凭据 <=
文章传送门 : => 微信小程序01: springboot获取accessToken方式 <=

2.3.2 获取手机号

在这里插入图片描述

官方文档地址: => 获取手机号 <=
文章传送门: => 微信小程序02: 使用微信快速验证组件code获取手机号

2.3.3 获取小程序二维码(不限制)

在这里插入图片描述

官方文档地址: => 获取不限制的小程序码 <=
文章传送门:

2.3.4 获取openId与unionId

在这里插入图片描述

官方文档地址: => 小程序登录 <=
文章传送门:

3. 文章的总结

3.1 本文总结

本文使用的技术栈

  • springboot相关操作
  • restTemplate远程调用使用方式
  • redisTemplate 操作redis的操作
  • redis的序列化与反序列化

3.2 本文统一说明

本篇涵盖大部分的微信小程序操作(无支付), 统一封装
2.3 中统一传送到具体功能配置
避免多次配置, 传送门内的功能细节不在这篇介绍
特别注意: 文章传送门会在近期完善, 这是本专栏的第一篇, 之后也会围绕此篇进行更新, 接入支付等等



作者: pingzhuyan

;