SpringBoot中快速实现微信授权回调获取用户信息,支持配置多个appId
1.引入weixin-java-mp
pom.xml
文件中引入
<!-- https://mvnrepository.com/artifact/com.github.binarywang/weixin-java-mp -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2.配置文件配置公众号信息
application.yml
中配置
wx:
mp:
configs:
- appId: wxdafc984af070114c # 第一个公众号的appid
secret: c5b427fdf9d925816cdc4f5420c47278 # 公众号的appsecret
token: token # 接口配置里的Token值
aesKey: # 接口配置里的EncodingAESKey值
- appId: 2222 # 第二个公众号的appid,以下同上
secret: 1111
token: 111
aesKey: 111
3.加载配置信息
3.1加载配置属性
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* wechat mp properties
*/
@Data
@ConfigurationProperties(prefix = "wx.mp")
public class WxMpProperties {
/**
* 多个公众号配置信息
*/
private List<MpConfig> configs;
@Data
public static class MpConfig {
/**
* 设置微信公众号的appid
*/
private String appId;
/**
* 设置微信公众号的app secret
*/
private String secret;
/**
* 设置微信公众号的token
*/
private String token;
/**
* 设置微信公众号的EncodingAESKey
*/
private String aesKey;
}
}
3.2初始化配置
import com.google.common.collect.Maps;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Configuration
@EnableConfigurationProperties(WxMpProperties.class)
public class WxMpConfiguration {
private WxMpProperties properties;
private static Map<String, WxMpService> mpServices = Maps.newHashMap();
@Autowired
public WxMpConfiguration(WxMpProperties properties) {
this.properties = properties;
}
public static Map<String, WxMpService> getMpServices() {
return mpServices;
}
@Bean
public Object services() {
final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
if (configs == null) {
throw new RuntimeException("无相关配置");
}
mpServices = configs.stream().map(a -> {
WxMpDefaultConfigImpl configStorage = new WxMpDefaultConfigImpl();
configStorage.setAppId(a.getAppId());
configStorage.setSecret(a.getSecret());
configStorage.setToken(a.getToken());
configStorage.setAesKey(a.getAesKey());
WxMpService service = new WxMpServiceImpl();
service.setWxMpConfigStorage(configStorage);
return service;
}).collect(Collectors.toMap(s -> s.getWxMpConfigStorage().getAppId(), a -> a, (o, n) -> o));
return Boolean.TRUE;
}
}
3.3Controller实现
import com.alibaba.fastjson.JSON;
import com.cztv.news.common.util.ToUrlParamsUtils;
import com.cztv.news.portal.config.WxMpConfiguration;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@RestController
@RequestMapping("/api/wx")
public class WxOauthController {
@RequestMapping("/callback")
public ModelAndView wxCallback(@RequestParam String appId,
@RequestParam String state,
@RequestParam String code) {
final WxMpService wxService = WxMpConfiguration.getMpServices().get(appId);
if (!wxService.switchover(appId)) {
throw new IllegalArgumentException(String.format("未找到对应appId=[%s]的配置,请核实!", appId));
}
try {
WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code);
WxOAuth2UserInfo user = wxService.getOAuth2Service().getUserInfo(accessToken, null);
String url = ToUrlParamsUtils.addParamToUrl(state, "wxUserInfo", JSON.toJSONString(user));
return new ModelAndView("redirect:" + url);
} catch (WxErrorException e) {
log.error("微信appId:{}授权获取用户信息异常", appId, e);
}
String url = ToUrlParamsUtils.addParamToUrl(state, "wxUserInfo", null);
return new ModelAndView("redirect:" + url);
}
}
防止URL中可能有同名参数以及?#
的拼接处理
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* @Description 实体类对象转换为url参数
*/
@Slf4j
public class ToUrlParamsUtils {
/**
* url后追加参数
* 如果url包含当前参数,则输出原始url
* @param url
* @param param
* @return
*/
public static String addParamToUrl(String url, String param, String paramValue) {
if (StringUtils.isBlank(url)) {
return StringUtils.EMPTY;
}
if (url.contains("?")) {
if (isContainParam(url, param)) {
return url;
}
return url.contains("#") && url.indexOf("#") > url.indexOf("?") ? url.replaceFirst("#", "&" + param + "=" + paramValue + "#") : url + "&" + param + "=" + paramValue;
} else {
return url.contains("#") ? url.replaceFirst("#", "?" + param + "=" + paramValue + "#") : url + "?" + param + "=" + paramValue;
}
}
/**
* 解析出url请求的路径,包括页面
* @param strURL url地址
* @return url路径
*/
public static String urlPage(String strURL) {
String strPage = null;
String[] arrSplit;
strURL = strURL.trim().toLowerCase();
arrSplit = strURL.split("[?]");
if (strURL.length() > 0) {
if (arrSplit.length > 1) {
if (arrSplit[0] != null) {
strPage = arrSplit[0];
}
}
}
return strPage;
}
/**
* 去掉url中的路径,留下请求参数部分
* @param strURL url地址
* @return url请求参数部分
*/
private static String truncateUrlPage(String strURL) {
String strAllParam = null;
String[] arrSplit;
strURL = strURL.trim().toLowerCase();
arrSplit = strURL.split("[?]");
if (strURL.length() > 1) {
if (arrSplit.length > 1) {
if (arrSplit[1] != null) {
strAllParam = arrSplit[1];
}
}
}
return strAllParam;
}
/**
* 解析出url参数中的键值对
* 如 "index.jsp?Action=del&id=123",解析出Action:del,id:123存入map中
* @param url url地址
* @return url请求参数部分
*/
public static Map<String, String> urlRequest(String url) {
Map<String, String> mapRequest = new HashMap<>();
String[] arrSplit;
String strUrlParam = truncateUrlPage(url);
if (strUrlParam == null) {
return mapRequest;
}
// 每个键值为一组
arrSplit = strUrlParam.split("[&]");
return putKVInMap(mapRequest, arrSplit);
}
public static Map<String, String> strToMap(String splitStr, String delimiter) {
Map<String, String> map = new HashMap<>();
if (StringUtils.isBlank(splitStr)) {
return map;
}
// 以指定分隔符切分
String[] arrSplit = splitStr.split("[" + delimiter + "]");
return putKVInMap(map, arrSplit);
}
private static Map<String, String> putKVInMap(Map<String, String> map, String[] arrSplit) {
for (String strSplit : arrSplit) {
String[] arrSplitEqual = strSplit.split("[=]");
//解析出键值
if (arrSplitEqual.length > 1) {
//正确解析
map.put(arrSplitEqual[0], arrSplitEqual[1]);
} else {
if (!arrSplitEqual[0].equals("")) {
//只有参数没有值,不加入
map.put(arrSplitEqual[0], "");
}
}
}
return map;
}
/**
* 请求参数中是否包含参数
* @param url
* @param param
* @return
*/
public static boolean isContainParam(String url, String param) {
return urlRequest(url).containsKey(param);
}
/**
* @param clazz 参数实体类
* @return String
* @Description: 将实体类clazz的属性转换为url参数
*/
public static String getParams(Object clazz) {
// 遍历属性类、属性值
Field[] fields = clazz.getClass().getDeclaredFields();
StringBuilder requestURL = new StringBuilder();
try {
boolean flag = true;
String property, value;
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
// 允许访问私有变量
field.setAccessible(true);
// 属性名
property = field.getName();
// 属性值
value = Optional.ofNullable(String.valueOf(field.get(clazz))).orElse("");
String params = property + "=" + value;
if (flag) {
requestURL.append(params);
flag = false;
} else {
requestURL.append("&" + params);
}
}
} catch (Exception e) {
log.error("转换URL参数异常:", e);
}
return requestURL.toString();
}
}
4.设置微信公众平台
5.前台请求及地址跳转
前台访问地址
https://open.weixin.qq.com/connect/oauth2/authorize?
appid=wxdafc984af070114c&redirect_uri=http://www.xxx.com/solang/api/wx/callback?appId=wxdafc984af070114c
&response_type=code&scope=snsapi_userinfo&state=http://www.baidu.com?id=123#wechat_redirect
http://www.xxx.com/solang/api/wx/callback?appId=wxdafc984af070114c
改为服务器端回调的地址,state
微信会原样返回,此处修改为要跳转的链接地址。
用户授权后跳转会在地址上带上用户信息json参数
https://www.baidu.com/?id=123&wxUserInfo={%22openid%22:%22oCK1JuNf_CIsSBvHAnmRZMa5SMOw%22,%22nickname%22:%22suo?%22,%22sex%22:1,%22city%22:%22??%22,%22province%22:%22??%22,%22country%22:%22??%22,%22headimgurl%22:%22https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLRAEjMy5cspdSHPL91ILukRVxN1dc7qOyrRrOk4FPicFfJVFSeQpd9mNXaaEx2UGbFT3IcBicne86w/132%22,%22privilege%22:[]}
这里面跳转实现可以根据业务处理,用户信息可以存储下然后单独出个接口供前台调用。由于考虑到多个appId处理的通用性,本人这边获取到用户信息后直接拼接到前台要跳转的地址上,由前台处理。一开始考虑redirect_uri
地址使用http://www.xxx.com/solang/api/wx/callback?appId=wxdafc984af070114c&redirect=http://www.baidu.com?id=123
,然后Controller层接参数,@RequestParam String appId, @RequestParam String redirect, @RequestParam String code
,但是微信回调时&redirect=http://www.baidu.com?id=123
未带入到回调地址上,故使用state参数处理页面跳转。注意state参数微信会原样回调,所以和前台约定好形式就好。
本文参考: