int## 基础名词概念
**权限:**属于系统的安全范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全控制策略用户可以访问而且只能访问自己被授权的资源,主要包括用户身份认证和请求鉴权两部分,简称认证鉴权
认证判断一个用户是否为合法用户的处理过程,最常用的简单身份认证是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确
鉴权:即访问控制,控制谁能访问那些资源;进行身份认证后需要分配权限可访问的系统资源,对于某些资源没有权限是无法访问的,如下图所示
权限控制:用户是某个角色、或拥有某个资源时,才可访问系统资源我们称之为权限控制,权限控制分为下列2类型:
基于角色
RBAC基于角色的访问控制是以角色为中心进行访问控制,比如:主体的角色为总经理可以查询企业运营报表,查询员工薪资信息等,访问控制流程如下:
基于资源
RBAC基于资源的访问控制,是以资源中心进行访问控制,企业中常用的权限管理方法,实现思路是:将系统操作的每个URL配置在资源表中,将资源对应到角色,将角色分配给用户,用户访问系统功能通过Filter进行过滤,过滤器获取到用户的url,只要访问的url是用户分配角色中的URL是用户分配角色的url则进行访问,其具体流程如下:
匿名资源:无需认证鉴权就可以访问的资源
公共资源:只需登录既可以访问的资源
多平台权限控制
xxxx作为一个SaaS平台,商家提供运营主体信息后,运营平台会为商家开通系统,各个商家平台都需要在运营平台的管理下去工作:
1、运营平台可以管理所有商家平台的企业信息
2、运营平台可以管理所有商家平台的资源信息
3、运营平台可以管理所有商家平台的角色信息
4、运营平台可以管理商家平台的用户信息
第二章 基础信息简介
在开始做权限开发之前我们需要看下权限设计的数据库结构:
通过上图,我们可以得到如下的信息:
一个企业可以有多个用户
一个用户可以有多个角色
一个角色可以有多个资源
这个是经典的权限设计,也就是:企业,用户,角色,资源通过它们可以来完成整个权限的控制。
企业信息
商家想申请入驻平台,首先在申请页面【也可以后端录入】进行信息填写,填写完成【运营平台】对商家资质进行审核,审核通过后商家即可入职使用,如图所示:
数据库结构设计
CREATE TABLE `tab_enterprise` (
`id` bigint(18) NOT NULL,
`enterprise_id` bigint(18) NOT NULL COMMENT '商户ID【系统内部识别使用】',
`enterprise_name` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '企业名称',
`enterprise_no` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '工商号',
`province` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '地址(省)',
`area` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '地址(区)',
`city` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '地址(市)',
`address` varchar(200) COLLATE utf8_bin NOT NULL COMMENT '详细地址',
`status` varchar(8) COLLATE utf8_bin NOT NULL COMMENT '状态(试用:trial,停用:stop,正式:official)',
`proposer_Id` bigint(18) DEFAULT NULL COMMENT '申请人Id',
`enable_flag` varchar(18) CHARACTER SET utf8 NOT NULL COMMENT '是否有效',
`created_time` datetime NOT NULL COMMENT '创建时间',
`updated_time` datetime NOT NULL COMMENT '创建时间',
`expire_time` datetime NOT NULL COMMENT '到期时间 (试用下是默认七天后到期,状态改成停用)',
`web_site` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '商户门店web站点',
`sharding_id` bigint(18) NOT NULL COMMENT '分库id',
`app_web_site` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '商户h5web站点',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='企业账号管理';
实现细节
对于这个功能的CRUD这里就不做赘述,这里主要思考2个问题:
为什么我们要为商家绑定域名?
通过一个图来分析下整个的工作流程
员工在浏览器中发起ppsk.shop.eehp.cn的访问请求
阿里云域名解析会把ppsk.shop.eehp网址解析到阿里云ECS服务器101.101.108.2
阿里云ECS服务器【101.101.108.2】宿主机会把信息转发到docker-nginx服务器
docker-nginx服务器配置的serverName【*.shop.eehp.cn】转发到gateway服务
gateway服务根据ppsk.shop.eehp.cn兑换企业号100001
根据企业号100001访问目标的商家A
域名和企业号如何建立关联
在security模块的initEnterpriseWeb方法,这里主要有四个方法:
init:初始化企业站点信息到redis,此方法上有==@PostConstruct==注解,表示项目启动时即加载信息
addWebSiteforRedis:添加缓存中的站点,当我们【新增】企业主体信息时调用此方法
deleteWebSiteForRedis:移除缓存中的站点,当我们【删除,仅用】企业主体信息时调用此方法
updateWebSiteforRedis:更新缓存中的站点,当我们修改禁用企业主体信息时调用此方法
/**
* @ClassName initEnterpriseWebSIteInfo.java
* @Description 初始化企业站点信息到redis
*/
@Component
public class InitEnterpriseSite {
@Autowired
IEnterpriseService enterpriseService;
@Autowired
RedissonClient redissonClient;
/**
*获得两时间的秒间隔
*/
public Long secondInterval(Date date1, Date date2) {
long secondInterval = (date2.getTime() - date1.getTime()) / 1000;
return secondInterval;
}
/***
* @description 初始化企业站点信息到redis
*/
@PostConstruct
public void init(){
QueryWrapper<Enterprise> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Enterprise::getEnableFlag, SuperConstant.YES)
.and(wrapper->wrapper
.eq(Enterprise::getStatus,SuperConstant.TRIAL)
.or()
.eq(Enterprise::getStatus,SuperConstant.OFFICIAL));
List<Enterprise> list = enterpriseService.list(queryWrapper);
List<EnterpriseVo> enterpriseVos = BeanConv.toBeanList(list, EnterpriseVo.class);
for (EnterpriseVo enterpriseVo : enterpriseVos) {
String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
Long secondInterval = this.secondInterval(new Date(), enterpriseVo.getExpireTime());
if (secondInterval.longValue()>0){
webSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
appWebSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
}
}
}
/***
* @description 添加缓存中的站点
* @param enterpriseVo 企业号
* @return:
*/
public void addWebSiteforRedis(EnterpriseVo enterpriseVo){
String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
Long secondInterval = this.secondInterval(new Date(), enterpriseVo.getExpireTime());
if (secondInterval.longValue()>0){
webSiteBucket.trySet(enterpriseVo,secondInterval, TimeUnit.SECONDS);
appWebSiteBucket.trySet(enterpriseVo,secondInterval, TimeUnit.SECONDS);
}
}
/***
* @description 移除缓存中的站点
* @param enterpriseVo 企业号
* @return:
*/
public void deleteWebSiteforRedis( EnterpriseVo enterpriseVo){
String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
webSiteBucket.delete();
appWebSiteBucket.delete();
}
/***
* @description 更新缓存中的站点
* @param enterpriseVo 企业号
* @return:
*/
public void updataWebSiteforRedis(EnterpriseVo enterpriseVo){
String webSiteKey = SecurityCacheConstant.WEBSITE+enterpriseVo.getWebSite();
RBucket<EnterpriseVo> webSiteBucket = redissonClient.getBucket(webSiteKey);
String appWebSiteKey = SecurityCacheConstant.APP_WEBSITE+enterpriseVo.getAppWebSite();
RBucket<EnterpriseVo> appWebSiteBucket = redissonClient.getBucket(appWebSiteKey);
Long secondInterval = this.secondInterval(new Date(), enterpriseVo.getExpireTime());
if (secondInterval.longValue()>0){
webSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
appWebSiteBucket.set(enterpriseVo,secondInterval, TimeUnit.SECONDS);
}
}
考虑到企业表【Enterprise】做CRUD的时候会影响缓存的更新,需要在EnterpriseFaceImpl中做同步的处理
@Override
public EnterpriseVo createEnterprise(EnterpriseVo eterperiseVo) {
Enterprise enterpriseResult = EnterpriseService.createEnterprise(eterperiseVo);
//同步缓存
if (!EmptyUtil.isNullOrEmpty(enterpriseResult)){
initEnterpriseWebSiteInfo.addWebSiteforRedis(eterperiseVo.getWebSite(),eterperiseVo);
}
return BeanConv.toBean(enterpriseResult,EnterpriseVo.class);
}
@Override
public Boolean updateEnterprise(EnterpriseVo enterpriseVo) {
Boolean flag = EnterpriseService.updateEnterprise(enterpriseVo);
//同步缓存
if (flag){
if (enterpriseVo.getEnableFlag().equals(SuperConstant.YES)){
initEnterpriseWebSiteInfo.updataWebSiteforRedis(enterpriseVo.getWebSite(),enterpriseVo);
}else {
initEnterpriseWebSiteInfo.deleteWebSiteforRedis(enterpriseVo.getWebSite(),enterpriseVo);
}
}
return flag;
}
@Override
public Boolean deleteEnterprise(String[] checkedIds) {
//同步缓存
for (String checkedId : checkedIds) {
Enterprise enterprise = EnterpriseService.getById(checkedId);
EnterpriseVo enterpriseVo = BeanConv.toBean(enterprise, EnterpriseVo.class);
initEnterpriseWebSiteInfo.deleteWebSiteforRedis(enterprise.getWebSite(),enterpriseVo);
}
Boolean flag = EnterpriseService.deleteEnterprise(checkedIds);
return flag;
}
CREATE TABLE `tab_resource` (
`id` bigint(18) NOT NULL COMMENT '主键',
`parent_id` bigint(18) DEFAULT NULL COMMENT '父Id',
`resource_name` varchar(36) DEFAULT NULL COMMENT '资源名称',
`request_path` varchar(200) DEFAULT NULL COMMENT '资源路径',
`icon` varchar(20) DEFAULT NULL COMMENT '图标',
`is_leaf` varchar(18) DEFAULT NULL COMMENT '是否叶子节点',
`resource_type` varchar(36) DEFAULT NULL COMMENT '资源类型',
`sort_no` int(11) DEFAULT NULL COMMENT '排序',
`description` varchar(200) DEFAULT NULL COMMENT '描述',
`system_code` varchar(36) DEFAULT NULL COMMENT '系统归属',
`is_system_root` varchar(18) DEFAULT NULL COMMENT '是否根节点',
`enable_flag` varchar(18) DEFAULT NULL COMMENT '是否有效',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '创建时间',
`sharding_id` bigint(18) DEFAULT NULL,
`label` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='资源表';
为了解决信息系统中的访问控制管理的问题,适当简化授权工作量,提高权限管理效率,需要建立基于角色的多系统授权管理模型,其业务管理模式如下:
由运营平台系统管理员负责角色的权限及用户权限及用户分配。
由运营平台系统管理员负责角色的权限匹配,同时赋予商家管理员对角色分配用户的权限,定义标准角色,实现权限管理的部分下放
在此模式下,系统管理员不再兼任单位管理员工作,需要实现权限的多级下放,其架构设计如图所示
数据库结构:
角色表:
CREATE TABLE `tab_role` (
`id` bigint(18) NOT NULL COMMENT '主键',
`role_name` varchar(36) DEFAULT NULL COMMENT '角色名称',
`label` varchar(36) DEFAULT NULL COMMENT '角色标识',
`description` varchar(200) DEFAULT NULL COMMENT '角色描述',
`sort_no` int(36) DEFAULT NULL COMMENT '排序',
`enable_flag` varchar(18) DEFAULT NULL COMMENT '是否有效',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '创建时间',
`sharding_id` bigint(18) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色表';
角色资源表:
CREATE TABLE `tab_role_resource` (
`id` bigint(18) NOT NULL,
`enable_flag` varchar(18) DEFAULT NULL,
`role_id` bigint(18) DEFAULT NULL,
`resource_id` bigint(18) DEFAULT NULL,
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '创建时间',
`sharding_id` bigint(18) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色资源表';
用户信息
xxx系统中用户分为:运营商员工、商家平台员工,其信息的维护规则如下:
由运营平台系统管理员:可以定义,管理所有的用户,并且从角色中选择权限
由运营平台系统管理员:负责定义角色,同时赋予商家管理员对角色,商家管理员分配用户的权限(定义标准角色,实现权限管理的部分下放)。
多个运营商之间的员工信息是相互隔绝的
数据库结构
用户表:
CREATE TABLE `tab_user` (
`id` bigint(18) NOT NULL COMMENT '主键',
`store_id` bigint(32) DEFAULT NULL COMMENT '门店Id',
`enterprise_id` bigint(18) NOT NULL COMMENT '商户号',
`username` varchar(36) DEFAULT NULL COMMENT '登录名称',
`real_name` varchar(36) DEFAULT NULL COMMENT '真实姓名',
`password` varchar(150) DEFAULT NULL COMMENT '密码',
`sex` varchar(11) DEFAULT NULL COMMENT '性别',
`mobil` varchar(36) DEFAULT NULL COMMENT '电话',
`email` varchar(36) DEFAULT NULL COMMENT '邮箱',
`discount_limit` decimal(10,2) DEFAULT NULL COMMENT '折扣上线',
`reduce_limit` decimal(10,2) DEFAULT NULL COMMENT '减免金额上线',
`duties` varchar(36) DEFAULT NULL COMMENT '职务',
`sort_no` int(11) DEFAULT NULL COMMENT '排序',
`enable_flag` varchar(18) DEFAULT NULL COMMENT '是否有效',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '创建时间',
`sharding_id` bigint(18) DEFAULT NULL COMMENT '分库id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户表';
用户角色表
CREATE TABLE `tab_user_role` (
`id` bigint(36) NOT NULL,
`enable_flag` varchar(18) DEFAULT NULL,
`user_id` bigint(18) DEFAULT NULL,
`role_id` bigint(18) DEFAULT NULL,
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '创建时间',
`sharding_id` bigint(18) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户角色表';
统一权限认证
认证:判断一个用户是否为合法用户的处理过程,最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户和口令一致,来判断用户身份是否正确,如下图所示:
集成的方式:本权限是基于spring-cloud-gateway网关来做权限的控制,因为gateway是基于响应式webflux【响应式编程】的机制进行处理的,所以这里的用法和原本httpservlet是有所区别的,首先我们来看一下整体的模块依赖处理:
商家发起请求到gateway-shop网关
gateway网关调用model-security-client,进行认证或鉴权过滤器
model-security-client作为服务消费者通过model-security-interface接口进行用户认证、鉴权接口调用
model-security-producer作为服务生产者进行当前用户登录、角色、权限的查询
model-security-client:模块是本权限系统核心,他提供了具体的认证、鉴权的逻辑,如果一个gateway想要实现权限的控制只需要依赖此客户端