前言
各种官方网站通常都会有 app、微信公众号等。比方央视网,银行等。
当咱们关注公众号或者 app 后,这些利用就能够在挪动端不便地将信息推送给用户。
对立各产品线的账号体系,实现一个账号处处应用的指标是十分有必要的。
于是有需要:能够实现微信扫码登陆。
微信对接
通常网站与集体微信的对接有两种形式:
第一种是 Oauth2 登陆:
用户通过扫描网站的二维码,受权公开信息,如昵称、头像等。便能够胜利登陆网站。
例如在爱奇艺网站扫描二维码登陆后,受权给网站,之后胜利登陆爱奇艺。
登陆完之后,微信会提示信息。
第二种是关注微信公众号
网站本人弹出一个二维码,扫描二维码后弹出公众号的关注界面、只有一关注公众号网站主动登录、第二次扫描登录的时候网站间接登录。
例如这种,轻易找的网站。
扫码后跳到公众号,关注后主动登陆。
这两种扫码登陆办法,对应微信提供的两种开发方式
一种是基于微信公众平台的扫码登录,另一种是基于微信开放平台的扫码登录。
微信开放平台就是为了让第三方利用投入微信的怀抱而设计的,这第三方利用指的是比方 android、ios、网站、零碎等;
援用
微信公众平台就是为了让程序员小伙伴利用微信自家技术 (公众号、小程序) 开发公众号、小程序而筹备的。
微信开放平台入口:https://open.weixin.qq.com/
微信公众平台入口:https://mp.weixin.qq.com/
两者应用微信扫码登录的区别:
微信开放平台须要开企业认证能力注册。比拟难申请
微信公众平台须要认证微信服务号,能力进行扫码登录的开发。只需申请一个公众号。
上面采纳第二种。
后盾与公众号配置
后盾应用的是 spring boot。前台用的 angular
这里引入 github 一个公众号开发工具包
github 仓库
后盾 pom.xml 依赖:
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
1. 申请一个公众号。若无,可用测试公众号。
微信公众平台接口测试帐号申请
2. 配置服务器信息
URL 为开发者服务器的接口地址,微信服务器通过该接口与开发者服务器建设连贯。Token 可由开发者能够任意填写,用作生成签名(该 Token 会和接口 URL 中蕴含的 Token 进行比对,从而验证安全性)
若是没有服务器,能够用 ngfork 进行内网穿透,收费好用。这里用的就是 ngfork
3. 配置后盾:
# 公众号配置(必填)
wx:
mp:
appid: wxaf7fe05a8xxxxxxxxx
secret: 57b48fcec2d5db1axxxxxxxxxxx
token: yunzhi
aesKey: 123
@Service
public class WeChatMpService extends WxMpServiceImpl {
@Autowired
private WxMpConfig wxMpConfig;
@PostConstruct
public void init() {final WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
// 设置微信公众号的 appid
config.setAppId(this.wxMpConfig.getAppid());
// 设置微信公众号的 app corpSecret
config.setSecret(this.wxMpConfig.getAppSecret());
// 设置微信公众号的 token
config.setToken(this.wxMpConfig.getToken());
// 设置音讯加解密密钥
config.setAesKey(this.wxMpConfig.getAesKey());
super.setWxMpConfigStorage(config);
}
}
4. 验证服务器地址的有效性
开发者提交信息后,微信服务器将发送 GET 申请 到方才填写的服务器地址 URL 上,
通过确认后,须要向微信服务器原样返回 echostr 参数内容,接入才失效,否则接入失败。
例如,微信服务器未正确收到返回信息,显示配置失败。
GET 申请携带参数如下表所示:
参数 | 形容 |
---|---|
signature | 微信加密签名,signature 联合了开发者填写的 token 参数和申请中的 timestamp 参数、nonce 参数 |
timestamp | 工夫戳 |
nonce | 随机数 |
echostr | 随机字符串 |
验证签名的次要代码:
用的验证签名的函数是 weixin-java-mp 包提供的。
@RequestMapping("wechat")
@RestController
public class WechatController {
@Autowired
WeChatMpService weChatMpService;
/*
* @param signature 微信加密签名,signature 联合了开发者填写的 token 参数和申请中的 timestamp 参数、nonce 参数。* @param timestamp 工夫戳
* @param nonce 这是个随机数
* @param echostr 随机字符串,验证胜利后原样返回
*/
@GetMapping
public void get(@RequestParam(required = false) String signature,
@RequestParam(required = false) String timestamp,
@RequestParam(required = false) String nonce,
@RequestParam(required = false) String echostr,
HttpServletResponse response) throws IOException {if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {this.logger.warn("接管到了未通过校验的微信音讯,这可能是 token 配置错了,或是接管了非微信官网的申请");
return;
}
response.setCharacterEncoding("UTF-8");
response.getWriter().write(echostr);
response.getWriter().flush();
response.getWriter().close();
}
}
扫码登录的开发
先来看一下大抵流程
1. 向微信服务器发送申请,获取 access_token。
微信申请 access_token 文档
access_token 是公众号的全局惟一接口调用凭据,公众号调用各接口时都需应用 access_token。开发者须要进行妥善保留。access_token 的存储至多要保留 512 个字符空间。access_token 的有效期目前为 2 个小时,需定时刷新,反复获取将导致上次获取的 access_token 生效。
url(1)申请示例地址: https://api.weixin.qq.com/cgi…
所以如果间接对微信文档开发的话,须要先申请 access_token。
但用了 weixin-java-mp 包,会帮咱们做这件事。上面跳到下一步
2. 申请获取 ticket,用于换取登陆二维码。
微信获取二维码文档
获取带参数的二维码的过程包含两步:
- 首先创立二维码 ticket
- 凭借 ticket 到指定 URL 换取二维码。
目前有 2 种类型的二维码:
- 长期二维码,有过期工夫的,最长能够设置为在二维码生成后的 30 天后过期,可能生成较多数量。
- 永恒二维码,无过期工夫的,数量较少(目前为最多 10 万个)。
长期二维码足以满足需要。
用包提供的办法获取 ticket
String qrUuid = UUID.randomUUID().toString();
WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
每次创立二维码 ticket 须要提供一个开发者自行设定的参数(scene_id), 这里用了随机生成的 uuid。
3. 用 ticket 向微信申请二维码。前端显示
申请二维码文档
获取二维码 ticket 后,开发者可用 ticket 换取二维码图片。
HTTP GET 申请(请应用 https 协定)https://mp.weixin.qq.com/cgi-…
揭示:TICKET 记得进行 UrlEncode
ticket 正确状况下,http 返回码是 200,是一张图片的 url 地址,能够间接展现或者下载。
应用包提供的办法进行申请获取
String qrCodeUrl = this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
之后返回给前端,前端用 <img> 标签解决,作为 src 的值。
<img class="img-thumbnail" [src]="qrCodeUrl"/>
4. 用户扫描二维码,微信进行事件推送
微信用户应用公共号时可能会产生很多事件,例如
- 关注 / 勾销关注事件
- 扫描带参数二维码事件
- 自定义菜单事件
- 点击菜单拉取音讯时的事件推送
- 点击菜单跳转链接时的事件推送
而以后的场景就是 用户扫描带参数二维码事件
用户扫描带场景值二维码时,可能推送以下两种事件:
- 如果用户还未关注公众号,则用户能够关注公众号,关注后微信会将带场景值关注事件推送给开发者。
- 如果用户曾经关注公众号,则微信会将带场景值扫描事件推送给开发者。
微信推送事件文档
承受微信的事件推送:
/**
* 当设置完微信公众号的接口后,微信会把用户发送的音讯,扫码事件等推送过去
*
* @param signature 微信加密签名,signature 联合了开发者填写的 token 参数和申请中的 timestamp 参数、nonce 参数。* @param encType 加密类型(暂未启用加密音讯)* @param msgSignature 加密的音讯
* @param timestamp 工夫戳
* @param nonce 随机数
* @throws IOException
*/
@PostMapping(produces = "text/xml; charset=UTF-8")
public void api(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
@RequestParam("signature") String signature,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce) throws IOException {if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {this.logger.warn("接管到了未通过校验的微信音讯,这可能是 token 配置错了,或是接管了非微信官网的申请");
return;
}
BufferedReader bufferedReader = httpServletRequest.getReader();
String str;
StringBuilder requestBodyBuilder = new StringBuilder();
while ((str = bufferedReader.readLine()) != null) {requestBodyBuilder.append(str);
}
String requestBody = requestBodyBuilder.toString();
this.logger.info("\n 接管微信申请:[signature=[{}], encType=[{}], msgSignature=[{}],"
+ "timestamp=[{}], nonce=[{}], requestBody=[\\n{}\\n]",
signature, encType, msgSignature, timestamp, nonce, requestBody);
}
5. 后盾进行逻辑解决
微信推送给公众号的音讯类型很多,而公众号也须要针对用户不同的输出做出不同的反馈。
如果应用 if … else … 来实现的话十分难以保护,这时能够依据 weixin-java-mp 开发文档,应用 WxMpMessageRouter
来对音讯进行路由保护。
先写出几个事件的 handler。比方扫码事件和关注事件。源码在文末
/**
* 解决关注事件
* 新用户走关注事件;老用户走扫码事件
* @author Binary Wang
*/
@Component
public class SubscribeHandler extends AbstractHandler {
private final WeChatMpService weChatMpService;
private final WechatService wechatService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public SubscribeHandler(WeChatMpService weChatMpService , WechatService wechatService) {super(weChatMpService);
this.weChatMpService = weChatMpService;
this.wechatService = wechatService;
}
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) throws WxErrorException {this.logger.info("新关注用户 OPENID:" + wxMessage.getFromUser());
if (this.logger.isDebugEnabled()) {this.logger.info("新关注用户 OPENID:" + wxMessage.getFromUser());
}
WeChatUser weChatUser = this.wechatService.getOneByOpenidAndAppId(wxMessage.getFromUser(), wxMessage.getToUser());
if (wxMessage.getEventKey().startsWith("qrscene_")) {String key = wxMessage.getEventKey().substring("qrscene_".length());
return this.handleByEventKey(key, weChatUser, wxMessage);
}
return new TextBuilder().build("感激关注,祝您生存欢快!",
wxMessage,
weChatMpService);
}
}
对写的几个 handler 进行路由注册
private WxMpMessageRouter router;
private void refreshRouter() {final WxMpMessageRouter newRouter = new WxMpMessageRouter(this);
// 关注事件
newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
// 勾销关注事件
newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.unsubscribeHandler).end();
// 扫码事件 newRouter.rule().async(false).msgType(EVENT).event(SCAN).handler(this.scanHandler).end();
// 默认
newRouter.rule().async(false).handler(this.msgHandler).end();
this.router = newRouter;
}
/**
* 微信事件通过这个入口进来
* 依据不同事件,调用不同 handler
*/
public WxMpXmlOutMessage route(WxMpXmlMessage message) {
try {return this.router.route(message);
} catch (Exception e) {this.logger.error(e.getMessage(), e);
}
return null;
}
当初曾经配置好了,微信推送的扫码事件或者关注事件曾经被咱们定义的 handler 获取到了。
那么有下一个问题:如果有多个用户同时在应用微信登录,咱们怎么要推送给哪个客户端,让扫码胜利的用户胜利登陆呢?
这时就须要在返回用户登陆二维码前,
- 记录该客户端的 seesionId
- 同时增加一个 key 为 uuid,value 为自定义 handler 到 hashmap 中。
这里的 uuid 为,后面获取 ticket 随机生成的 uuid。
这样就能够在扫码事件 handler 中,调用这个自定义的 handler,实现向指定客户端发送信息。
在返回二维码前,定义自定义 handler
@Override
public String getLoginQrCode(String wsLoginToken, HttpSession httpSession) {
try {if (this.logger.isDebugEnabled()) {this.logger.info("1. 生成用于回调的 uuid,请将推送给微信,微信当推送带有 UUID 的二维码,用户扫码后微信则会把带有 uuid 的信息回推过来");
}
String qrUuid = UUID.randomUUID().toString();
WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
this.wxMpService.addHandler(qrUuid, new WeChatMpEventKeyHandler() {long beginTime = System.currentTimeMillis();
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean getExpired() {return System.currentTimeMillis() - beginTime > 10 * 60 * 1000;
}
/**
* 扫码后调用该办法
* @param wxMpXmlMessage 扫码音讯
* @param weChatUser 扫码用户
* @return 输入音讯
*/
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, WeChatUser weChatUser) {if (this.logger.isDebugEnabled()) {this.logger.info("2. 用户扫描后触发该办法, 发送扫码胜利的同时,将 wsUuid 与微信用户绑定在一起,用前面应用 wsU");
}
String openid = wxMpXmlMessage.getFromUser();
if (openid == null) {this.logger.error("openid is null");
}
if (weChatUser.getUser() != null) {String uuid = UUID.randomUUID().toString();
bindWsUuidToWeChatUser(uuid, weChatUser);
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
uuid);
return new TextBuilder().build(String.format("登录胜利,登录的用户为:%s", weChatUser.getUser().getName()),
wxMpXmlMessage,
null);
} else {
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
false);
return new TextBuilder().build(String.format("登录准则,起因:您尚未绑定微信用户"),
wxMpXmlMessage,
null);
}
}
});
return this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
} catch (Exception e) {this.logger.error("获取长期公众号图片时产生谬误:" + e.getMessage());
}
return "";
}
利用 websocket 向前端发送信息
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连贯上进行全双工通信的协定。
WebSocket 使得客户端和服务器之间的数据交换变得更加简略,容许服务端被动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只须要实现一次握手,两者之间就间接能够创立持久性的连贯,并进行双向数据传输。
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
uuid);
之后前端就能够利用这个身份信息 loginUid 登陆了。
前台向后盾发送登陆申请
/**
* 微信扫码登录
*/
onWeChatLogin() {this.userService.getLoginQrCode()
.subscribe(src => {
this.qrCodeSrc = src;
this.loginModel = 'wechat';
this.userService.onScanLoginQrCode$.pipe(first()).subscribe(data => {
const uuid = data.body;
this.login({username: uuid, password: uuid});
});
});
}
后盾再判断 loginUid 是否在 map 中,是则登陆胜利。
/**
* 校验微信扫码登录后的认证 ID 是否无效
* @param wsAuthUuid websocket 认证 ID
*/
@Override
public boolean checkWeChatLoginUuidIsValid(String wsAuthUuid) {return this.map.containsKey(wsAuthUuid);
}