(1)小程序端调用 wx.login方法获取用户登录凭证code,将code发送给小程序后台服务器;服务器调用登录凭证校验接口(需要传参appid+appsecret+code),进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等,将这些信息存入缓存中。
(2)点击button按钮触发获取微信手机号弹框!
(3)绑定微信账号,存在相应手机号账号时自动登录;不存在时自动创建
1.App登录,如果没有绑定过,重新绑定,es_third_login插入一条记录,并且要存unionId字段
2.APP登录,如果之前绑定过,并且之前的es_third_login表没有存unionId,则更新原数据存入unionId
3.小程序登录,如果之前APP未授权登录,没有存入unionId,首次授权的时候,会在es_third_login插入一条记录
4.小程序登录,如果之前APP授权登录过,存过unionId,那么就直接登录,不会插入数据。
小程序和APP的unionId是相同的,小程序和APP微信联合登录,是通过unionId实现的。只要es_third_login表中同一个会员存在unionId就可以直接登录。
App登录: 调用 thirdLoginSubmit接口
小程序登录: 调用thirdLoginSubmit接口
授权登录:调用getUnionId接口获取到UnionId调用thirdLoginSubmit接口;
thirdLoginSubmit登录接口:
@Override
public BaseResponse<Map<String,Object>> thirdLoginSubmit(@RequestBody ThirdLoginInfoRequest request){
try {
if(request==null){
return MessageResponse.messageResponse("500","false","shop.requestParam.empty",null);
}
String deviceId=request.getDeviceId();
String pluginId=request.getPluginId();
String openId=request.getOpenId();
String appType=request.getAppType();
String unionId = request.getUnionId();
ThirdLogin thirdLogin = thirdLoginService.find(pluginId, openId);
ThirdLogin thirdLoginByUnionId = null;
if (thirdLogin == null){
thirdLoginByUnionId = thirdLoginService.findByUnionId(unionId);
}else{
thirdLoginByUnionId = thirdLogin;
}
HashMap<String, Object> retMap = new HashMap<String, Object>();
if(thirdLoginByUnionId!=null){
Member member = memberService.find(thirdLoginByUnionId.getMember());
Map<String, Object> result = registerMemberUtil.loginCheck(member, RegisterMemberUtil.Type.app, deviceId, appType, request.getAppVersion());
if("error".equals(result.get("type"))){
return MessageResponse.messageResponse("500", "false", result.get("message").toString(), null);
}else {
member=(Member)result.get("member");
}
//老账户unionId为空的,存入unionId
if(StringUtils.isBlank(thirdLoginByUnionId.getUnionId())){
thirdLoginByUnionId.setUnionId(request.getUnionId());
thirdLoginByUnionId.setLastModifiedDate(new Date());
thirdLoginService.update(thirdLoginByUnionId);
}
VipDto vipDto = new VipDto();
vipDto.setMessageId(UUID.randomUUID().toString().replace("-",""));
vipDto.setMemberId(member.getId());
vipDto.setAppType(member.getAppType());
vipDto.setDeviceId(member.getDeviceId());
vipDto.setMessageType(MessageType.login);
loginProducer.send(GsonUtil.toJson(vipDto));
ckToReceiptProducer.send(vipDto);
retMap.put("root", "login");
String appToken=JWTUtils.createAppToken(member.getId(),member.getPassword(),member.getDeviceId(),member.getAppLoginDate());
if(StringUtils.isNotEmpty(appToken) && appToken.startsWith("CharlesKeith ")){
RedisUtils.set(APP_TOKEN + member.getId(), appToken);
appToken=appToken.replaceFirst("CharlesKeith ","");
}
retMap.put(Constants.TOKEN_PARAMETER_NAME, appToken);
retMap.put(Constants.USER_ID, member.getId());
retMap.put(Constants.USERNAME, member.getUsername());
retMap.put(Constants.USERTYPE, member.getType().name());
retMap.put(Constants.REGISTER_LOCATION, member.getRegisterLocation());
retMap.put(Constants.MOBILE, member.getMobile());
retMap.put("valid",valid(member.getId()));
//添加购物车
addCart(request,member);
}else {
retMap.put("root", "bindOrRegister");
}
return BaseResponse.successResponse(retMap);
}catch (Exception e){
e.printStackTrace();
}
return BaseResponse.errorResponse();
}
首先从request当中获取到unionId,pluginId,openId,先根据pluginId,和openId查询第三方绑定信息,根据pluginId和openId查询到的第三方绑定信息为空则根据unionId查询第三方绑定信息
如果查询到的第三方绑定信息为空则返回一个map,封装"root":"bindOrRegister"信息给前端;
如果查询到的第三方绑定信息不为空则:从第三方绑定信息中获取到memberId查询会员信息,对会员信息进行登录校验,校验第三方登录信息是否为空,为空的话把request中的unionid,修改时间跟新保存第三方信息.
异步调用登录活动发劵,同步小票信息,封装"root","login"到map中生成token信息,把token和会员信息封装到map当中返回给前端,跟新购物车信息;
thirdBindOrRegister第三方登录或注册接口:
@Override
public BaseResponse<Map<String,Object>> thirdBindOrRegister(@RequestBody ThirdLoginInfoRequest request){
try {
if(request==null){
return MessageResponse.messageResponse("500","false","shop.parameter.notempty",null);
}
String mobile=request.getMobile();
String captcha=request.getCaptcha();
VerificationCodeRequest codeRequest=new VerificationCodeRequest();
codeRequest.setPhoneNumber(mobile);
VerificationCodeResponse codeResponse = null;
BaseResponse<VerificationCodeResponse> verificationCode = smsClient.getVerificationCode(codeRequest);
if(verificationCode!=null && "true".equals(verificationCode.getStatus())){
codeResponse=verificationCode.getResult();
}
if(codeResponse==null || !codeResponse.getVerifyCode().equals(captcha)){
return MessageResponse.messageResponse("500","false","shop.code.wrong",null);
}
String pluginId=request.getPluginId();
String deviceId=request.getDeviceId();
String openId=request.getOpenId();
String appType=request.getAppType();
String unionId = request.getUnionId();
//小程序不需要传openId
if(!"wx".equals(appType)){
if(StringUtils.isBlank(pluginId) || StringUtils.isBlank(openId)){
return MessageResponse.messageResponse("500","false","shop.common.invalid",null);
}
}
Member member = memberService.findByMobile(mobile);
if(member!=null){
ThirdLogin thirdLogin = thirdLoginService.findByPluginId(pluginId, member.getId());
if(Validator.isNotNullOrEmpty(thirdLogin) && Validator.isNotNullOrEmpty(thirdLogin.getOpenId()) && thirdLogin.getOpenId().equals(openId)){
return MessageResponse.messageResponse("500","false","shop.thirdAccount.error",null);
}
Map<String, Object> checkResult = registerMemberUtil.loginCheck(member, RegisterMemberUtil.Type.app, appType, null,request.getAppVersion());
if(checkResult.get("type").equals("error")){
return MessageResponse.messageResponse("500","false",checkResult.get("message").toString(),null);
}else{
member=(Member) checkResult.get("member");
}
VipDto vipDto=new VipDto();
vipDto.setMessageId(UUID.randomUUID().toString().replace("-",""));
vipDto.setMemberId(member.getId());
vipDto.setDeviceId(member.getDeviceId());
vipDto.setAppType(member.getAppType());
vipDto.setMessageType(MessageType.login);
loginProducer.send(GsonUtil.toJson(vipDto));
ckToReceiptProducer.send(vipDto);
}else{
// 注册操作(没有密码)
member=new Member();
member.setCreatedDate(new Date());
member.setLastModifiedDate(new Date());
member.setRegisterLocation(Constants.REGISTER_APP);
member.setMemberRank(memberRankService.findDefault().getId());
member.setAppLoginDate(new Date());
member.setMobile(mobile);
member.setDeviceId(deviceId);
member.setAppType(appType);
member.setType(MemberType.normal);
member.setPoint(Long.valueOf(0L));
member.setBalance(BigDecimal.ZERO);
member.setAmount(BigDecimal.ZERO);
member.setIsLocked(Boolean.valueOf(false));
member.setIsEnabled(Boolean.valueOf(true));
member.setLoginFailureCount(Integer.valueOf(0));
member.setAppVersion(request.getAppVersion());
Map<String,Object> checkResult = registerMemberUtil.registerCheck(member, RegisterMemberUtil.Type.app,null);
if(checkResult.get("type").equals("error")){
return MessageResponse.messageResponse("500","false",checkResult.get("message").toString(),null);
}else {
member = (Member) checkResult.get("member");
}
Member sourceMember = memberService.saveAndReturn(member);
VipDto vipDto=new VipDto();
vipDto.setMessageId(UUID.randomUUID().toString().replace("-",""));
vipDto.setMemberId(sourceMember.getId());
vipDto.setMessageType(MessageType.register);
ckToReceiptProducer.send(vipDto);
}
ThirdLogin thirdLogin=new ThirdLogin();
thirdLogin.setCreatedDate(new Date());
thirdLogin.setLastModifiedDate(new Date());
thirdLogin.setMember(member.getId());
thirdLogin.setOpenId(openId);
thirdLogin.setPluginId(pluginId);
thirdLogin.setUnionId(unionId);
thirdLoginService.save(thirdLogin);
HashMap<String, Object> retMap = new HashMap<String, Object>();
String appToken=JWTUtils.createAppToken(member.getId(),member.getPassword(),member.getDeviceId(),member.getAppLoginDate());
if(StringUtils.isNotEmpty(appToken) && appToken.startsWith("CharlesKeith ")){
RedisUtils.set(APP_TOKEN + member.getId(), appToken);
appToken=appToken.replaceFirst("CharlesKeith ","");
}
retMap.put("root", "login");
retMap.put(Constants.MOBILE, member.getMobile());
retMap.put(Constants.USERTYPE, member.getType().name());
retMap.put(Constants.TOKEN_PARAMETER_NAME, appToken);
retMap.put(Constants.USER_ID, member.getId());
retMap.put(Constants.USERNAME, member.getUsername());
retMap.put(Constants.REGISTER_LOCATION, member.getRegisterLocation());
retMap.put("valid",valid(member.getId()));
//添加购物车
addCart(request,member);
return BaseResponse.successResponse(retMap);
}catch (Exception e){
e.printStackTrace();
return BaseResponse.errorResponse();
}
}
当前端调用登录返回map为"root":"bindOrRegister"信息时调用接口
首先校验request信息不为空,从request中获取到手机号和验证码,对验证码进行校验。从request中获取到pluginId,deviceId,openId,appType,unioId,根据appType判断当不为微信小程序登录时候对pluginId.openId校验不能为空
根据手机号获取到会员信息,会员信息不为空时进行登录操作: 根据pluginId和会员Id查询到该会员的第三方绑定信息,把第三方中的openId与request获取的openId比较是否相同来判断是否已经绑定过该信息;
对会员信息进行登录校验,异步调用活动发劵,小票同步信息, 根据会员信息和request的信息生成第三方绑定信息保存,生成token,返回"root":"login"和token信息和会员信息到Map中返回给前端跟新购物车信息
会员信息为空时进行注册操作: 根据request信息生成新会员信息,对会员信息进行注册校验保存会员信息,调用异步小票同步信息。根据会员信息和request的信息生成第三方绑定信息保存,生成token,返回"root":"login"和token信息和会员信息到Map中返回给前端更新购物车信息
以下逻辑是小程序授权登录
一、获取unionId
通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程
请求地址
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
请求参数
属性 类型 默认值 必填 说明
appid string 是 小程序 appId
secret string 是 小程序 appSecret
js_code string 是 登录时获取的 code
grant_type string 是 授权类型,此处只需填写 authorization_code
返回值
Object
返回的 JSON 数据包
属性 类型 说明
openid string 用户唯一标识
session_key string 会话密钥
unionid string 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明。
errcode number 错误码
errmsg string 错误信息
2.用户未关注公众号等相关主体账号时,通过code获取不到unionId时,则通过session_key、encryptedData、iv获取unionId
encryptedData string 包括敏感数据在内的完整用户信息的加密数据
iv string 加密算法的初始向量
接口如果涉及敏感数据(如wx.getUserInfo当中的 openId 和 unionId),接口的明文内容将不包含这些敏感数据。开发者如需要获取敏感数据,需要对接口返回的加密数据(encryptedData) 进行对称解密。 解密算法如下:
对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
对称解密的目标密文为 Base64_Decode(encryptedData)。
对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。
如接口 wx.getUserInfo 敏感数据当中的 watermark:
{
"openId": "OPENID",
"nickName": "NICKNAME",
"gender": GENDER,
"city": "CITY",
"province": "PROVINCE",
"country": "COUNTRY",
"avatarUrl": "AVATARURL",
"unionId": "UNIONID",
"watermark":
{
"appid":"APPID",
"timestamp":TIMESTAMP
}
}
a.首先通过code获取到session_key
b.通过session_key解密,从解密后的接送中提取unionId
二、获取session_key
小程序点击个人中心,首先会调thirdLogin/getSessionKey,后端通过传递的code获取到session_key保存到redis,用于后续获取手机号
session_key的过期时间通过小程序的checkSession检查,不需要后端做任何操作,获取到的session_key也不需要返还给小程序,只需自己保存即可
三、获取手机号
文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
小程序调thirdLogin/getPhoneNumber,传给后端encryptedData,iv。后端通过之前redis保存的session_key解密数据
解密坑很多(前端必须将传递的数据通过encode处理,防止数据传递丢失,同时encryptedData,iv必须保证16个字节长度)
解密成功返给前端手机号,前端拿到手机号后通过手机号授权快速登录
获取得到的开放数据为以下 json 结构:
{
"phoneNumber": "13580006666",
"purePhoneNumber": "13580006666",
"countryCode": "86",
"watermark":
{
"appid":"APPID",
"timestamp": TIMESTAMP
}
}
参数 类型 说明
phoneNumber String 用户绑定的手机号(国外手机号会有区号)
purePhoneNumber String 没有区号的手机号
countryCode String 区号
四、小程序通过手机号快速授权登录
通过手机号查询会员(手机号为之前解密传给前端的手机号)
会员存在,直接将会员数据返回给小程序
会员不存在,直接注册新会员,并将数据返回给小程序(小程序快捷登录的不绑定第三方)
五、开发中的坑
前端传过来的code,通过后台请求微信接口的时候,code只能用一次,重复使用小程序报错msg:code been used, hints
获取手机号的时候,需要用到session_key,在刚开始通过code获取到session_key的时候,可以放到redis里,等下次用的时候直接在redis拿,切忌不可将session_key返回前端然后在获取手机号的时候通过前端传递
保存session_key的到redis时候,一定要记得redis的key要唯一,最开始我就是直接保存的,导致多个用户同时登录时候,只有一个能登录成功,其实是因为他们共用了同一个session_key。一定要加以区分,可以把openId传过来放入key中作为唯一约束
也许前端生成的加密数据和加密向量是正常的,直接复制给我们调微信接口可以获取到数据,但是在传递的时候可能会丢失字符,比如%等字符会解析成其他字符,可通过base64处理