バックエンドのみが関係します。すべてのディレクトリ、コード、ドキュメント、およびインターフェイス パスについては、一番上の列を参照してください。
[リリショップモール] B2B2Cモールシステムの学習ノートを記録〜
記事全体では、インターフェースクラスとビジネスクラスを含む設計ロジックに焦点を当てたビジネス紹介と、特定のソースコード分析を組み合わせます。ソースコードは複雑ではありません〜
注意: ソースコード内のコメントには、間違っているもの、まったく逆の意味のコメント、正しくないコメントがあります. 読み取り過程で更新し、わからないところに新しいコメントを追加しました. ソースコードを読むときは注意してください. !
目次
1. エンティティークラスはアカウントをそれぞれ openid と unionid に接続してバインドします
2. システム内の WeChat アプレットの構成情報を取得する
3.キャッシュから WeChat ユーザーを取得するためのキーとして、ヘッダーの uuid が使用されます
A1.会員ログインモジュール
B1.Wechatアプレットのログインインターフェース開発
WeChat アプレット ログイン用のインターフェイスは 1 つだけであり、パラメーターを渡す必要があります: 一時的なログイン資格情報コード、ターゲット暗号文の encryptedData、携帯電話番号の対称復号化のデータ、携帯電話番号の対称復号化アルゴリズムの初期ベクトル iv、WeChat アバター、および WeChatユーザーのニックネーム。ヘッダーで uuid を渡します
ヘッダーの uuid:今回のユーザー ログインの uuid。これは、インターフェイスへの複数の呼び出しによって引き起こされるサードパーティ インターフェイスへの複数の呼び出しを防ぐために使用されます。フロントエンドは、バックエンドの戻り値に応じて uuid を更新するかどうかを判断する必要があります。
一時的なログイン資格情報コード:ユーザーの一意の識別子 OpenID、WeChat オープン プラットフォーム アカウント (現在のミニ プログラムが WeChat オープン プラットフォーム アカウントにバインドされている場合) でのユーザーの一意の識別子 UnionID、およびセッション キー session_key を取得するために使用されます。取得後、アカウントの復号化とバインドに使用されます. 参照:ミニ プログラム ログイン | WeChat オープン ドキュメント
携帯電話番号の対称復号化のターゲット暗号文 encryptedData、携帯電話番号の対称復号化アルゴリズムの初期ベクトル iv : ユーザーの携帯電話番号を取得するために使用され、アカウント番号または登録を取得するために使用されます。ここでは古い方法が使用されています。詳細については、以前のNo4-1 C1.WeChat アプレットのドキュメントを参照してください。
WeChat アバター、WeChat ユーザーのニックネーム: 取得後にアカウント登録に使用、wx.getUserProfile(Object object) | WeChat ドキュメントを開く
インターフェイスはパラメータを受け取った後、アカウントを取得できるかどうかを判断し、取得できた場合はアカウント ログイン用の Token を直接返し、取得できなかった場合はアカウントを登録してアカウント ログイン用の Token を返します。
ビジネスの論理:
ビジネスロジックを導入する場合は、他のコード構造が関係します. 説明が必要な場合は、緑色の網掛け でマークし、次のコードロジックで詳しく紹介します.
この時点で、新しいエンティティ クラス Connectを追加する必要があります。これは、ログイン関連付けテーブル li_connect と組み合わされ、アカウントをサードパーティ ユーザーの一意の識別子にバインドするために使用されます。
ConnectServiceImpl クラス: エンティティ クラス li_connect には mybatis-plus のみが使用され、カスタム マッパーは使用されません
- まずヘッダーのuuidをキーにしてWeChatのユーザー情報をキャッシュから取得し、取得できれば2のみ、取得できなければシステム内のWeChatアプレットの構成情報を取得し、次に、フロント エンドから渡された一時的なログイン資格情報コードを結合します。入力パラメーターとして、WeChat が提供する取得インターフェイスを呼び出し、応答から WeChat アプレット ユーザーの openid、unionid、および session_key の基本情報を取得します。そして、この情報をヘッダーのuuidをキーとしてキャッシュに保存します
- session_key、携帯電話番号の対称復号化のターゲット暗号文 encryptedData、および携帯電話番号の対称復号化アルゴリズムの初期ベクトル iv に従って復号化し、ユーザーの携帯電話番号を取得します。【復号ロジックはWeChat提供~No4-1で解説】
- 携帯電話番号にバインドされたアカウントを照会します。メンバーが存在する場合は、それぞれ openid と unionid を介してアカウントをバインドし、それを li_connect テーブルに格納して、アカウントのログイン トークンを返します。アカウントが存在しない場合は 4.
- メンバーがいない場合は、携帯電話番号、WeChat アバター、および WeChat ユーザーのニックネームに従ってメンバーを登録します. ユーザー名も携帯電話番号と連結され、それぞれ openid と unionid を介してアカウントにバインドされ、. li_connect テーブル. これは登録されたアカウントであるため、メンバー登録の小さなイベントを処理することもできます [プラットフォーム登録と同じ、説明はもうありません]。
コード ロジック:
//cn.lili.controller.passport.connect.MiniProgramBuyerController
@RestController
@RequestMapping("/buyer/passport/connect/miniProgram")
@Api(tags = "买家端,小程序登录接口")
public class MiniProgramBuyerController {
@Autowired
public ConnectService connectService;
@GetMapping("/auto-login")
@ApiOperation(value = "小程序登录/自动注册")
public ResultMessage<Token> autoLogin(@RequestHeader String uuid, WechatMPLoginParams params) {
params.setUuid(uuid);
return ResultUtil.data(this.connectService.miniProgramAutoLogin(params));
}
。。。
}
//cn.lili.modules.connect.serviceimpl.ConnectServiceImpl
@Slf4j
@Service
public class ConnectServiceImpl extends ServiceImpl<ConnectMapper, Connect> implements ConnectService {
@Autowired
private SettingService settingService;
@Autowired
private MemberService memberService;
@Autowired
private MemberTokenGenerate memberTokenGenerate;
@Autowired
private Cache cache;
/**
* RocketMQ 配置
*/
@Autowired
private RocketmqCustomProperties rocketmqCustomProperties;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
@Transactional
public Token miniProgramAutoLogin(WechatMPLoginParams params) {
Object cacheData = cache.get(CachePrefix.WECHAT_SESSION_PARAMS.getPrefix() + params.getUuid());
Map<String, String> map = new HashMap<>(3);
if (cacheData == null) {
//通过前端传入的微信返回的登录凭证code ,去换取微信小程序端用户基本信息
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);
cache.put(CachePrefix.WECHAT_SESSION_PARAMS.getPrefix() + params.getUuid(), map, 900L);
} else {
map = (Map<String, String>) cacheData;
}
//手机号 绑定 且 自动登录
return this.phoneMpBindAndLogin(map.get("sessionKey"), params, map.get("openId"), map.get("unionId"));
}
/**
* 通过微信返回等code 获取openid 等信息
*
* @param code 微信code
* @return 微信返回的信息
*/
public JSONObject getConnect(String code) {
//获取系统里的微信小程序配置
WechatConnectSettingItem setting = this.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) {
String encryptedData = params.getEncryptedData();
String iv = params.getIv();
//使用旧版本解密方式,获取微信信息
JSONObject userInfo = this.getUserInfo(encryptedData, sessionKey, iv);
log.info("联合登陆返回:{}", userInfo.toString());
//拿到手机号码
String phone = (String) userInfo.get("purePhoneNumber");
//手机号登录
LambdaQueryWrapper<Member> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Member::getMobile, phone);
//查询手机号码绑定的账号
Member member = memberService.getOne(lambdaQueryWrapper);
//如果存在会员,则进行绑定微信openid 和 unionid,并且登录
if (member != null) {
//会员绑定 绑定微信小程序
this.bindMpMember(openId, unionId, member);
return memberTokenGenerate.createToken(member, true);
}
//如果没有会员,则根据手机号注册会员
Member newMember = new Member("m" + phone, "111111", phone, params.getNickName(), params.getImage());
memberService.save(newMember);
newMember = memberService.findByUsername(newMember.getUsername());
//会员绑定 绑定微信小程序
this.bindMpMember(openId, unionId, newMember);
//发送会员注册信息
applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("new member register", rocketmqCustomProperties.getMemberTopic(), MemberTagsEnum.MEMBER_REGISTER.name(), newMember));
return memberTokenGenerate.createToken(newMember, true);
}
/**
* 解密,获取微信信息,此类的逻辑是根据微信的加密数据解密算法逻辑开发的,具体见:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
* 此方法是旧版本~~~详见:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/deprecatedGetPhoneNumber.html
* 新版本是拿到code后调用HTTPS接口来获取手机号码~~~这个就很简单就是用 HttpUtils 调用接口获取响应就可以了~
* 详见:
* 获取手机号码组件:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
* 获取手机号码接口:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html
*
* @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", "BC");
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);
}
/**
* 会员绑定 绑定微信小程序
* <p>
* 如果openid 已经绑定其他账号,则这里不作处理,如果未绑定,则绑定最新的会员
* 这样,微信小程序注册之后,其他app 公众号页面,都可以实现绑定自动登录功能
* </p>
*
* @param openId 微信openid
* @param unionId 微信unionid
* @param member 会员
*/
private void bindMpMember(String openId, String unionId, Member member) {
//如果 unionid 不为空 则为账号绑定unionid
if (CharSequenceUtil.isNotEmpty(unionId)) {
LambdaQueryWrapper<Connect> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.eq(Connect::getUnionId, unionId);
lambdaQueryWrapper.eq(Connect::getUnionType, ConnectEnum.WECHAT.name());
List<Connect> connects = this.list(lambdaQueryWrapper);
//只有为绑定过的才会绑定,已绑定过的不会再次绑定!!!!
if (connects.isEmpty()) {
Connect connect = new Connect();
connect.setUnionId(unionId);
connect.setUserId(member.getId());
connect.setUnionType(ConnectEnum.WECHAT.name());
this.save(connect);
}
}//如果 openid 不为空 则为账号绑定openid
if (CharSequenceUtil.isNotEmpty(openId)) {
LambdaQueryWrapper<Connect> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Connect::getUnionId, openId);
lambdaQueryWrapper.eq(Connect::getUnionType, ConnectEnum.WECHAT_MP_OPEN_ID.name());
List<Connect> connects = this.list(lambdaQueryWrapper);
//只有为绑定过的才会绑定,已绑定过的不会再次绑定!!!!
if (connects.isEmpty()) {
Connect connect = new Connect();
connect.setUnionId(openId);
connect.setUserId(member.getId());
connect.setUnionType(ConnectEnum.WECHAT_MP_OPEN_ID.name());
this.save(connect);
}
}
}
/**
* 获取微信小程序配置
*
* @return 微信小程序配置
*/
private WechatConnectSettingItem getWechatMPSetting() {
//从数据库中拿到微信 联合登陆设置
Setting setting = settingService.get(SettingEnum.WECHAT_CONNECT.name());
//然后将设置转化成使用JavaBean对象。此对象里面是用list存放多种微信应用设置的
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);
}
。。。
}
1.エンティティ クラスは、 それぞれ openid と unionid を介してアカウントを接続 および バインドします。
最初にエンティティ クラス Connect について話しましょう. 次の 3 つのフィールドが重要です. 各共同ログイン関連付けテーブルは、アカウント ID、共同 (サードパーティ) ユーザー ID、および共同 (サードパーティ) タイプに関連付けられます. 共同ユーザー ID がアカウントに関連付けられていない限り、関連付けが追加されます。
ユニオンの種類には、WeChat の各製品のさまざまな openid (ミニプログラム、モバイル アプリケーション、および Web サイト アプリケーション)、WeChat ユニオン ID、QQ の各製品、Weibo (Weibo に複数のアプリケーションがあるかどうかはわかりません。まだ使っていない)、アリペイなど、第三者を理解してから細分化する必要があります。
同じユーザー ID が複数の共同ユーザー ID にバインドされる場合があります。
ただし、一般的に言えば、第三者を使用してログインを承認する場合、実際のユーザーは同じアカウントに関連付けられる必要がありますが、ショップではそうではないため、注意が必要です~~~ ~【どうして?会員ログイン方法をすべてお読みいただき、ご理解ください】
@ApiModelProperty("用户id")
private String userId;
@ApiModelProperty("联合登录id")
private String unionId;
/**
* @see cn.lili.modules.connect.entity.enums.ConnectEnum
*/
@ApiModelProperty(value = "联合登录类型")
private String unionType;
したがって、サードパーティのユーザー ID をプラットフォーム アカウントにバインドする、それぞれ openid と unionid を介してアカウントをバインドするロジックを説明する必要はありません。
バインドは、最初に共同ユーザー ID が既に存在するかどうか、つまり、アカウントがバインドされているかどうかを判断することでもあり、そうでない場合はバインドされます。
2.システム内の WeChat アプレットの構成情報を取得する
構成情報には、appId、appSecret、および clientType が含まれます.これらは WeChat に適用する必要があります.承認後、WeChat が発行します.各製品には独自の一意性があります.たとえば、ショップ プロジェクトでは、WeChat アプレットと WeChat Web サイト アプリケーションは両方とも異なっています!
この情報はプラットフォームに属しているため、運用側に渡して管理することができます.これらは小さなデータであるため、単一のデータ テーブルを開く価値がないため、運用 M 側のシステム設定ロジックを使用して、. K:Vに従って保存し、Vにjson型で保存します。
使用する際は、clientType によってどの製品であるかを判別します。
//WECHAT_CONNECT 的
{
"wechatConnectSettingItems":[
{
"clientType":"PC",
"appId":"XXXXXXX",
"appSecret":"XXXXXXX"
},
{
"clientType":"H5",
"appId":"XXXXXXX",
"appSecret":"XXXXXXX"
},
{
"clientType":"WECHAT_MP",
"appId":"XXXXXXX",
"appSecret":"XXXXXXX"
}
]
}
3.ヘッダーの uuid は、キャッシュから WeChat ユーザーを取得するためのキーとして使用されます
実際にはキャッシングなしでも問題ありません. ここでキャッシングを追加するのは、 this.getConnect(params.getCode()); メソッドを何度も呼び出さないようにするためです. このメソッドは最終的に WeChat が提供するインターフェースを呼び出します. そしてWeChatのインターフェースがよく使われています!! ! 限界があります!! ! 他のエラーが原因で何度も電話をかけられないユーザーを回避します。
逆に小さいプログラムの呼び出し回数が多すぎるとパフォーマンスが向上します. 結局同じユーザーが取得した情報は同じなので最初に格納されます. 頻繁な呼び出しが多い場合はキャッシュ内の情報が使用されます。
これはもっと注意を払うことです。フロントエンドはバックエンドの応答に従って uuid を更新またはクリアする必要があります。そうしないと、ロジックは役に立ちません~~~
ログイン認証の応答が失敗した場合、uuid をクリアする必要はありません。ログインが成功した場合、uuid をクリアできます。