Skip to content

Commit

Permalink
feat: 以桥接模式和适配器模式两种方式各实现第三方 Oauth(github) 登录 [demo建设] (#57)
Browse files Browse the repository at this point in the history
* feat: 以桥接和适配器两种方式实现第三方 Oauth(github) 登录 [demo]
  • Loading branch information
BanTanger committed Nov 4, 2023
1 parent 684c443 commit d15f1b4
Show file tree
Hide file tree
Showing 88 changed files with 2,327 additions and 23 deletions.
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# IM-WhaleShark
<div align="center">

![IM-WhaleShark](assert/design/IM-WhaleShark.png)
![IM-WhaleShark](assert/design/photo/IM-WhaleShark.png)

IM-WhaleShark(鲸鲨)是基于 Netty 实现的高性能分布式 IM 即时通讯系统

Expand Down Expand Up @@ -169,11 +169,11 @@ Docker 部署测试请访问 `localhost:19000`

浏览方式通过 F12 查看服务端发送的 `json` 格式是否正确

![](assert/design/websocket窗口功能讲解.png)
![](assert/design/photo/websocket窗口功能讲解.png)

如图所示: 平台 [appId = 10001] 的用户 [userId = 10001, clientType = 3, imei = 200] 在登录 Login 之后向群组 [groupId = 27a35ff2f9be4cc9a8d3db1ad3322804] 通过操作指令`群发模式`[command = 2104] 发送一条群组消息

![websocket功能测试](assert/design/websocket功能测试.png)
![websocket功能测试](assert/design/photo/websocket功能测试.png)

## 架构设计
### 私有协议
Expand Down Expand Up @@ -211,7 +211,7 @@ IM 的私有协议确立信息如下:


### 消息投递过程
![一条消息的流转](assert/design/消息流转.png)
![一条消息的流转](assert/design/photo/消息流转.png)

流程如下:
1. 客户端 userA 发送一条消息到服务器, 消息通过私有协议转化为二进制序列化, 通过 TCP 三次握手保证消息在传输层的稳定性(上下行 ACK 保证消息在应用层的稳定性 )
Expand All @@ -228,7 +228,7 @@ IM 的私有协议确立信息如下:
当然, 也可采用 TCP、UDP 连接甚至是 HTTP 短连接也行,只不过这样会需要更多的设计,需要考虑用户弱网行为,后续设计中我会添加

### 路由层
![分布式路由层](assert/design/分布式路由层.png)
![分布式路由层](assert/design/photo/分布式路由层.png)

由于使用了分布式, 用户的信息会因为负载均衡分布在不同的服务器上,怎么保证多 Channel 的跨节点通讯就显得额外的重要。

Expand All @@ -254,7 +254,7 @@ IM 的私有协议确立信息如下:
### 读写扩散模型
#### 写扩散
![写扩散](assert/design/写扩散.png)
![写扩散](assert/design/photo/写扩散.png)
+ 在架构中, 单聊会话消息采用写扩散

写扩散优缺:
Expand All @@ -272,7 +272,7 @@ IM 的私有协议确立信息如下:
+ 先写扩散后读,实时性差。

#### 读扩散
![读扩散](assert/design/读扩散.png)
![读扩散](assert/design/photo/读扩散.png)
+ 在架构中, 群聊会话消息采用读扩散

读扩散优缺:
Expand All @@ -292,21 +292,21 @@ IM 的私有协议确立信息如下:
#### 多端消息同步的弊端:
由于 WhaleShark 实现了用户多端同步,因此需要保证一条消息既同步给发送方的其他端,又得保证消息能发送给目标对象的所有端。一条消息的处理流程如下:

![多端消息同步的弊端](assert/design/多端消息同步的弊端.png)
![多端消息同步的弊端](assert/design/photo/多端消息同步的弊端.png)

如架构图所演示,一条消息如图所示就裂变成三条消息了,如果说端的类型更多 (设计上是有六种端: Windows、Mac、Web、Android、IOS、WebApi) 但实际上我们基本是通过 WebApi 来接收消息, 再同步给其他端,也就需要裂变出 `5 * 5 - 1 = 24` 条消息

一口气发送如此多条消息对于服务器来说,压力是巨大的,因此我们需要重新设计一些策略来实现消息同步

#### 多端消息同步改进:
![多端消息同步改进](assert/design/多端消息同步改进.png)
![多端消息同步改进](assert/design/photo/多端消息同步改进.png)
1. 发送方 userA 发送消息给服务端
2. 服务端接收发送方的消息之后向发送方回应消息接收确认 ACK 数据包表示服务端已经成功接收消息
3. 先将消息同步给发送方其他端(在线端使用 TCP 通知投递,离线端存储最新的 1000 条数据到离线消息队列里)
4. 发送消息给接收方所有端

#### 群聊消息同步流程:
![群聊消息同步流程](assert/design/群聊消息同步流程.png)
![群聊消息同步流程](assert/design/photo/群聊消息同步流程.png)
1. 发送方 userA 发送消息给服务端
2. 服务端接收发送方的消息之后向发送方回应消息接收确认 ACK 数据包表示服务端已经成功接收消息
3. 先将消息同步给发送方其他端(在线端使用 TCP 通知投递,离线端存储最新的 1000 条数据到离线消息队列里)
Expand All @@ -317,7 +317,7 @@ IM 的私有协议确立信息如下:
### 消息可靠传达模型
我们难以保证消息全都可靠传达,不会产生丢失现象,在 IM 系统中也不允许丢失一条消息。如下图:

![有了TCP为什么还要保证可靠性传达](assert/design/有了TCP为什么还要保证可靠性传达.png)
![有了TCP为什么还要保证可靠性传达](assert/design/photo/有了TCP为什么还要保证可靠性传达.png)

+ 在传输层,TCP的三次握手保证了双方通讯的可靠性,稳定性。简而言之,用户发送的消息,
在忽视应用层的情况下,无论如何都会从自身主机的 “发送缓冲区” 抵达对方主机的 “接收缓冲区”
Expand All @@ -327,7 +327,7 @@ IM 的私有协议确立信息如下:
> 如果只是单台机器进行双向通信,则不会经历传输层拆包装包的过程,而是直接将数据包通过内核拷贝到另一个进程进行通讯
在设计上,我们采用应用层两次握手(上下行 ACK)来保证消息在应用层的可靠传达
![上下行ACK](assert/design/上下行ACK.png)
![上下行ACK](assert/design/photo/上下行ACK.png)
+ 上行 ACK:服务端发送给消息发送方的接收确认 ACK
+ 下行 ACK:目标用户发送给消息发送方的接收确认 ACK

Expand All @@ -345,10 +345,10 @@ ps: 上行 ACK 也同理, 服务端的消息发送实际抵达 MQ 时有一个
RecvID, ServerID, ClientID, SendTime 做冗余避免查库提升性能

#### 在线用户消息接收
![在线用户消息确认](assert/design/在线用户消息确认.png)
![在线用户消息确认](assert/design/photo/在线用户消息确认.png)

#### 离线用户消息接收
![离线用户消息确认](assert/design/离线用户消息确认.png)
![离线用户消息确认](assert/design/photo/离线用户消息确认.png)

#### ACK 丢失现象
下面分别探讨上下行 ACK 丢失现象的处理流程
Expand All @@ -369,13 +369,13 @@ ACK 丢失现象解决策略: 由于我们 ack 中含有 msgId, 可以在客户

或者是消息到达时做一个缓存,缓存时间尽量短,缓存时间内的消息重试直接让接收方接收消息,不进行二次持久化。

![消息幂等性保证](assert/design/消息幂等性保证.png)
![消息幂等性保证](assert/design/photo/消息幂等性保证.png)

为了避免客户端无限制重发的现象,我们可以对缓存做一个过期时间,只有在过期时间之前的缓存才能做幂等;
当超过缓存时间时, 服务端忽略重投的消息, 直到客户端计时器超时并且已经超过了最大重试次数,
才让客户端重新生成消息唯一id:messageId, 也就是重新做一个消息体

![禁止客户端无限制重发](assert/design/禁止客户端无限制重发.png)
![禁止客户端无限制重发](assert/design/photo/禁止客户端无限制重发.png)

### 消息有序性保证
为了提高消息在服务端的处理流程(MQ消费,落库存储,ACK确认),我们在程序实现中采用了线程池技术来提高消息处理时长
Expand Down Expand Up @@ -412,12 +412,12 @@ ACK 丢失现象解决策略: 由于我们 ack 中含有 msgId, 可以在客户
## 前后端对接
IM 服务采用 SDK 方式集成到前端代码。一个大致的流程演示如下:

![SDK执行流程](assert/design/SDK执行流程.png)
![SDK执行流程](assert/design/photo/SDK执行流程.png)
对此我已经大致实现了后端的一个 im-app-server 与前端的 SDK 进行对接,欢迎前端同学与我一起来完善 WhaleShark

## 联系
如果有什么不懂的设计, 可通过提 ISSUE 或者是加我微信进行探讨

欢迎与我联系交流,我拉你进交流群,微信二维码为(注明来意喔~):

![与我联系.png](assert/design/与我联系.png)
![与我联系.png](assert/design/photo/与我联系.png)
63 changes: 63 additions & 0 deletions assert/design/design_pattern/adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 适配器模式(Adapter)说明

适配器:意在将一个类的接口适配成用户所需的接口,它能帮助不兼容的接口变得兼容,宏观做法是将用户自定义的接口包裹在想要适配的接口里,就好比苹果的数据线..

适配器模式有三个角色

+ Target: 目标角色, 在 im-register-login-demo#adapter#Login3rdTarget, 是暴露给用户的接口, 根据设计模式六大原则之迪米特法则,一个类最好只暴露实现方法,而不暴露具体细节
+ Adaptee: 被适配角色, 在 im-register-login-demo#adapter#UserService,适配器将继承 UserService 类以达到扩展新功能而不改动原有类的需求,这是设计模式六大原则的开闭原则,即对修改关闭,对扩展开放
+ Adapter: 适配器角色, 在 im-register-login-demo#adapter#Login3rdAdapter,他将扩展出第三方登陆的核心逻辑方法,并且还具有 UserService 已实现的查询数据库是否有账号和注册逻辑

适配器根据适配的对象不同,可分为对象适配器和类适配器
+ 前者适配器关联一个包裹它的类实例
+ 后者适配器继承被适配的类对象(一般采用这种方式)

对象适配器的一种实现方式:

```java
@Component
public class Login3rdAdapter {
@Resource
private UserService userService;
// ...
}
```

类适配器的一种实现方式:

```java
@Component
public class Login3rdAdapter extends UserService {

}
```

Target 是接口,自然需要子类真正实现,在这里子类自然是 Adapter
不难写出这样的代码

```java
public class Login3rdAdapter extends UserService implements Login3rdTarget {

public Login3rdAdapter(UserRepository userRepository) {
super(userRepository);
}

@Override
public String loginByGithub(String code, String state) {
return null;
}

@Override
public String loginByWechat() {
return null;
}

@Override
public String loginByQQ() {
return null;
}
}
```
+ 继承 UserService,以实现不侵入原有方法前提下进行第三方登录的扩展


61 changes: 61 additions & 0 deletions assert/design/design_pattern/login3rd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 第三方登录的实现以及使用说明

## 使用说明

+ im-design-demo/im-register-login-demo/login-design-adapter-demo
+ im-design-demo/im-register-login-demo/login-design-bridge-demo

这两个子模块都对接了第三方登录的功能

体验步骤:
1. 运行项目
2. 点击命令行里出现的蓝链 进行权限校验

## Github Oauth 第三方登录实现
参考这篇文章:

[官方文档](https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)

[github第三方登录超详细流程及分析(小白笔记)](https://blog.csdn.net/qq_43516238/article/details/105884926)

[GitHub OAuth 第三方登录示例教程 - 阮一峰的网络日志](https://ruanyifeng.com/blog/2019/04/github-oauth.html)

```yml
client_id: cf00a9382ce8110c2a70
client_secret: fd348b2050f64c7a99c07294b390a5adfaa21e8c
redirect_uri: http://localhost:21001/github
```

yml 配置
```yml
github:
state: GITHUB
user_prefix: ${github.state}@

# ========= 自己申请 client_id\secret 用完之后记得删除 =========
client_id: cf00a9382ce8110c2a70
client_secret: fd348b2050f64c7a99c07294b390a5adfaa21e8c
# ==========================================================

callback: http://localhost:21001/github # github 回调 callback 会携带 code 参数
token_url: https://github.com/login/oauth/access_token?client_id=${github.client_id}&client_secret=${github.client_secret}&redirect_uri=${github.callback}&code= # 拼接 code
user_url: https://api.github.com/user # 使用访问令牌访问 API
```

![img.png](../../../im-design-demo/im-register-login-demo/login-design-adapter-demo/assert/img.png)

权限访问:

https://github.com/login/oauth/authorize?client_id=cf00a9382ce8110c2a70&redirect_uri=http://localhost:21001/github&state=GITHUB

可能会出现超时的情况

![timeout.png](../../../im-design-demo/im-register-login-demo/login-design-adapter-demo/assert/timeout.png)

成功

![success.png](../../../im-design-demo/im-register-login-demo/login-design-adapter-demo/assert/success.png)

数据库存在该数据

![数据库存有github账号.png](../../../im-design-demo/im-register-login-demo/login-design-adapter-demo/assert/数据库存有github账号.png)
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
13 changes: 13 additions & 0 deletions assert/sql/register_login_demo.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE DATABASE IF NOT EXISTS im_register_login_demo;
USE im_register_login_demo;

CREATE TABLE user (
id BIGINT AUTO_INCREMENT
PRIMARY KEY,
username VARCHAR(64),
password VARCHAR(64),
create_time DATE,
user_email VARCHAR(64)
);

INSERT INTO user (id, username, password, create_time, user_email) VALUES ('10001', 'admin', 'admin', now(), '[email protected]')
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ services:
timeout: 5s
retries: 3
ports:
- "3306:3306"
- "13306:3306"
networks:
- im-network

Expand Down
6 changes: 3 additions & 3 deletions docker/mysql/conf/my.cnf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[mysqld]
# 设置3306端口
port=3306
# 设置13306端口
port=13306

# 设置mysql的安装目录
basedir=/usr/local/mysql
Expand Down Expand Up @@ -47,5 +47,5 @@ default-character-set=utf8mb4

[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
port=13306
default-character-set=utf8mb4
13 changes: 13 additions & 0 deletions docker/mysql/db/register_login_demo.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE DATABASE IF NOT EXISTS im_register_login_demo;
USE im_register_login_demo;

CREATE TABLE user (
id BIGINT AUTO_INCREMENT
PRIMARY KEY,
username VARCHAR(64),
password VARCHAR(64),
create_time DATE,
user_email VARCHAR(64)
);

INSERT INTO user (id, username, password, create_time, user_email) VALUES ('10001', 'admin', 'admin', now(), '[email protected]')
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 适配器模式(Adapter)说明

适配器:意在将一个类的接口适配成用户所需的接口,它能帮助不兼容的接口变得兼容,宏观做法是将用户自定义的接口包裹在想要适配的接口里,就好比苹果的数据线..

适配器模式有三个角色

+ Target: 目标角色, 在 im-register-login-demo#adapter#Login3rdTarget, 是暴露给用户的接口, 根据设计模式六大原则之迪米特法则,一个类最好只暴露实现方法,而不暴露具体细节
+ Adaptee: 被适配角色, 在 im-register-login-demo#adapter#UserService,适配器将继承 UserService 类以达到扩展新功能而不改动原有类的需求,这是设计模式六大原则的开闭原则,即对修改关闭,对扩展开放
+ Adapter: 适配器角色, 在 im-register-login-demo#adapter#Login3rdAdapter,他将扩展出第三方登陆的核心逻辑方法,并且还具有 UserService 已实现的查询数据库是否有账号和注册逻辑

适配器根据适配的对象不同,可分为对象适配器和类适配器
+ 前者适配器关联一个包裹它的类实例
+ 后者适配器继承被适配的类对象(一般采用这种方式)

对象适配器的一种实现方式:

```java
@Component
public class Login3rdAdapter {
@Resource
private UserService userService;
// ...
}
```

类适配器的一种实现方式:

```java
@Component
public class Login3rdAdapter extends UserService {

}
```

Target 是接口,自然需要子类真正实现,在这里子类自然是 Adapter
不难写出这样的代码

```java
public class Login3rdAdapter extends UserService implements Login3rdTarget {

public Login3rdAdapter(UserRepository userRepository) {
super(userRepository);
}

@Override
public String loginByGithub(String code, String state) {
return null;
}

@Override
public String loginByWechat() {
return null;
}

@Override
public String loginByQQ() {
return null;
}
}
```
+ 继承 UserService,以实现不侵入原有方法前提下进行第三方登录的扩展

适配器的好处在于不修改原有逻辑就能实现扩展与替换,但如果需要扩展的子类过多,例如 demo 里所展示的第三方账号越来越多,手机短信验证码、CSDN账号、Gitee、twitter 等等,可能会导致适配器适配的种类越来越多
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit d15f1b4

Please sign in to comment.