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