1

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/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<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);

}

Smile3k
197 声望22 粉丝