共计 8243 个字符,预计需要花费 21 分钟才能阅读完成。
1. 角色权限模块
1.1 RBAC 概述
RBAC 通过定义角色的权限,并对用户授予某个角色从而来管制用户的权限,实现了用户和权限的逻辑拆散(区别于 ACL 模型),极大中央便了权限的治理
上面在解说之前,先介绍一些名词:
- User(用户):每个用户都有惟一的 UID 辨认,并被授予不同的角色
- Role(角色):不同角色具备不同的权限
- Permission(权限):拜访权限
- 用户 - 角色映射:用户和角色之间的映射关系
-
角色 - 权限映射:角色和权限之间的映射
1.2 以后零碎设计
权限零碎日益简单,需求方提出须要反对多种维度受权
如: 研发部的员工能够拜访 gitlab;java 开发工程师能够拜访跳板机; 杭州的员工能够看到亚运会信息;P6 级别以上能力看到公司利润报表。于是,零碎的受权也变得越来越简单,更有甚者,只有研发部部门的 leader 能够看到以后部门研发部成员的根本信息 …
多 tag 模型权限设计(tag 是反对受权的字段,维度也能够称之为 tag 标签)
因为通常是将某一类权限赋予给用户,故抽离出权限组的概念。权限组是若单个干权限的汇合
以后零碎:
查问权限的逻辑为
1. 依据 employeeId 查问 EmployeeRoleMap 表获取角色汇合 roleIds
select roleIds from EmployeeRoleMap where employeeId = ?
2. 查问 permission 表获取权限关联:(以后 tag 只有 RoleDimssionKey.ROLE)
select menuUID,menuGroupId from permission where value in [roleIds...] and key = RoleDimssionKey.ROLE
3. 若存在 menuGroupId(权限组 id), 则查问 menu_group_mapping(权限 - 权限组关联表)获取权限组关联的所有 menuUID
select menuUID,menuGroupId from menu_group_mapping where menuGroupId in [...]
4. 依据 menuUID 查问所有 Menu(若步骤 3 中存在 menuId,累计一起查问)
select * from menu_group_mapping where menuId in [...]
权限组相干逻辑为
权限组配置(经营平台)
商品 spu 绑定有 menuGroup 属性(长期解决方案,前期倡议剥离商品属性,间接绑定对应的 spu 和权限组)
用户购买商品付款胜利后,后盾逻辑会查问出以后 sku 绑定的菜单组,并增加到 permission(tag- 权限关联表)中
insert into permission (KEY=ROLEDIMISSION.ROLEID,value=?,MENUGROUPID=?DATA_BI_MENU_GROUP_ID?)
2.sku 商品价格计算
为了避免薅羊毛,0 元价格商品只能购买一次
2.1 新用户 NoneUpgradeSkuFilter
间接查问 sku 商品价格即可
2.2 降级账号数量 UpgradeAccountSkuFilter
锁定 时长 = 离以后套餐最近的时长 , 账号数量大于以后套餐的账号 的套餐
2.3 降级时长 UpgradeTimeSkuFilter
锁定 账号数量等于以后套餐的账号 的套餐
代码逻辑为
1. 购买时查问 organization_payment_detail 表,确定能够购买的类型。(购买胜利会更新 organization_payment_detail)
organization_payment 为空(新用户)前端显示购买按钮,
organization_payment(过期或已购买状态)前端显示降级时长、降级账号按钮
2. 前端发动查问 sku 申请并携带购买类型参数,后端依据购买类型确定 filter 来进行商品的过滤和价格计算(如 NoneUpgradeSkuFilter、UpgradeAccountSkuFilter、UpgradeTimeSkuFilter)
由对应的购买类型如 UpgradeAccountSkuFilter 负责商品的过滤及价格的计算
计算逻辑为 补差价(理论价格 = 应酬价格 - 差价)
套餐 A 1 个月 10 个 10 元
套餐 B 1 个月 20 个 20 元
套餐 C 1 个月 30 个 30 元
套餐 D 4 个月 10 个 40 元
套餐 E 5 个月 10 个 50 元
1. 路人甲用户 降级账号
case1 假如明天是 09-15 日,09-01 日购买套餐 A
则可降级套餐为 B\C
如 购买 B 套餐价格为(20/30(30-15))-10/30×15= 5 元 能够简化为须要补 15 天的差价(30-15)x(20/30-10/30)= 5 元,以后(套餐变为 09-15—->09-30 日 20 个账号)
2. 路人甲用户 降级时长
case1 假如明天是 09-15 日,09-01 日购买套餐 A
则可降级套餐为 D\E
购买 D 价格为 40 元 -10 元 /30 天 * 未应用天数 15 天 =(40-10/30×15)=35 元,以后 套餐变为 09-15—->09-15 后 4 个月 10 个账号
购买 E 价格为 50 元 -10 元 /30 天 * 未应用天数 15 天 =(50-10/30×15)=45 元,以后 套餐变为 09-15—->09-15 后 5 个月 10 个账号
3. AD 模块
3.1 AD 域控根底
AD 是 windows 计算机近程登录的账户管理中心,关上近程利用,会为每个数影用户调配独立的办公空间即创立 AD 账号。AD 账号创立是通过 java 调用 powershell 命令行实现的
3.2 连接池
模仿 C3P0 连接池、线程池等原理实现一个能够复用的 powershell 连接池
需要剖析:以后零碎 powershell 次要用于帮助 DDC 机器、AD 相干资源 CRUD 及其他辅助 powershell 命令。因为 AD 域控是连通的,且 powershell 能够近程运行。故咱们冀望部署在 DDC01 机器上的 agent 能够间接管制本机和 app01\addc01 上 powershell 的经营
入参:机器名、script 脚本
Powershell:近程执行、本地执行
IRecycle 可复用对象。id 作为惟一标示,reset 办法重置所有属性
ObjectPool 形象可复用资源池,应用 LinkedBlockingQueue 作为容器,避免多线程并发平安问题
DefaultRecyclePowerShellFactory powershell 连接池
+String getId();
+void reset(); 销毁以后 powershell session 上下文
Remove-Variable * -ErrorAction SilentlyContinue -Exclude @(...)
DefaultRecyclePowerShell powershell 可复用对象
4.websocket 模块
绝对于传统 HTTP 每次申请 - 应答都须要客户端与服务端建设连贯的模式,WebSocket 是相似 Socket 的 TCP 长连贯通信模式。WebSocket 连贯建设后,后续数据都以帧序列的模式传输。
握手阶段
a. 浏览器、服务器建设 TCP 连贯,三次握手。这是通信的根底,传输管制层,若失败后续都不执行。
b. TCP 连贯胜利后,浏览器通过 HTTP 协定向服务器传送 WebSocket 反对的版本号等信息。(开始前的 HTTP 握手)
c. 服务器收到客户端的握手申请后,同样采纳 HTTP 协定回馈数据。
d. 当收到了连贯胜利的音讯后,通过 TCP 通道进行传输通信。
客户端发送音讯:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
服务端返回音讯:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
技术选型:
原生 websocket
springboot websocket(轻量级,spring 集成,开发成本小)
Stomp(相似于 spring stream 音讯,springboot websocket 高级协定,前端须要应用 SOCKJS)
Netty SocketIO(轻量级,性能好,前端须要引入 socket.io.js)
spring websocket 次要组件
WebSocketConfigurer websocket 配置类:增加音讯处理器和握手拦截器
void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
如 registry.addHandler(agentWSHandler(), "/api/v1/websocket/dsAgent")
.setAllowedOrigins("*")
.addInterceptors(agentWSInterceptor);
TextWebSocketHandler 文本音讯处理器
void afterConnectionEstablished(WebSocketSession session)连贯建设胜利之后
void handleMessage(WebSocketSession session, WebSocketMessage<?> message) 收到客户端推送的音讯
void afterConnectionClosed(WebSocketSession session, CloseStatus status) 连贯断开之前
HandshakeInterceptor 握手拦截器
beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) 握手之前。能够做音讯的拦挡逻辑解决
websocketSession 多实例存在以下问题:
A websocket 连贯 app1 服务器,下次申请负载平衡连贯到了 app02 服务器。这个时候服务端须要推送 websocket 音讯
解决方案:形象出 WebsocketSender 专门负责 message 的发送。以后实现为 RocketMqMessageSender
先查看以后服务是否存在符合条件的 websocketSession, 若存在间接发送,若不存在发送到 rocketMq 中期待其余实例拉取生产。(留神死循环问题,不要始终发)
4.1 DSClient-StoreFront
WebsocketConfiguration 配置类配置了两条 websocket 通道:Dsclient 侧、前端侧
Dsclient-storeFront
DsclientHandler Dsclient 侧 websocket 音讯处理器 /api/v1/websocket/dsClient
ExtractParameterInterceptor 提取 request 中的参数并封装到 websocketSession 中
BinderIdCheckInterceptor 查看是否申请中具备 BinderId 参数
前端侧 -storeFront
WebClientHandler 前端侧 websocket 音讯处理器 /api/v1/websocket/webClient
AuthHttpSessionInterceptor 校验是否登录
WebClientHandler
INIT_BINDER_INFO 服务端返回 binderId 信息
REFRESH_APPLICATION_LIST 服务端转发 Dsagent 触发的 REFRESH_APPLICATION_LIST 工夫
OPEN_APPLICATION 关上利用,转发给 Dsagent
WEB_CLIENT_DIS_CONNECT 前端退出登录,转发给 Dsagent
DsClientHandler
PUSH_LATEST_APPLICATION_INFO 刚连贯时服务端会发送最新的本地利用列表
REPORT_LOCAL_APPLICATION_INFO 上报本地利用详情如装置进度,会触发 REFRESH_APPLICATION_LIST 事件
流程如下:
4.2 DSAgent-AgentManagerWeb
WSConfiguration websocket 配置类,配置 AgentWSHandler 和 AgentWSInterceptor
AgentWSHandler websocket 音讯处理器
AgentWSInterceptor 提取参数封装到 websocketSession 上下文中
Dsagent 侧 -storeFront
AgentWSInterceptor 负责 Dsagent 侧 websocket 握手。为了后续不再传递以后 session 的惟一标识信息,如 sessionId、machineSessionName 等,故在握手胜利时将这部分身份信息间接放入 websocketSession 中,相似 httpHeader 中的 cookie 标示
如
ws://localhost:9071/api/v1/websocket/dsAgent?machineName=machineName&machineSessionId=machineSessionId&userName=userName
AgentWSHandler 负责 Dsagent 音讯解决
MACHINE_SESSION_REPORT:Dsagent 上报会话利用信息
MACHINE_REPORT:Dsagent 上报 system0 机器信息
MACHINE_SESSION_LOGOUT: 经营平台下发。由服务端转发给 Dsagent
流程如下:
session 会话信息上报流程:(非 system0 用户)
1.DsAgent 每 15 秒全量上报以后 session 信息,即 MACHINE_SESSION_REPORT 事件
2. 服务端存储信息到 Redis,过期工夫为 20s
3. 经营平台前端查看会话治理,反对分页查问,含糊查问
4. 经营平台前端点击登记按钮,下发 MACHINE_SESSION_LOGOUT 给 DsAgent
5.Dsagent 收到 MACHINE_SESSION_LOGOUT,会话胜利登记。服务端 webs co ke t 断开清空 redis 中以后 session 会话信息
机器信息上报流程(system0 用户):DsAgent 每 15 秒上报机器信息(MACHINE_REPORT), 服务端存储音讯过期工夫为 20s
数据分页小工具:
redis 作为内存数据库 数据须要分页查问,依赖于 SimpleStringCache<T>。SimpleStringCache 会基于 @CacheIndex 注解构建索引 Map
例如:
@Data
@Accessors(chain = true)
static class A{
@CacheIndex
private String name;
@CacheIndex
private String id;
}
public static void main(String[] args) {List<A> list = new ArrayList();
A haha1 = new A().setName("haha").setId("51");
A haha2 = new A().setName("shiha").setId("761");
list.add(haha1);
list.add(haha2);
SimpleStringCache simpleStringCache = new SimpleStringCache(list);
List<Map> filter = new ArrayList<>();
Map map = new HashMap();
map.put("id", "1");
map.put("name", "sh");
filter.add(map);
simpleStringCache.query(filter).forEach(System.out::println);
}
simpleStringCache 会构建如下索引 Map 用于疾速定位
Originate:<0,haha1><1,haha2>
IndexMap:
<id,51,[0]><id,761,[1]>
<name,haha,[0]>,<name,shiha,[1]>
{
"id": [
{
"51": "0",
"761": "1"
}
],
"name": [
{
"haha": "0",
"shiha": "1"
}
]
}
查问时会根据传入的 List<Map> filter 进行含糊查问
[
{“id”: “1”,
"name",:"sh"
}
}]
如上述申请会命中 id 索引、name 索引,首先查问 IndexMap 依据 id= 1 含糊查问出【0,1】,依据 name=sh 含糊查问出【1】。and 关系故最终只命中【1】,最初后果去 originData 中查问最终 data 为 <1,haha2>
5. 拓展
5.1 散布式调度问题
目前我的项目中应用自定义 @DistributeTask 注解:通过分布式锁的形式简略躲避了高可用环境下任务调度的并发问题。APP01 执行调度时会应用 Redission 红锁创立一个分布式锁,工作执行完结后开释锁。APP02 工作来长期同样会获取这个分布式锁。
举荐 Xxx-job 解决分布式定时工作
5.2 外部服务鉴权问题
目前外部服务接口鉴权是依赖了公共模块 dsphere-rpc-auth
须要鉴权的外部服务如 dsphere-marketing-platform 须要依赖 dsphere-rpc-auth-service 模块,dsphere-rpc-auth-service 会通过 spring.factories 以 springboot starter 的形式注入一个 HandlerIntecptor, 该 HandlerIntecptor 会拦挡 url 合乎 /api/v1/auth/* 申请,确保申请头 header 中携带 AUTHORIZATION=xxx,否则校验失败。
5.3 remote debug
java 近程 debug 依赖
5.4 线上问题排查
arthas 反编译、动静批改并加载 clas 文件、jvm 调优及 gc 问题剖析
5.5 分布式自增序列 id
依赖于数据库 InnoDB 引擎行锁实现
@Component
@Slf4j
public class SequenceUtil {
@Autowired
private SequenceRepo sequenceRepo;
/**
* INNODB 引擎默认行锁,能够保障更改不产生失落(只存在以后一个原子性操作)* MVCC 机制 应用以后读 获取最新版本数据
* @param sequenceEnum
* @return
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer getId(SequenceEnum sequenceEnum) {sequenceRepo.incrementCounter(sequenceEnum.getPrimaryKeyId());
int counterByName = sequenceRepo.findCounterById(sequenceEnum.getPrimaryKeyId());
log.info("id"+counterByName);
return counterByName;
}
}
public interface SequenceRepo extends CrudRepository<Sequence, Integer> {@Query(value = "update sequence set counter = counter + 1 where id = (:id)", nativeQuery = true)
@Modifying
@Transactional
int incrementCounter(@Param("id")Integer id);
@Query(value = "select counter from sequence where id = (:id)", nativeQuery = true)
int findCounterById(@Param("id")Integer id);
}