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:更新緩存中的站點,當我們修改禁用企業主體信息時調用此方法
@Component
public class InitEnterpriseSite {@AutowiredIEnterpriseService enterpriseService
;@AutowiredRedissonClient redissonClient
;public Long secondInterval(Date date1
, Date date2
) {long secondInterval
= (date2
.getTime() - date1
.getTime()) / 1000;return secondInterval
;}@PostConstructpublic 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
);}}}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
);}}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();}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想要實現權限的控制只需要依賴此客戶端
認證流程總述
認證總體流程如下:
用戶在登錄頁選擇登錄方式
判斷登錄方式是短信登錄、賬號密碼登錄,進行域名校驗,兌換企業ID【enterpriseid】
通過服務鑒權轉換器ServerAuthenticationConverter構建權限對象Authentication
Authentication對象交于認證管理器【ReactiveAuthenticationManager】進行認證
服務鑒權轉換器
ServerAuthenticationConverter:主要是負責表單的自動轉換,在spring-security中的默認的登錄頁面是long頁面,我們需要從表單中獲取用戶名和密碼或者用戶短信驗證碼
package com.xxxx.restkeeper.converter;import com.itheima.restkeeper.converter.LoginConverter;
import com.itheima.restkeeper.utils.EmptyUtil;
import com.itheima.restkeeper.utils.RegisterBeanHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class ReactiveServerAuthenticationConverter implements ServerAuthenticationConverter {private String loginTypeParameter
= "loginType";private String siteTypeParameter
= "siteType";@AutowiredRegisterBeanHandler registerBeanHandler
;@Overridepublic Mono<Authentication> convert(ServerWebExchange exchange
) {String loginType
= exchange
.getRequest().getHeaders().getFirst("loginType");String siteType
= exchange
.getRequest().getHeaders().getFirst("siteType");if (EmptyUtil.isNullOrEmpty(loginType
)){throw new BadCredentialsException("客戶登陸異常");}LoginConverter loginConverter
= registerBeanHandler
.getBean(loginType
, LoginConverter.class);return loginConverter
.convert(exchange
,loginType
,siteType
);}
}
**LoginConverter:**登錄轉換接口定義
import org.springframework.security.core.Authentication;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public interface LoginConverter {public Mono<Authentication> convert(ServerWebExchange exchange
,String loginType
,String siteType
);
}
**MobilLoginConverter:**手機驗證碼登錄轉換器
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component("mobilLogin")
public class MobilLoginConverter implements LoginConverter {private String mobileParameter
= "mobile";private String authCodeParameter
= "authCode";@AutowiredRedissonClient redissonClient
;@Overridepublic Mono<Authentication> convert(ServerWebExchange exchange
,String loginType
,String siteType
) {String hostName
= exchange
.getRequest().getURI().getHost();String key
= null;if (siteType
.equals(SuperConstant.WEBSITE
)){key
= SecurityCacheConstant.WEBSITE
+hostName
;}else if (siteType
.equals(SuperConstant.APP_WEBSITE
)){key
= SecurityCacheConstant.APP_WEBSITE
+hostName
;}else {return Mono.error(new BadCredentialsException("站點類型未定義"));}RBucket<EnterpriseVo> bucket
= redissonClient
.getBucket(key
);EnterpriseVo enterpriseVo
= bucket
.get();if (EmptyUtil.isNullOrEmpty(enterpriseVo
)){return Mono.error(new BadCredentialsException("Invalid hostName"));}String enterpriseId
= String.valueOf(enterpriseVo
.getEnterpriseId());return exchange
.getFormData().map( data
-> {String mobile
= data
.getFirst(this.mobileParameter
);String authCode
= data
.getFirst(this.authCodeParameter
);if (EmptyUtil.isNullOrEmpty(mobile
)||EmptyUtil.isNullOrEmpty(authCode
)){throw new BadCredentialsException("客戶登陸異常");}String principal
= mobile
+":"+enterpriseId
+":"+loginType
+":"+siteType
;return new UsernamePasswordAuthenticationToken(principal
, authCode
);});}
}
**UsernameLoginConverter:**用戶名密碼登錄
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component("usernameLogin")
public class UsernameLoginConverter implements LoginConverter {private String usernameParameter
= "username";private String passwordParameter
= "password";@AutowiredRedissonClient redissonClient
;@Overridepublic Mono<Authentication> convert(ServerWebExchange exchange
,String loginType
,String siteType
) {String hostName
= exchange
.getRequest().getURI().getHost();String key
= null;if (siteType
.equals(SuperConstant.WEBSITE
)){key
= SecurityCacheConstant.WEBSITE
+hostName
;}else if (siteType
.equals(SuperConstant.APP_WEBSITE
)){key
= SecurityCacheConstant.APP_WEBSITE
+hostName
;}else {return Mono.error(new BadCredentialsException("站點類型未定義"));}RBucket<EnterpriseVo> bucket
= redissonClient
.getBucket(key
);EnterpriseVo enterpriseVo
= bucket
.get();if (EmptyUtil.isNullOrEmpty(enterpriseVo
)){return Mono.error(new BadCredentialsException("Invalid hostName"));}String enterpriseId
= String.valueOf(enterpriseVo
.getEnterpriseId());return exchange
.getFormData().map( data
-> {String username
= data
.getFirst(this.usernameParameter
);String password
= data
.getFirst(this.passwordParameter
);if (EmptyUtil.isNullOrEmpty(username
)||EmptyUtil.isNullOrEmpty(password
)){throw new BadCredentialsException("用戶登陸異常");}String principal
= username
+":"+enterpriseId
+":"+loginType
+":"+siteType
;return new UsernamePasswordAuthenticationToken(principal
, password
);});}
}
用戶信息明細
ReactiveUserDetailsServiceImpl:主要負責認證過程,對于用戶信息的獲得,這里分為四種獲得方式
user賬戶登錄
user手機登錄
customer賬戶登錄
customer手機登錄
統一調用UserAdapterFace或者CustomerAdapterFace進行用戶登錄消息的獲得方式
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;import java.util.HashSet;
@Component("reactiveUserDetailsService")
@Slf4j
public class ReactiveUserDetailsServiceImpl implements ReactiveUserDetailsService{@DubboReference(version
= "${dubbo.application.version}", check
= false)UserAdapterFace userAdapterFace
;@DubboReference(version
= "${dubbo.application.version}", check
= false)CustomerAdapterFace customerAdapterFace
;@Overridepublic Mono<UserDetails> findByUsername(String principal
) {String[] principals
= principal
.split(":");if (principals
.length
!=4){log
.warn("用戶:{}登錄信息不完整",principal
);return Mono.empty();}String mobile
=principals
[0];String username
=principals
[0];Long enterpriseId
=Long.valueOf(principals
[1]);String loginType
=principals
[2];String siteType
=principals
[3];UserVo userVo
= null;if (loginType
.equals(SuperConstant.USERNAME_LOGIN
)&&siteType
.equals(SuperConstant.WEBSITE
)){userVo
= userAdapterFace
.findUserByUsernameAndEnterpriseId(username
, enterpriseId
);}if (loginType
.equals(SuperConstant.MOBIL_LOGIN
)&&siteType
.equals(SuperConstant.WEBSITE
)){userVo
= userAdapterFace
.findUserByMobilAndEnterpriseId(mobile
, enterpriseId
);}if (loginType
.equals(SuperConstant.USERNAME_LOGIN
)&&siteType
.equals(SuperConstant.APP_WEBSITE
)){userVo
= customerAdapterFace
.findCustomerByUsernameAndEnterpriseId(username
, enterpriseId
);}if (loginType
.equals(SuperConstant.MOBIL_LOGIN
)&&siteType
.equals(SuperConstant.APP_WEBSITE
)){userVo
= customerAdapterFace
.findCustomerByMobilAndEnterpriseId(mobile
, enterpriseId
);}if (EmptyUtil.isNullOrEmpty(userVo
)){log
.warn("用戶:{}不存在",principal
);return Mono.empty();}UserAuth userAuth
= new UserAuth(userVo
.getUsername(),userVo
.getPassword(),new HashSet<>(),userVo
.getId(),userVo
.getShardingId(),userVo
.getEnterpriseId(),userVo
.getStoreId(),userVo
.getJwtToken(),userVo
.getRealName(),userVo
.getSex(),userVo
.getMobil(),userVo
.getEmail(),userVo
.getDiscountLimit(),userVo
.getReduceLimit(),userVo
.getDuties(),userVo
.getCreatedTime(),userVo
.getUpdatedTime());return Mono.just(userAuth
);}
}
認證管理器
**JwtReactiveAuthenticationManager:**查詢用戶明細信息之后,與請求傳遞過來的密碼或者短信驗證碼進行比對,如果比對成功則表示登錄成功
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
@Component
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {private PasswordEncoder passwordEncoder
= PasswordEncoderFactories.createDelegatingPasswordEncoder();private Scheduler scheduler
= Schedulers.parallel();@Autowiredprivate ReactiveUserDetailsService reactiveUserDetailsService
;@AutowiredRedissonClient redissonClient
;@Overridepublic Mono<Authentication> authenticate(Authentication authentication
) {final String principal
= authentication
.getName();final String password
= (String) authentication
.getCredentials();String[] principals
= principal
.split(":");String mobile
=principals
[0];String enterpriseId
=principals
[1];String loginType
=principals
[2];String siteType
=principals
[3];Mono<UserDetails> userDetailsMono
= this.reactiveUserDetailsService
.findByUsername(principal
);if (loginType
.equals(SuperConstant.USERNAME_LOGIN
)){return userDetailsMono
.publishOn(this.scheduler
).filter(u
-> this.passwordEncoder
.matches(password
, u
.getPassword())).switchIfEmpty(Mono.defer(()->Mono.error(new BadCredentialsException("Invalid Credentials")))).map(u
->new UsernamePasswordAuthenticationToken(u
, u
.getPassword(), u
.getAuthorities()));}if (loginType
.equals(SuperConstant.MOBIL_LOGIN
)){String key
= SmsCacheConstant.LOGIN_CODE
+principals
[0];RBucket<String> bucket
= redissonClient
.getBucket(key
);String authCode
= bucket
.get();if (EmptyUtil.isNullOrEmpty(authCode
)){Mono.error(new BadCredentialsException("Invalid Credentials"));}return userDetailsMono
.publishOn(this.scheduler
).filter(u
-> authCode
.equals(password
)).switchIfEmpty(Mono.defer(()->Mono.error(new BadCredentialsException("Invalid Credentials")))).map(u
->new UsernamePasswordAuthenticationToken(u
, u
.getPassword(), u
.getAuthorities()));}throw new BadCredentialsException("Invalid Credentials");}public void setPasswordEncoder(PasswordEncoder passwordEncoder
) {Assert.notNull(passwordEncoder
, "passwordEncoder cannot be null");this.passwordEncoder
= passwordEncoder
;}}
認證成功
import io.netty.util.CharsetUtil;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;import java.util.*;
@Component
public class JsonServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {@AutowiredJwtTokenManager jwtTokenManager
;@DubboReference(version
= "${dubbo.application.version}", check
= false)UserAdapterFace userAdapterFace
;@Overridepublic Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange
, Authentication authentication
) {ServerHttpResponse response
= webFilterExchange
.getExchange().getResponse();response
.setStatusCode(HttpStatus.OK
);response
.getHeaders().set(HttpHeaders.CONTENT_TYPE
, "application/json; charset=UTF-8");UserAuth authUser
= (UserAuth) authentication
.getPrincipal();UserVo userVo
= UserVo.builder().id(authUser
.getId()).username(authUser
.getUsername()).reduceLimit(authUser
.getReduceLimit()).discountLimit(authUser
.getDiscountLimit()).enterpriseId(authUser
.getEnterpriseId()).storeId(authUser
.getStoreId()).build();List<RoleVo> roleByUserId
= userAdapterFace
.findRoleByUserId(userVo
.getId());Set<String> roles
= new HashSet<>();for (RoleVo roleVo
: roleByUserId
) {roles
.add(roleVo
.getLabel());}List<ResourceVo> resourceByUserId
= userAdapterFace
.findResourceByUserId(userVo
.getId());Set<String> resources
= new HashSet<>();for (ResourceVo resourceVo
: resourceByUserId
) {resources
.add(resourceVo
.getRequestPath());}userVo
.setRoles(roles
);userVo
.setResources(resources
);Map<String,Object> claims
= new HashMap<>();String userVoJsonString
= JSONObject.toJSONString(userVo
);claims
.put("currentUser",userVoJsonString
);String jwtToken
= jwtTokenManager
.issuedToken("system",jwtTokenManager
.getJwtProperties().getTtl(),authUser
.getId().toString(),claims
);userVo
.setJwtToken(jwtToken
);ResponseWrap<UserVo> responseWrap
= ResponseWrapBuild.build(AuthEnum.SUCCEED
, userVo
);String result
= JSONObject.toJSONString(responseWrap
);DataBuffer buffer
= response
.bufferFactory().wrap(result
.getBytes(CharsetUtil.UTF_8
));return response
.writeWith(Mono.just(buffer
));}
}
認證失敗
認證失敗:只需要返回錯誤信息即可
import io.netty.util.CharsetUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class JsonServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {@Overridepublic Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange
, AuthenticationException exception
) {ServerHttpResponse response
= webFilterExchange
.getExchange().getResponse();response
.setStatusCode(HttpStatus.OK
);response
.getHeaders().set(HttpHeaders.CONTENT_TYPE
, "application/json; charset=UTF-8");ResponseWrap<UserAuth> responseWrap
= ResponseWrapBuild.build(AuthEnum.FAIL
, null);String result
= JSONObject.toJSONString(responseWrap
);DataBuffer buffer
= response
.bufferFactory().wrap(result
.getBytes(CharsetUtil.UTF_8
));return response
.writeWith(Mono.just(buffer
));}
}
總結
以上是生活随笔為你收集整理的gateway-统一权限-认证的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。