目录
前提:
准备一个测试公众号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就万事大吉了。
剩下的就交给前端调用了。
完结(如果对你有帮助,别忘了点赞哦)