在前面的设计和实现中,我们的微服务开发平台通过JustAuth来实现第三方授权登录,通过集成公共组件,着实减少了很多工作量,大多数的第三方登录直接通过配置就可以实现。而在第三方授权登录中,微信小程序授权登录和APP微信授权登录是两种特殊的第三方授权登录。
JustAuth之所以能够将多种第三方授权登录服务整合在一起,抽象公共组件的原因是大多数的授权登录服务器都是遵循OAuth2.0协议开发,虽然略有不同但可通过适配器进行转换为统一接口。微信小程序授权登录和APP的微信授权登录也是OAutn2.0协议的授权登录,但在对接的流程中不是完整的OAuth2.0对接流程。
通常的第三方授权登录过程中,获取token的state和code是在回调客户端url中获取的,而微信小程序授权登录和APP的微信授权登录获取token的state和code是使用微信提供的特定方法获取到的,然后通过微信传给客户端,客户端拿到code之后到后台取获取openid等微信用户信息。然后,再进行系统登录相关操作。
一、微信小程序授权登录、注册、绑定流程设计说明
- 微信小程序授权登录、注册、绑定流程说明:
1、用户进入小程序。
2、小程序前端通过从缓存中获取是否有token来判定用户是否登录。
3、如果未登录,那么跳转到小程序登录页。
4、小程序前端执行微信登录方法wx.login获取微信登录的code(此时并未进行微信授权登录)。
5、小程序前端通过code向业务后台发送请求获取用户唯一的openid。
6、业务系统根据openid或者unionid判断该用户是否绑定了业务用户,并将是否绑定信息返回给前台。
7、如果没有绑定过,那么前端展示微信授权登录按钮。
8、用户点击“授权登录”按钮之后,小程序前端会获取到加密的用户信息。
9、小程序前端将加密的用户信息传到业务后台进行解密。
10、业务后台收到加密用户信息后,通过请求微信服务器解密用户信息,并将用户信息存储到业务系统表。
11、后台将解密后的用户信息(非私密信息)返回到小程序前台。
12、如果是没有绑定的,那么小程序前台弹出是否获取当前用户手机号的弹出框。
13、用户选择是否获取微信绑定的手机号来注册或绑定到业务系统的用户。
14、当用户点击统一获取手机号时,微信会返回加密后的手机号,然后前端将加密后的手机号发送到业务后台解密。
15、业务后台获取到手机号码之后,会根据手机号码在系统用户表中进行匹配,如果匹配到用户,那么直接返回小程序用户信息。
16、当用户不同意获取手机号时,那么小程序跳转到输入账号密码进行绑定页面。
17、当绑定操作执行成功之后,微信小程序调用第三方登录获取token方式,向业务后台获取token。
18、用户小程序授权登录、注册、绑定成功。
二、微信小程序授权登录、注册、绑定业务后台功能实现
微信通过其开放平台提供小程序登录功能接口,我们的业务服务可以通过小程序的登录接口方便地获取微信提供的用户身份标识,进而将业务自身用户体系和微信用户相结合,从而更完美地在微信小程序中实现业务功能。
微信小程序提供了对接登录的SDK,我们只需要按照其官方文档对接开发即可。同时也有很多开源组件将SDK再次进行封装,在业务开发中可以更快速的集成小程序各个接口的调用。
出于快速开发的原则,同时也少走弯路、少踩坑,我们可以选择一款实现比较完善的组件进行微信小程序的对接。weixin-java-miniapp是集成微信小程序相关SDK操作的工具包,我们在项目中集成此工具包来实现微信小程序授权登录。
1、引入weixin-java-miniapp相关maven依赖,目前发布版本为4.4.0正式版。
一般在选择开源工具包时,我们不会选择最新版,而是选择稳定版本,但是微信的开放接口经常变动,这里为了能够兼容最新的微信小程序接口,我们在引用包的时候一定要选择更新版本,否则会影响部分接口的调用。
......
......
4.4.0
......
com.github.binarywang
weixin-java-miniapp
${weixin-java-miniapp.version}
......
2、在配置文件application-dev.yml、application-test.yml、application-prod.yml中新增微信小程序需要的配置项。
关于小程序如何注册,appid和appsecret如何获取,这里不展开讲,微信开放平台有详细的说明文档。
wx:
miniapp:
configs:
- appid: #微信小程序appid
secret: #微信小程序secret
token: #微信小程序消息服务器配置的token
aesKey: #微信小程序消息服务器配置的EncodingAESKey
msgDataFormat: JSON
3、将weixin-java-miniapp配置类文件WxMaConfiguration.java和WxMaProperties.java添加到我们的工程中。
- WxMaConfiguration.java关键代码
......
@Data
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {
private List configs;
@Data
public static class Config {
/**
* 设置微信小程序的appid
*/
private String appid;
/**
* 设置微信小程序的Secret
*/
private String secret;
/**
* 设置微信小程序消息服务器配置的token
*/
private String token;
/**
* 设置微信小程序消息服务器配置的EncodingAESKey
*/
private String aesKey;
/**
* 消息格式,XML或者JSON
*/
private String msgDataFormat;
}
}
......
......
private final WxMaProperties properties;
@Autowired
public WxMaConfiguration(WxMaProperties properties) {
this.properties = properties;
}
@Bean
public WxMaService wxMaService() {
List configs = this.properties.getConfigs();
if (configs == null) {
throw new WxRuntimeException("配置错误!");
}
WxMaService maService = new WxMaServiceImpl();
maService.setMultiConfigs(
configs.stream()
.map(a -> {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(a.getAppid());
config.setSecret(a.getSecret());
config.setToken(a.getToken());
config.setAesKey(a.getAesKey());
config.setMsgDataFormat(a.getMsgDataFormat());
return config;
}).collect(Collectors.toMap(WxMaDefaultConfigImpl::getAppid, a -> a, (o, n) -> o)));
return maService;
}
......
4、新建WxMaUserController.java用于实现微信小程序请求的相关接口。
- 实现小程序登录的login接口,此接口会根据微信小程序前端传来的code进行获取用户session_key、openid/unionid,我们的业务系统会根据openid/unionid结合第三方登录表进行用户匹配,如果存在该用户则返回给小程序前台已存在的用户信息;如果不存在,说明该用户是第一次微信小程序登录,那么我们将获取到的微信唯一身份标识加密,并返回微信小程序前台进行下一步绑定账户或注册操作。
/**
* 登陆接口
*/
@ApiOperation(value = "小程序登录接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "code", value = "小程序code", dataType="String", paramType = "query"),
})
@GetMapping("/login")
public Result login(@PathVariable String appid, String code) {
if (StringUtils.isBlank(code)) {
return Result.error("code 不能为空");
}
if (!wxMaService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
WeChatMiniAppLoginDTO weChatMiniAppLoginDTO = new WeChatMiniAppLoginDTO();
try {
WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
weChatMiniAppLoginDTO.setOpenid(session.getOpenid());
weChatMiniAppLoginDTO.setUnionid(session.getUnionid());
// 通过openId获取在系统中是否是已经绑定过的用户,如果没有绑定,那么返回到前台,提示需要绑定或者注册用户
LambdaQueryWrapper socialLambdaQueryWrapper = new LambdaQueryWrapper<>();
// 如果微信开通了开放平台,那么各个渠道(小程序、公众号等)都会有统一的unionid,如果没开通,就仅仅使用openId
if (StringUtils.isBlank(session.getUnionid()))
{
socialLambdaQueryWrapper.eq(JustAuthSocial::getOpenId, session.getOpenid())
.eq(JustAuthSocial::getSource, "WECHAT_MINI_APP");
}
else
{
socialLambdaQueryWrapper.eq(JustAuthSocial::getUnionId, session.getUnionid())
.and(e -> e.eq(JustAuthSocial::getSource, "WECHAT_MINI_APP")
.or().eq(JustAuthSocial::getSource, "WECHAT_OPEN")
.or().eq(JustAuthSocial::getSource, "WECHAT_MP")
.or().eq(JustAuthSocial::getSource, "WECHAT_ENTERPRISE")
.or().eq(JustAuthSocial::getSource, "WECHAT_APP"));
}
JustAuthSocial justAuthSocial = justAuthSocialService.getOne(socialLambdaQueryWrapper, false);
if (null == justAuthSocial)
{
weChatMiniAppLoginDTO.setUserInfoAlready(false);
weChatMiniAppLoginDTO.setUserBindAlready(false);
justAuthSocial = new JustAuthSocial();
justAuthSocial.setAccessCode(session.getSessionKey());
justAuthSocial.setOpenId(session.getOpenid());
justAuthSocial.setUnionId(session.getUnionid());
justAuthSocial.setSource("WECHAT_MINI_APP");
justAuthSocialService.save(justAuthSocial);
} else {
justAuthSocial.setAccessCode(session.getSessionKey());
justAuthSocialService.updateById(justAuthSocial);
}
// 将socialId进行加密返回,用于前端进行第三方登录,获取token
DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
// 这里将source+uuid通过des加密作为key返回到前台
String socialKey = "WECHAT_MINI_APP" + StrPool.UNDERLINE + (StringUtils.isBlank(session.getUnionid()) ? session.getOpenid() : session.getUnionid());
// 将socialKey放入缓存,默认有效期2个小时,如果2个小时未完成验证,那么操作失效,重新获取,在system:socialLoginExpiration配置
redisTemplate.opsForValue().set(AuthConstant.SOCIAL_VALIDATION_PREFIX + socialKey, String.valueOf(justAuthSocial.getId()), socialLoginExpiration,
TimeUnit.SECONDS);
String desSocialKey = des.encryptHex(socialKey);
weChatMiniAppLoginDTO.setBindKey(desSocialKey);
// 查询是否绑定用户
// 判断此第三方用户是否被绑定到系统用户
Result