部分研发设计文档

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=https://www.it610.com/article/?,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/30x15=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/30x15)=35元,当前套餐变为09-15---->09-15后4个月 10个账号
购买E价格为 50元-10元/30天*未使用天数15天=(50-10/30x15)=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 attributes) 握手之前。可以做消息的拦截逻辑处理

websocketSession多实例存在以下问题:
A websocket连接app1服务器,下次请求负载均衡连接到了app02服务器。这个时候服务端需要推送websocket消息
解决方案:抽象出WebsocketSender专门负责message的发送。当前实现为RocketMqMessageSender
先检查当前服务是否存在符合条件的websocketSession,若存在直接发送,若不存在发送到rocketMq中等待其他实例拉取消费。(注意死循环问题,不要一直发)
4.1 DSClient-StoreFront
WebsocketConfiguration配置类配置了两条websocket通道:Dsclient侧、前端侧Dsclient-storeFront DsclientHandlerDsclient侧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=userNameAgentWSHandler 负责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。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 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 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", "761": "1" } ], "name": [ { "haha": "0", "shiha": "1" } ] }

查询时会依据传入的List 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 {@Query(value = "https://www.it610.com/article/update sequence set counter = counter + 1 where id = (:id)", nativeQuery = true) @Modifying @Transactional int incrementCounter(@Param("id")Integer id); @Query(value = "https://www.it610.com/article/select counter from sequence where id = (:id)", nativeQuery = true) int findCounterById(@Param("id")Integer id); }

    推荐阅读