package cn.lili.modules.connect.serviceimpl; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.lili.base.Result; import cn.lili.cache.Cache; import cn.lili.common.enums.ClientTypeEnum; import cn.lili.common.enums.ResultCode; import cn.lili.common.exception.ServiceException; import cn.lili.common.properties.RocketmqCustomProperties; import cn.lili.common.security.AuthUser; import cn.lili.common.security.context.UserContext; import cn.lili.common.security.token.Token; import cn.lili.common.utils.HttpUtils; import cn.lili.common.utils.UuidUtils; import cn.lili.modules.connect.entity.Connect; import cn.lili.modules.connect.entity.dto.AuthToken; import cn.lili.modules.connect.entity.dto.ConnectAuthUser; import cn.lili.modules.connect.entity.dto.MemberConnectLoginMessage; import cn.lili.modules.connect.entity.dto.WechatMPLoginParams; import cn.lili.modules.connect.entity.enums.ConnectEnum; import cn.lili.modules.connect.entity.enums.SourceEnum; import cn.lili.modules.connect.mapper.ConnectMapper; import cn.lili.modules.connect.service.ConnectService; import cn.lili.modules.member.entity.dos.Member; import cn.lili.modules.member.entity.dto.ConnectQueryDTO; import cn.lili.modules.member.service.MemberService; import cn.lili.modules.member.token.MemberTokenGenerate; import cn.lili.modules.system.entity.dos.Setting; import cn.lili.modules.system.entity.dto.connect.WechatConnectSetting; import cn.lili.modules.system.entity.dto.connect.dto.WechatConnectSettingItem; import cn.lili.modules.system.entity.enums.SettingEnum; import cn.lili.modules.system.service.SettingService; import cn.lili.rocketmq.RocketmqSendCallbackBuilder; import cn.lili.rocketmq.tags.MemberTagsEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.naming.NoPermissionException; import java.nio.charset.StandardCharsets; import java.security.AlgorithmParameters; import java.security.Security; import java.util.*; /** * 联合登陆接口实现 * * @author Chopper */ @Slf4j @Service public class ConnectServiceImpl extends ServiceImpl implements ConnectService { static final boolean AUTO_REGION = true; @Autowired private SettingService settingService; @Autowired private MemberService memberService; @Autowired private MemberTokenGenerate memberTokenGenerate; @Autowired private Cache cache; /** * RocketMQ */ @Autowired private RocketMQTemplate rocketMQTemplate; /** * RocketMQ配置 */ @Autowired private RocketmqCustomProperties rocketmqCustomProperties; static String DEFAULT_PASSWORD = "111111"; @Override @Transactional(rollbackFor = Exception.class) public Token unionLoginCallback(ConnectAuthUser authUser, String uuid) { return this.unionLoginCallback(authUser, false); } @Override public void bind(String unionId, String type) { AuthUser authUser = Objects.requireNonNull(UserContext.getCurrentUser()); Connect connect = new Connect(authUser.getId(), unionId, type); this.save(connect); } @Override @Transactional(rollbackFor = Exception.class) public void unbind(String type) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Connect::getUserId, UserContext.getCurrentUser().getId()); queryWrapper.eq(Connect::getUnionType, type); this.remove(queryWrapper); } @Override public List bindList() { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Connect::getUserId, UserContext.getCurrentUser().getId()); List connects = this.list(queryWrapper); List keys = new ArrayList<>(); connects.forEach(item -> keys.add(item.getUnionType())); return keys; } @Override @Transactional public Token miniProgramAutoLogin(WechatMPLoginParams params) { Map map = new HashMap<>(3); //得到微信小程序联合登陆信息 JSONObject json = this.getConnect(params.getCode()); //存储session key 后续登录用得到 String sessionKey = json.getStr("session_key"); String unionId = json.getStr("unionid"); String openId = json.getStr("openid"); map.put("sessionKey", sessionKey); map.put("unionId", unionId); map.put("openId", openId); //微信联合登陆参数 return phoneMpBindAndLogin(map.get("sessionKey"), params, map.get("openId"), map.get("unionId")); } /** * 通过微信返回等code 获取openid 等信息 * * @param code 微信code * @return 微信返回的信息 */ public JSONObject getConnect(String code) { WechatConnectSettingItem setting = getWechatMPSetting(); String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + setting.getAppId() + "&" + "secret=" + setting.getAppSecret() + "&" + "js_code=" + code + "&" + "grant_type=authorization_code"; String content = HttpUtils.doGet(url, "UTF-8", 100, 1000); log.error(content); return JSONUtil.parseObj(content); } /** * 手机号 绑定 且 自动登录 * * @param sessionKey 微信sessionKey * @param params 微信小程序自动登录参数 * @param openId 微信openid * @param unionId 微信unionid * @return token */ @Transactional(rollbackFor = Exception.class) public Token phoneMpBindAndLogin(String sessionKey, WechatMPLoginParams params, String openId, String unionId) { try { String encryptedData = params.getEncryptedData(); String iv = params.getIv(); JSONObject userInfo = this.getUserInfo(encryptedData, sessionKey, iv); log.info("联合登陆返回:{}", userInfo.toString()); ConnectAuthUser connectAuthUser = new ConnectAuthUser(); connectAuthUser.setUuid(openId); connectAuthUser.setNickname(params.getNickName()); connectAuthUser.setAvatar(params.getImage()); if (userInfo.containsKey("purePhoneNumber")) { String phone = (String) userInfo.get("purePhoneNumber"); connectAuthUser.setUsername("m" + phone); connectAuthUser.setPhone(phone); } else { connectAuthUser.setUsername(UuidUtils.getUUID()); } connectAuthUser.setSource(ConnectEnum.WECHAT); connectAuthUser.setType(ClientTypeEnum.WECHAT_MP); AuthToken authToken = new AuthToken(); authToken.setUnionId(unionId); connectAuthUser.setToken(authToken); return this.unionLoginCallback(connectAuthUser, true); } catch (Exception e) { e.printStackTrace(); } return null; } @Override public Connect queryConnect(ConnectQueryDTO connectQueryDTO) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(CharSequenceUtil.isNotEmpty(connectQueryDTO.getUserId()), Connect::getUserId, connectQueryDTO.getUserId()) .eq(CharSequenceUtil.isNotEmpty(connectQueryDTO.getUnionType()), Connect::getUnionType, connectQueryDTO.getUnionType()) .eq(CharSequenceUtil.isNotEmpty(connectQueryDTO.getUnionId()), Connect::getUnionId, connectQueryDTO.getUnionId()); return this.getOne(queryWrapper, false); } @Override public void deleteByMemberId(String userId) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Connect::getUserId, userId); this.remove(queryWrapper); } /** * 成功登录,则检测cookie中的信息,进行会员绑定 * * @param userId 用户ID * @param unionId 第三方用户ID * @param type 类型 */ @Override public void loginBindUser(String userId, String unionId, String type) { Connect connect = this.queryConnect( ConnectQueryDTO.builder().unionId(unionId).unionType(type).build() ); //如果未绑定则直接绑定 if (connect == null) { connect = new Connect(userId, unionId, type); this.save(connect); //如果已绑定不是当前用户信息则删除绑定信息,重新绑定 } else if (!connect.getUserId().equals(userId)) { this.removeById(connect.getId()); this.loginBindUser(userId, unionId, type); } } @Override public Result silentLogin(String code) { JSONObject res = this.getConnect(code); String unionId = res.getStr("unionid"); String openId = res.getStr("openid"); Member user = memberService.findByUUID(openId); // 不存在就注册 if (Objects.isNull(user)) { user = new Member(); user.setUuId(openId); user.setUsername(UuidUtils.getUUID()); user.setNickName("微信用户"); user.setPassword(DEFAULT_PASSWORD); user.setFace("https://i.loli.net/2020/11/19/LyN6JF7zZRskdIe.png"); memberService.registerHandler(user); } Token token = memberTokenGenerate.createToken(user, Boolean.TRUE); return Result.ok().data(token); } /** * 第三方联合登陆 * 1.判断是否使用开放平台 * 1.1如果使用开放平台则使用UnionId进行登录 * 1.2如果不适用开放平台则使用OpenId进行登录 *

* 2.用户登录后判断绑定OpenId * * @param authUser 第三方登录封装类 * @param longTerm 是否长时间有效 * @return token * @throws NoPermissionException 不允许操作 */ private Token unionLoginCallback(ConnectAuthUser authUser, boolean longTerm) { try { Member member = null; //判断是否传递手机号,如果传递手机号则使用手机号登录 if (StrUtil.isNotBlank(authUser.getPhone())) { member = memberService.findByMobile(authUser.getPhone()); } if (StrUtil.isNotBlank(authUser.getUuid())) { member = memberService.findByUUID(authUser.getUuid()); } //如果未查到手机号的会员则使用第三方登录 if (member == null) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); //使用UnionId登录 if (authUser.getToken() != null && StrUtil.isNotBlank(authUser.getToken().getUnionId())) { queryWrapper.eq(Connect::getUnionId, authUser.getToken().getUnionId()) .eq(Connect::getUnionType, authUser.getSource()); } else { //使用OpenID登录 SourceEnum sourceEnum = SourceEnum.getSourceEnum(authUser.getSource(), authUser.getType()); queryWrapper.eq(Connect::getUnionId, authUser.getUuid()) .eq(Connect::getUnionType, sourceEnum.name()); } //查询绑定关系 Connect connect = this.getOne(queryWrapper); if (connect == null) { member = memberService.autoRegister(authUser); } else { //查询会员 member = memberService.getById(connect.getUserId()); //如果未绑定会员,则把刚才查询到的联合登录表数据删除 if (member == null) { this.remove(queryWrapper); member = memberService.autoRegister(authUser); } } } //发送用户第三方登录消息 MemberConnectLoginMessage memberConnectLoginMessage = new MemberConnectLoginMessage(); memberConnectLoginMessage.setMember(member); memberConnectLoginMessage.setConnectAuthUser(authUser); String destination = rocketmqCustomProperties.getMemberTopic() + ":" + MemberTagsEnum.MEMBER_CONNECT_LOGIN.name(); //发送用户第三方登录消息 rocketMQTemplate.asyncSend(destination, JSONUtil.toJsonStr(memberConnectLoginMessage), RocketmqSendCallbackBuilder.commonCallback()); return memberTokenGenerate.createToken(member, longTerm); } catch (Exception e) { log.error("联合登陆失败:", e); throw e; } } /** * 获取微信小程序配置 * * @return 微信小程序配置 */ private WechatConnectSettingItem getWechatMPSetting() { Setting setting = settingService.get(SettingEnum.WECHAT_CONNECT.name()); WechatConnectSetting wechatConnectSetting = JSONUtil.toBean(setting.getSettingValue(), WechatConnectSetting.class); if (wechatConnectSetting == null) { throw new ServiceException(ResultCode.WECHAT_CONNECT_NOT_EXIST); } //寻找对应对微信小程序登录配置 for (WechatConnectSettingItem wechatConnectSettingItem : wechatConnectSetting.getWechatConnectSettingItems()) { if (wechatConnectSettingItem.getClientType().equals(ClientTypeEnum.WECHAT_MP.name())) { return wechatConnectSettingItem; } } throw new ServiceException(ResultCode.WECHAT_CONNECT_NOT_EXIST); } /** * 解密,获取微信信息 * * @param encryptedData 加密信息 * @param sessionKey 微信sessionKey * @param iv 微信揭秘参数 * @return 用户信息 */ public JSONObject getUserInfo(String encryptedData, String sessionKey, String iv) { log.info("encryptedData:{},sessionKey:{},iv:{}", encryptedData, sessionKey, iv); //被加密的数据 byte[] dataByte = Base64.getDecoder().decode(encryptedData); //加密秘钥 byte[] keyByte = Base64.getDecoder().decode(sessionKey); //偏移量 byte[] ivByte = Base64.getDecoder().decode(iv); try { //如果密钥不足16位,那么就补足. 这个if 中的内容很重要 int base = 16; if (keyByte.length % base != 0) { int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0); byte[] temp = new byte[groups * base]; Arrays.fill(temp, (byte) 0); System.arraycopy(keyByte, 0, temp, 0, keyByte.length); keyByte = temp; } //初始化 Security.addProvider(new BouncyCastleProvider()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES"); parameters.init(new IvParameterSpec(ivByte)); //初始化 cipher.init(Cipher.DECRYPT_MODE, spec, parameters); byte[] resultByte = cipher.doFinal(dataByte); if (null != resultByte && resultByte.length > 0) { String result = new String(resultByte, StandardCharsets.UTF_8); return JSONUtil.parseObj(result); } } catch (Exception e) { log.error("解密,获取微信信息错误", e); } throw new ServiceException(ResultCode.USER_CONNECT_ERROR); } }