微信公众号
1.近期在做微信公众号相关授权,借此机会记录一下,以备后续
相关登录流程,请参考微信官方文档:
1.测试接口号申请
1.首先验证成功开发者
/*** @author: cc* @date: 2020/7/15 13:26*/@GetMapping("/wxDomainToken")@ApiOperation(value = "微信接口域验证", httpMethod = "GET", notes = "微信接口域验证")public void show(HttpServletRequest request, HttpServletResponse response) {try {//微信加密签名String signature = request.getParameter("signature");// 时间戳String timestamp = request.getParameter("timestamp");// 随机数String nonce = request.getParameter("nonce");// 随机字符串String echoStr = request.getParameter("echostr");PrintWriter out = response.getWriter();// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败String asscii= WxPublicUtil.getAscii(WxPublicConfig.WX_PUBLIC_TOKEN, timestamp, nonce);String newSignature = WxPublicUtil.SHA1(asscii);if (newSignature.equals(signature)) {out.write(echoStr);log.info("微信公众号服务验证成功!" + echoStr );}else {out.print(echoStr);log.info("微信公众号服务验证失败!" + echoStr );}out.flush();out.close();}catch (Exception e){e.printStackTrace();log.error("微信公众号服务验证异常:" + e.getMessage());}}
当验证成功后,则成为开发者成功。
2.用户统一授权,获取Code
通过redirect_url跳转的地址,获取code值,为了后续获取openId做准备。
注意:1.该Code5分钟有效,且只能使用一次
2.只能在微信客户端打开
3.重定向的url如果有特殊符号需urlencode编码
4.跳转的地址需要和网页授权的地址一样
5.重定向之后,在jsp或html中获取code参数
跳转的地址:public final static String WX_REDIRECT_URI = ".jsp";/*** @author: cc* @date: 2020/7/15 13:26*/@GetMapping("/getWxUserCode")@ApiOperation(value = "用户同意授权,并获取微信用户CODE", httpMethod = "POST", notes = "用户同意授权,获取微信用户CODE")public void getWxUserCode(HttpServletResponse response){StringBuffer sb = new StringBuffer();sb.append("=");sb.append(WxPublicConfig.WX_APPID);try {//对重定向url进行编码,官方文档要求,没有编码也可以sb.append("&redirect_uri=").append(URLEncoder.encode(WxPublicConfig.WX_REDIRECT_URI, "utf-8"));// sb.append("&redirect_uri=").append(WxPublicConfig.WX_REDIRECT_URI);sb.append("&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect");log.info("获取用户CODE请求的地址:" + sb.toString());response.sendRedirect(sb.toString());} catch (Exception e) {log.error("response重定向失败:>>" + e.getMessage());e.printStackTrace();}}
jsp中获取code参数如下: /?code=081Mia6N0ZQDU721Wy5N0Npa6N0Mia6U&state=STATE#/
如果重定向的url地址是:。那么微信会返回:
/?code=081Mia6N0ZQDU721Wy5N0Npa6N0Mia6U&state=STATE#/pages/base/login
期望是这种:
/?code=081Mia6N0ZQDU721Wy5N0Npa6N0Mia6U&state=STATE#/
解决方案是:redirect_url的#号问题
前端控制
const w = location.href.indexOf('?');
const j = location.href.indexOf('#');
let href = window.location.href;
// 处理微信回调url
if (w !== -1 && j > w) { href = location.href.substr(0, w) + location.href.substr(j, location.href.length) + location.search;location.href = href;
}
3.根据Code,获取用户的openId、unionid等
/*** @author: cc* @date: 2020/7/15 13:26*/@PostMapping("/getWxUserInfo")@ApiOperation(value = "获取微信用户信息", httpMethod = "POST", notes = "获取微信用户信息")public BaseResponse<WxPublicUserInfoBean> getWxUserInfo(@RequestBody @Validated(value = {CustWxUserInDto.wxCode.class}) CustWxUserInDto custWxUserInDto){try {return wxPublicService.getWxUserInfo(custWxUserInDto.getCode());} catch (Exception e) {e.printStackTrace();return BaseResponse.error(ResponseStatusEnum.ERROR);}}
3.1实现类
/*** @description: 通过用户Code获取用户信息openID和unionID等* @param code* @return: com.fsk.common.response.BaseResponse<java.lang.Object>* @author: cc* @date: 2020/7/11 13:10**/@Overridepublic BaseResponse<WxPublicUserInfoBean> getWxUserInfo(String code) throws Exception{BaseResponse<WxPublicUserInfoBean> response = new BaseResponse<>();//1.通过code换取网页授权access_tokenMap<String,String> codeMap = new HashMap<>();codeMap.put("appid", WxPublicConfig.WX_APPID);codeMap.put("secret",WxPublicConfig.WX_APPSECRET);codeMap.put("code",code);codeMap.put("grant_type","authorization_code");String codeResult = HttpUtil.sendRequest(WxPublicConfig.WX_ACCESS_TOKEN_URL, "GET",codeMap);WxUserAccessTokenBean wxUserAccessTokenBean = JSONObject.parseObject(codeResult, WxUserAccessTokenBean.class);if (EmptyTool.isNull(wxUserAccessTokenBean.getAccessToken())) {log.error(WxExceptionMsg.WX_ACCESS_TOKEN_EX);return BaseResponse.error(ResponseStatusEnum.ERROR.getCode(), WxExceptionMsg.WX_ACCESS_TOKEN_EX);}//2根据获取到的access_token和openId获取用户信息的性别、城市、unionId等Map<String,String> scopeMap = new HashMap<>();scopeMap.put("access_token", wxUserAccessTokenBean.getAccessToken());scopeMap.put("openid",wxUserAccessTokenBean.getOpenId());scopeMap.put("lang","zh_CN");String userResult = HttpUtil.sendRequest(WxPublicConfig.WX_USER_INFO_URL, "GET",scopeMap);WxPublicUserInfoBean wxPublicUserInfoBean = JSONObject.parseObject(userResult, WxPublicUserInfoBean.class);if (EmptyTool.isNull(wxPublicUserInfoBean.getOpenId())) {log.error(WxExceptionMsg.WX_UNION_EX);return BaseResponse.error(ResponseStatusEnum.ERROR.getCode(), WxExceptionMsg.WX_UNION_EX);}//用户手机号String customerTel = "";//3.根据openId判断获取用户信息手机号、如果没有手机号再用unionId查CustWxUserOutDto openIdDto = custWxUserMapper.selectCustWxUserByOpenId(wxPublicUserInfoBean.getOpenId());if (EmptyTool.isNull(openIdDto) || EmptyTool.isNull(openIdDto.getCustomerCode())) {//根据unionId判断获取用户信息手机号、如果没有手机号前端需dialog弹框绑定注册String unionid = wxPublicUserInfoBean.getUnionId();List<CustWxUserOutDto> unionIdDtoList = custWxUserMapper.selectCustWxUserByUnionId(unionid);if (EmptyTool.isNull(unionIdDtoList) || unionIdDtoList.size() == 0) {//新增微信客户信息表CustWxUserInDto custWxUserInDto = new CustWxUserInDto();custWxUserInDto.setId(Snowflake.nextId() + "");custWxUserInDto.setAppId(WxPublicConfig.WX_APPID);custWxUserInDto.setOpenId(wxPublicUserInfoBean.getOpenId());custWxUserInDto.setUnionId(wxPublicUserInfoBean.getUnionId());custWxUserInDto.setCreateDate(DateUtil.parse("yyyy-MM-dd HH:mm:ss",DateUtil.getNowDate()));custWxUserMapper.insertBaseCustWxUser(custWxUserInDto);}else {for (CustWxUserOutDto custWxUserOutDto: unionIdDtoList) {customerTel = EmptyTool.isNull(custWxUserOutDto.getCustomerTel()) ? "" : custWxUserOutDto.getCustomerTel();if (!EmptyTool.isNull(customerTel)) {break;}}}}else {customerTel = openIdDto.getCustomerTel();}wxPublicUserInfoBean.setCustomerTel(customerTel);log.info("微信公众号获取用户的信息:" + wxPublicUserInfoBean.toString());response.setResponseData(wxPublicUserInfoBean);return response;}
这个方法会返回用户的openId、和unionId等一些其它信息
4.获取微信全局access_token
注意:这里access_token与用户的access_token不一样
2.有效期为2小时。咱们要注意续期。这里采用定时任务 和 分布式锁实现
4.1 定时任务实现类
package com.fsk.systemCust.utils;import com.fsk.systemCust.service.WxPublicService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class TimedTask {@Autowiredprivate WxPublicService wxPublicService;/*** @description: 微信全局刷新Token,每隔一小时执行一次* @return: void* @author: cc* @date: 2020/7/13 17:51*/@Scheduled(cron = "0 0 0/1 * * ? ")private void wxGlobalAccessToken(){log.info("=================================微信全局刷新Token开始=================================");try {wxPublicService.getWxGlobalAccessToken();} catch (Exception e) {e.printStackTrace();}log.info("=================================微信全局刷新Token结束=================================");}}
4.2 access_token实现类和分布式锁实现
/*** @description: 获取微信全局的accessToken* @return: BaseResponse<WxGlobalAccessTokenBean>* @author: cc* @date: 2020/7/13 13:48*/@Overridepublic BaseResponse<WxGlobalAccessTokenBean> getWxGlobalAccessToken() throws Exception{log.info("开始获取微信全局的accessToken");BaseResponse<WxGlobalAccessTokenBean> response = new BaseResponse<>();if (!redisService.setNX(WxPublicConfig.WX_REDIS_GLOBAL_ACCESS_TOKEN + "_","1",3600L)) {if (redisService.existsKey(WxPublicConfig.WX_REDIS_GLOBAL_ACCESS_TOKEN)) {WxGlobalAccessTokenBean wxGlobalAccessTokenBean = new WxGlobalAccessTokenBean();wxGlobalAccessTokenBean.setAccessToken(redisService.get(WxPublicConfig.WX_REDIS_GLOBAL_ACCESS_TOKEN));response.setResponseData(wxGlobalAccessTokenBean);log.info("redis获取微信全局accessToken:" + wxGlobalAccessTokenBean.toString());return response;}}Map<String,String> cMap = new HashMap<>();cMap.put("grant_type", "client_credential");cMap.put("appid", WxPublicConfig.WX_APPID);cMap.put("secret",WxPublicConfig.WX_APPSECRET);String request = HttpUtil.sendRequest(WxPublicConfig.WX_PUBLIC_ACCESS_TOKEN_URL, "GET", cMap);WxGlobalAccessTokenBean wxGlobalAccessTokenBean = JSONObject.parseObject(request, WxGlobalAccessTokenBean.class);redisService.putKeyValueExpire(WxPublicConfig.WX_REDIS_GLOBAL_ACCESS_TOKEN, wxGlobalAccessTokenBean.getAccessToken(), 7200L);response.setResponseData(wxGlobalAccessTokenBean);log.info("微信服务器获取微信全局的accessToken成功:" + wxGlobalAccessTokenBean.toString());return response;}
4.3 redis的分布式锁
采用setNX实现,具体可以看我另外redis分布式锁
package com.fsk.systemCust.service.redis.impl;import com.fsk.systemCust.service.redis.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
@Component
public class RedisServiceImpl implements RedisService {private final StringRedisTemplate stringRedisTemplate;private final RedisConnection redisConnection;@Autowiredpublic RedisServiceImpl(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;this.redisConnection = this.stringRedisTemplate.getConnectionFactory().getConnection();}@Overridepublic void putKeyValue(String key, String value) {stringRedisTemplate.opsForValue().set(key, value);}@Overridepublic void putKeyValueExpire(String key, String value, Long timeout) {stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);}@Overridepublic String get(String key) {return stringRedisTemplate.opsForValue().get(key);}@Overridepublic Boolean existsKey(String key) {return stringRedisTemplate.hasKey(key);}@Overridepublic boolean deleteByKey(String key) {return stringRedisTemplate.delete(key);}public Boolean setNX(String key, String value,long timeout){Boolean isExit = redisConnection.setNX(key.getBytes(), value.getBytes());//如果设置成功,要设置其过期时间if(isExit){stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);}return isExit;}
}
相关基础util类:
5.WxPublicUtil.java
package com.fsk.systemCust.misc;import com.fsk.common.utils.wxPay.MD5Utils;
import com.fsk.common.utils.wxPay.WxConfig;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.AlgorithmParameters;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.*;/*** @description: 微信公众号相关* @author: Cc* @data: 2020/7/8 15:50*/
@Slf4j
public class WxPublicUtil {/*** @description: ASCII码表字典排序* @param timestamp* @param nonce* @return: java.lang.String* @author: cc* @date: 2020/7/9 19:17**/public static String getAscii(String publicToken, String timestamp, String nonce){String[] src = {publicToken,timestamp,nonce};List<String> list = Arrays.asList(src);Collections.sort(list);StringBuilder sb = new StringBuilder();for (int i = 0; i < list.size(); i++){sb.append(list.get(i));}return sb.toString();}/*** @description: SHA1生成签名* @param decript 微信加密签名* @return: java.lang.String* @author: cc* @date: 2020/7/9 19:16**/public static String SHA1(String decript) {try {MessageDigest digest = MessageDigest.getInstance("SHA-1");digest.update(decript.getBytes());byte messageDigest[] = digest.digest();StringBuffer hexString = new StringBuffer();// 字节数组转换为 十六进制 数for (int i = 0; i < messageDigest.length; i++) {String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);if (shaHex.length() < 2) {hexString.append(0);}hexString.append(shaHex);}return hexString.toString();} catch (NoSuchAlgorithmException e) {e.printStackTrace();}return null;}/*** 验证回调签名* @return*/public static boolean isTenpaySign(Map<String, String> map) {String characterEncoding = "utf-8";String charset = "utf-8";String signFromAPIResponse = map.get("sign");if (signFromAPIResponse == null || signFromAPIResponse.equals("")) {System.out.println("API返回的数据签名数据不存在,有可能被第三方篡改!!!");return false;}System.out.println("服务器回包里面的签名是:" + signFromAPIResponse);//过滤空 设置 TreeMapSortedMap<String, String> packageParams = new TreeMap();for (String parameter : map.keySet()) {String parameterValue = map.get(parameter);String v = "";if (null != parameterValue) {v = parameterValue.trim();}packageParams.put(parameter, v);}StringBuffer sb = new StringBuffer();Set es = packageParams.entrySet();Iterator it = es.iterator();while (it.hasNext()) {Map.Entry entry = (Map.Entry) it.next();String k = (String) entry.getKey();String v = (String) entry.getValue();if (!"sign".equals(k) && null != v && !"".equals(v)) {sb.append(k + "=" + v + "&");}}sb.append("key=" + WxConfig.APP_KEY);//将API返回的数据根据用签名算法进行计算新的签名,用来跟API返回的签名进行比较//算出签名String resultSign = "";String tobesign = sb.toString();if (null == charset || "".equals(charset)) {resultSign = MD5Utils.MD5Encode(tobesign, characterEncoding).toUpperCase();} else {try {resultSign = MD5Utils.MD5Encode(tobesign, characterEncoding).toUpperCase();} catch (Exception e) {resultSign = MD5Utils.MD5Encode(tobesign, characterEncoding).toUpperCase();}}String tenpaySign = ((String) packageParams.get("sign")).toUpperCase();return tenpaySign.equals(resultSign);}/*** @description: 小程序解密手机号、unionId* @param encryptedData 需要解密的数据* @param session_key 用户session_key、密钥* @param iv 解密数据一起的数据* @return: com.alibaba.fastjson.JSONObject* @author: cc* @date: 2020/7/15 13:23*/public static String getPhoneNumber(String encryptedData, String session_key, String iv) {// 被加密的数据byte[] dataByte = com.sun.org.apache.xerces.internal.impl.dv.util.Base64.decode(encryptedData);// 加密秘钥byte[] keyByte = com.sun.org.apache.xerces.internal.impl.dv.util.Base64.decode(session_key);// 偏移量byte[] ivByte = Base64.decode(iv);try {// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要int base = 16;if (keyByte.length % base != 0) {int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);byte[] temp = new byte[groups * base];Arrays.fill(temp, (byte) 0);System.arraycopy(keyByte, 0, temp, 0, keyByte.length);keyByte = temp;}// 初始化Security.addProvider(new BouncyCastleProvider());Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");parameters.init(new IvParameterSpec(ivByte));cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化byte[] resultByte = cipher.doFinal(dataByte);if (null != resultByte && resultByte.length > 0) {return new String(resultByte, "UTF-8");}} catch (Exception e) {e.printStackTrace();log.error("小程序解密手机号异常:" + e.getMessage());}return null;}}
5.2 WxPublicConfig.java
package com.fsk.systemCust.misc.wx.config;/*** @description: 微信公众号相关* @author: Cc* @data: 2020/7/8 15:46*/
public class WxPublicConfig {/** 微信公众号接口配置测试号Token */public final static String WX_PUBLIC_TOKEN = "123_TOKEN";/** 微信的测试号APPID */public final static String WX_APPID = "wxa5";/** 微信的测试号密钥 */public final static String WX_APPSECRET = "e75e";/** 获取access_token填写client_credential */public final static String WX_GRANT_TYPE = "client_credential";/** 前端重定向地址:要求和配置域名同地址,具体页面可以不同 */public final static String WX_REDIRECT_URI = ".jsp";/** 微信公众号全局access_token */public final static String WX_PUBLIC_ACCESS_TOKEN_URL = "";/** 1.用户授权的URL */public final static String WX_USER_CODE_URL = "";/** 2.通过code换取网页授权access_token */public final static String WX_ACCESS_TOKEN_URL = "";/** 3.刷新用户授权的access_token,微信有效期为30天 */public final static String WX_REFRESH_TOKEN_URL = "";/** 4.拉取用户信息(需scope为 snsapi_userinfo) */public final static String WX_USER_INFO_URL = "";public final static String WX_REDIS_GLOBAL_ACCESS_TOKEN = "wxGlobalAccessToken";}
至此差不多完成,写的非常粗糙,个人只是自己记录一下。