部分研發設計文件

logan發表於2021-10-26

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

}

相關文章