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);
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。