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.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://example.comSec-WebSocket-Version: 13
服务端返回音讯:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-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-storeFrontDsclientHandler Dsclient侧websocket音讯处理器 /api/v1/websocket/dsClientExtractParameterInterceptor提取request中的参数并封装到websocketSession中BinderIdCheckInterceptor查看是否申请中具备BinderId参数前端侧-storeFrontWebClientHandler 前端侧websocket音讯处理器 /api/v1/websocket/webClientAuthHttpSessionInterceptor 校验是否登录
WebClientHandler
INIT_BINDER_INFO 服务端返回binderId信息REFRESH_APPLICATION_LIST 服务端转发Dsagent触发的REFRESH_APPLICATION_LIST工夫OPEN_APPLICATION 关上利用,转发给DsagentWEB_CLIENT_DIS_CONNECT 前端退出登录,转发给Dsagent
DsClientHandler
PUSH_LATEST_APPLICATION_INFO 刚连贯时服务端会发送最新的本地利用列表REPORT_LOCAL_APPLICATION_INFO 上报本地利用详情如装置进度,会触发REFRESH_APPLICATION_LIST事件
流程如下:
4.2 DSAgent-AgentManagerWeb
WSConfiguration websocket配置类,配置AgentWSHandler和AgentWSInterceptorAgentWSHandler 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<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@Slf4jpublic 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);}