微信公众号--授权相关
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 = "http://wghyqu.natappfree.cc/alipay/wappay/pay.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("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
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参数如下: http://wghyqu.natappfree.cc/?code=081Mia6N0ZQDU721Wy5N0Npa6N0Mia6U&state=STATE#/
如果重定向的url地址是:http://wghyqu.natappfree.cc/#/login这种。那么微信会返回:
http://wghyqu.natappfree.cc/?code=081Mia6N0ZQDU721Wy5N0Npa6N0Mia6U&state=STATE#/pages/base/login
期望是这种:
http://wghyqu.natappfree.cc/?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
**/
@Override
public BaseResponse<WxPublicUserInfoBean> getWxUserInfo(String code) throws Exception{
BaseResponse<WxPublicUserInfoBean> response = new BaseResponse<>();
//1.通过code换取网页授权access_token
Map<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 {
@Autowired
private 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
*/
@Override
public 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;
@Autowired
public RedisServiceImpl(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
this.redisConnection = this.stringRedisTemplate.getConnectionFactory().getConnection();
}
@Override
public void putKeyValue(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
@Override
public void putKeyValueExpire(String key, String value, Long timeout) {
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public Boolean existsKey(String key) {
return stringRedisTemplate.hasKey(key);
}
@Override
public 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);
//过滤空 设置 TreeMap
SortedMap<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 = "http://wghyqu.natappfree.cc/alipay/wappay/pay.jsp";
/** 微信公众号全局access_token */
public final static String WX_PUBLIC_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
/** 1.用户授权的URL */
public final static String WX_USER_CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
/** 2.通过code换取网页授权access_token */
public final static String WX_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
/** 3.刷新用户授权的access_token,微信有效期为30天 */
public final static String WX_REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
/** 4.拉取用户信息(需scope为 snsapi_userinfo) */
public final static String WX_USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo";
public final static String WX_REDIS_GLOBAL_ACCESS_TOKEN = "wxGlobalAccessToken";
}
至此差不多完成,写的非常粗糙,个人只是自己记录一下。
本文地址:https://blog.csdn.net/mufeng633/article/details/107393765