??前面我們詳細(xì)介紹了SSO株搔、OAuth2的定義和實(shí)現(xiàn)原理总滩,也舉例說明了如何在微服務(wù)框架中使用spring-security-oauth2實(shí)現(xiàn)單點(diǎn)登錄授權(quán)服務(wù)器和單點(diǎn)登錄客戶端。目前很多平臺都提供了單點(diǎn)登錄授權(quán)服務(wù)器功能幸逆,比如我們經(jīng)常用到的QQ登錄棍辕、微信登錄、新浪微博登錄秉颗、支付寶登錄等等痢毒。
??如果我們自己的系統(tǒng)需要調(diào)用第三方登錄,那么我們就需要實(shí)現(xiàn)單點(diǎn)登錄客戶端蚕甥,然后跟需要對接的平臺調(diào)試登錄SDK哪替。JustAuth是第三方授權(quán)登錄的工具類庫,對接了國外內(nèi)數(shù)十家第三方登錄的SDK菇怀,我們在需要實(shí)現(xiàn)第三方登錄時凭舶,只需要集成JustAuth工具包,然后配置即可實(shí)現(xiàn)第三方登錄爱沟,省去了需要對接不同SDK的麻煩帅霜。
??JustAuth官方提供了多種入門指南,集成使用非常方便呼伸。但是如果要貼合我們自有開發(fā)框架的業(yè)務(wù)需求身冀,還是需要進(jìn)行整合優(yōu)化钝尸。下面根據(jù)我們的系統(tǒng)需求,從兩方面進(jìn)行整合:一是支持多租戶功能搂根,二是和自有系統(tǒng)的用戶進(jìn)行匹配珍促。
一、JustAuth多租戶系統(tǒng)配置
-
GitEgg多租戶功能實(shí)現(xiàn)介紹
??GitEgg框架支持多租戶功能剩愧,從多租戶的實(shí)現(xiàn)來講猪叙,目前大多數(shù)平臺都是在登錄界面輸入租戶的標(biāo)識來確定屬于哪個租戶,這種方式簡單有效仁卷,但是對于用戶來講體驗(yàn)不是很好穴翩。我們更希望的多租戶功能是能夠讓用戶無感知,且每個租戶有自己不同的界面展示锦积。
??GitEgg在實(shí)現(xiàn)多租戶功能時芒帕,考慮到同一域名可以設(shè)置多個子域名,每個子域名可對應(yīng)不同的租戶充包。所以副签,對于多租戶的識別方式遥椿,首先是根據(jù)瀏覽器當(dāng)前訪問的域名或IP地址和系統(tǒng)配置的多租戶域名或IP地址信息進(jìn)行自動識別基矮,如果是域名或IP地址存在多個,或者未找到相關(guān)配置時冠场,才會由用戶自己選擇屬于哪個租戶家浇。
-
自定義JustAuth配置文件信息到數(shù)據(jù)庫和緩存
??在JustAuth的官方Demo中,SpringBoot集成JustAuth是將第三方授權(quán)信息配置在yml配置文件中的碴裙,對于單租戶系統(tǒng)來說钢悲,可以這樣配置。但是舔株,對于多租戶系統(tǒng)莺琳,我們需要考慮多種情況:一種是整個多租戶系統(tǒng)使用同一套第三方授權(quán),授權(quán)之后再由用戶選擇綁定到具體的租戶载慈;另外一種是每個租戶配置自己的第三方授權(quán)惭等,更具差異化。
??出于功能完整性的考慮办铡,我們兩種情況都實(shí)現(xiàn)辞做,當(dāng)租戶不配置自有的第三方登錄參數(shù)時,使用的是系統(tǒng)默認(rèn)自帶的第三方登錄參數(shù)寡具。當(dāng)租戶配置了自有的第三方登錄參數(shù)時秤茅,就是使用租戶自己的第三方授權(quán)服務(wù)器。我們將JustAuth原本配置在yml配置文件中的第三方授權(quán)服務(wù)器信息配置在數(shù)據(jù)庫中童叠,并增加多租戶標(biāo)識框喳,這樣在不同租戶調(diào)用第三方登錄時就是相互隔離的。
1. JustAuth配置信息表字段設(shè)計
??首先我們通過JustAuth官方Demo justauth-spring-boot-starter-demo 了解到JustAuth主要的配置參數(shù)為:
- JustAuth功能啟用開關(guān)
- 自定義第三方登錄的配置信息
- 內(nèi)置默認(rèn)第三方登錄的配置信息
- Http請求代理的配置信息
- 緩存的配置信息
justauth:
# JustAuth功能啟用開關(guān)
enabled: true
# 自定義第三方登錄的配置信息
extend:
enum-class: com.xkcoding.justauthspringbootstarterdemo.extend.ExtendSource
config:
TEST:
request-class: com.xkcoding.justauthspringbootstarterdemo.extend.ExtendTestRequest
client-id: xxxxxx
client-secret: xxxxxxxx
redirect-uri: http://oauth.xkcoding.com/demo/oauth/test/callback
MYGITLAB:
request-class: com.xkcoding.justauthspringbootstarterdemo.extend.ExtendMyGitlabRequest
client-id: xxxxxx
client-secret: xxxxxxxx
redirect-uri: http://localhost:8443/oauth/mygitlab/callback
# 內(nèi)置默認(rèn)第三方登錄的配置信息
type:
GOOGLE:
client-id: xxxxxx
client-secret: xxxxxxxx
redirect-uri: http://localhost:8443/oauth/google/callback
ignore-check-state: false
scopes:
- profile
- email
- openid
# Http請求代理的配置信息
http-config:
timeout: 30000
proxy:
GOOGLE:
type: HTTP
hostname: 127.0.0.1
port: 10080
MYGITLAB:
type: HTTP
hostname: 127.0.0.1
port: 10080
# 緩存的配置信息
cache:
type: default
prefix: 'demo::'
timeout: 1h
??在對配置文件存儲格式進(jìn)行設(shè)計時,結(jié)合對多租戶系統(tǒng)的需求分析五垮,我們需要選擇哪些配置是系統(tǒng)公共配置撰豺,哪些是租戶自己的配置。比如自定義第三方登錄的enum-class這個是需要由系統(tǒng)開發(fā)的拼余,是整個多租戶系統(tǒng)的功能污桦,這種可以看做是通用配置,但是在這里匙监,考慮到后續(xù)JustAuth系統(tǒng)升級凡橱,我們不打算破壞原先配置文件的結(jié)構(gòu),所以我們?nèi)赃x擇各租戶隔離配置亭姥。
??我們將JustAuth配置信息拆分為兩張表存儲稼钩,一張是配置JustAuth開關(guān)、自定義第三方登錄配置類达罗、緩存配置坝撑、Http超時配置等信息的表(t_just_auth_config),這些配置信息的同一特點(diǎn)是與第三方登錄系統(tǒng)無關(guān)粮揉,不因第三方登錄系統(tǒng)的改變而改變巡李;還有一張表是配置第三方登錄相關(guān)的參數(shù)、Http代理請求表(t_just_auth_source)扶认。租戶和t_just_auth_config為一對一關(guān)系侨拦,和t_just_auth_source為一對多關(guān)系。
t_just_auth_config(租戶第三方登錄功能配置表)表定義:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_just_auth_config
-- ----------------------------
DROP TABLE IF EXISTS `t_just_auth_config`;
CREATE TABLE `t_just_auth_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`enabled` tinyint(1) NULL DEFAULT NULL COMMENT 'JustAuth開關(guān)',
`enum_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義擴(kuò)展第三方登錄的配置類',
`http_timeout` bigint(20) NULL DEFAULT NULL COMMENT 'Http請求的超時時間',
`cache_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '緩存類型',
`cache_prefix` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '緩存前綴',
`cache_timeout` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '緩存超時時間',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '創(chuàng)建時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '創(chuàng)建者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '租戶第三方登錄功能配置表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
t_just_auth_sourc(租戶第三方登錄信息配置表)表定義:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_just_auth_source
-- ----------------------------
DROP TABLE IF EXISTS `t_just_auth_source`;
CREATE TABLE `t_just_auth_source` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`source_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第三方登錄的名稱',
`source_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第三方登錄類型:默認(rèn)default 自定義custom',
`request_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義第三方登錄的請求Class',
`client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客戶端id:對應(yīng)各平臺的appKey',
`client_secret` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客戶端Secret:對應(yīng)各平臺的appSecret',
`redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登錄成功后的回調(diào)地址',
`alipay_public_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付寶公鑰:當(dāng)選擇支付寶登錄時辐宾,該值可用',
`union_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否需要申請unionid狱从,目前只針對qq登錄',
`stack_overflow_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Stack Overflow Key',
`agent_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企業(yè)微信,授權(quán)方的網(wǎng)頁應(yīng)用ID',
`user_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企業(yè)微信第三方授權(quán)用戶類型叠纹,member|admin',
`domain_prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '域名前綴 使用 Coding 登錄和 Okta 登錄時季研,需要傳該值。',
`ignore_check_state` tinyint(1) NOT NULL DEFAULT 0 COMMENT '忽略校驗(yàn)code state}參數(shù)誉察,默認(rèn)不開啟与涡。',
`scopes` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支持自定義授權(quán)平臺的 scope 內(nèi)容',
`device_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '設(shè)備ID, 設(shè)備唯一標(biāo)識ID',
`client_os_type` int(11) NULL DEFAULT NULL COMMENT '喜馬拉雅:客戶端操作系統(tǒng)類型,1-iOS系統(tǒng)冒窍,2-Android系統(tǒng)递沪,3-Web',
`pack_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '喜馬拉雅:客戶端包名',
`pkce` tinyint(1) NULL DEFAULT NULL COMMENT ' 是否開啟 PKCE 模式,該配置僅用于支持 PKCE 模式的平臺综液,針對無服務(wù)應(yīng)用款慨,不推薦使用隱式授權(quán),推薦使用 PKCE 模式',
`auth_server_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Okta 授權(quán)服務(wù)器的 ID谬莹, 默認(rèn)為 default檩奠。',
`ignore_check_redirect_uri` tinyint(1) NOT NULL DEFAULT 0 COMMENT '忽略校驗(yàn) {@code redirectUri} 參數(shù)桩了,默認(rèn)不開啟。',
`proxy_type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Http代理類型',
`proxy_host_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Http代理Host',
`proxy_port` int(11) NULL DEFAULT NULL COMMENT 'Http代理Port',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '創(chuàng)建時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '創(chuàng)建者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '租戶第三方登錄信息配置表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
2. 使用GitEgg代碼生成工具生成JustAuth配置信息的CRUD代碼
??我們將JustAuth配置信息管理的相關(guān)代碼和JustAuth實(shí)現(xiàn)業(yè)務(wù)邏輯的代碼分開埠戳,配置信息我們在系統(tǒng)啟動時加載到Redis緩存井誉,JustAuth在調(diào)用時,直接調(diào)用Redis緩存中的配置整胃。
??前面講過如何通過數(shù)據(jù)庫表設(shè)計生成CRUD的前后端代碼颗圣,這里不再贅述,生成好的后臺代碼我們放在gitegg-service-extension工程下屁使,和短信在岂、文件存儲等的配置放到同一工程下,作為框架的擴(kuò)展功能蛮寂。
基礎(chǔ)配置:
第三方列表:
3. 代碼生成之后蔽午,需要做初始化緩存處理,即在第三方配置服務(wù)啟動的時候酬蹋,將多租戶的配置信息初始化到Redis緩存中及老。
- 初始化的CommandLineRunner類 InitExtensionCacheRunner.java
/**
* 容器啟動完成加載資源權(quán)限數(shù)據(jù)到緩存
* @author GitEgg
*/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class InitExtensionCacheRunner implements CommandLineRunner {
private final IJustAuthConfigService justAuthConfigService;
private final IJustAuthSourceService justAuthSourceService;
@Override
public void run(String... args) {
log.info("InitExtensionCacheRunner running");
// 初始化第三方登錄主配置
justAuthConfigService.initJustAuthConfigList();
// 初始化第三方登錄 第三方配置
justAuthSourceService.initJustAuthSourceList();
}
}
- 第三方登錄主配置初始化方法
/**
* 初始化配置表列表
* @return
*/
@Override
public void initJustAuthConfigList() {
QueryJustAuthConfigDTO queryJustAuthConfigDTO = new QueryJustAuthConfigDTO();
queryJustAuthConfigDTO.setStatus(GitEggConstant.ENABLE);
List<JustAuthConfigDTO> justAuthSourceInfoList = justAuthConfigMapper.initJustAuthConfigList(queryJustAuthConfigDTO);
// 判斷是否開啟了租戶模式,如果開啟了范抓,那么角色權(quán)限需要按租戶進(jìn)行分類存儲
if (enable) {
Map<Long, List<JustAuthConfigDTO>> authSourceListMap =
justAuthSourceInfoList.stream().collect(Collectors.groupingBy(JustAuthConfigDTO::getTenantId));
authSourceListMap.forEach((key, value) -> {
String redisKey = AuthConstant.SOCIAL_TENANT_CONFIG_KEY + key;
redisTemplate.delete(redisKey);
addJustAuthConfig(redisKey, value);
});
} else {
redisTemplate.delete(AuthConstant.SOCIAL_CONFIG_KEY);
addJustAuthConfig(AuthConstant.SOCIAL_CONFIG_KEY, justAuthSourceInfoList);
}
}
private void addJustAuthConfig(String key, List<JustAuthConfigDTO> configList) {
Map<String, String> authConfigMap = new TreeMap<>();
Optional.ofNullable(configList).orElse(new ArrayList<>()).forEach(config -> {
try {
authConfigMap.put(config.getTenantId().toString(), JsonUtils.objToJson(config));
redisTemplate.opsForHash().putAll(key, authConfigMap);
} catch (Exception e) {
log.error("初始化第三方登錄失斀径瘛:{}" , e);
}
});
}
- 第三方登錄參數(shù)配置初始化方法
/**
* 初始化配置表列表
* @return
*/
@Override
public void initJustAuthSourceList() {
QueryJustAuthSourceDTO queryJustAuthSourceDTO = new QueryJustAuthSourceDTO();
queryJustAuthSourceDTO.setStatus(GitEggConstant.ENABLE);
List<JustAuthSourceDTO> justAuthSourceInfoList = justAuthSourceMapper.initJustAuthSourceList(queryJustAuthSourceDTO);
// 判斷是否開啟了租戶模式,如果開啟了尉咕,那么角色權(quán)限需要按租戶進(jìn)行分類存儲
if (enable) {
Map<Long, List<JustAuthSourceDTO>> authSourceListMap =
justAuthSourceInfoList.stream().collect(Collectors.groupingBy(JustAuthSourceDTO::getTenantId));
authSourceListMap.forEach((key, value) -> {
String redisKey = AuthConstant.SOCIAL_TENANT_SOURCE_KEY + key;
redisTemplate.delete(redisKey);
addJustAuthSource(redisKey, value);
});
} else {
redisTemplate.delete(AuthConstant.SOCIAL_SOURCE_KEY);
addJustAuthSource(AuthConstant.SOCIAL_SOURCE_KEY, justAuthSourceInfoList);
}
}
private void addJustAuthSource(String key, List<JustAuthSourceDTO> sourceList) {
Map<String, String> authConfigMap = new TreeMap<>();
Optional.ofNullable(sourceList).orElse(new ArrayList<>()).forEach(source -> {
try {
authConfigMap.put(source.getSourceName(), JsonUtils.objToJson(source));
redisTemplate.opsForHash().putAll(key, authConfigMap);
} catch (Exception e) {
log.error("初始化第三方登錄失數:{}" , e);
}
});
}
4. 引入JustAuth相關(guān)依賴jar包
- 在gitegg-platform-bom工程中引入JustAuth包和版本璃岳,JustAuth提供了SpringBoot集成版本justAuth-spring-security-starter年缎,如果簡單使用,可以直接引用SpringBoot集成版本铃慷,我們這里因?yàn)樾枰鱿鄳?yīng)的定制修改单芜,所以引入JustAuth基礎(chǔ)工具包。
······
<!-- JustAuth第三方登錄 -->
<just.auth.version>1.16.5</just.auth.version>
<!-- JustAuth SpringBoot集成 -->
<just.auth.spring.version>1.4.0</just.auth.spring.version>
······
<!--JustAuth第三方登錄-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${just.auth.version}</version>
</dependency>
<!--JustAuth SpringBoot集成-->
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${just.auth.spring.version}</version>
</dependency>
······
- 新建gitegg-platform-justauth工程犁柜,用于實(shí)現(xiàn)公共自定義代碼洲鸠,并在pom.xml中引入需要的jar包。
<dependencies>
<!-- gitegg Spring Boot自定義及擴(kuò)展 -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-boot</artifactId>
</dependency>
<!--JustAuth第三方登錄-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<!--JustAuth SpringBoot集成-->
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<!-- 不使用JustAuth默認(rèn)版本-->
<exclusions>
<exclusion>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
3. 自定義實(shí)現(xiàn)獲取和實(shí)例化多租戶第三方登錄配置的AuthRequest工廠類GitEggAuthRequestFactory.java
/**
* GitEggAuthRequestFactory工廠類
*
* @author GitEgg
*/
@Slf4j
@RequiredArgsConstructor
public class GitEggAuthRequestFactory {
private final RedisTemplate redisTemplate;
private final AuthRequestFactory authRequestFactory;
private final JustAuthProperties justAuthProperties;
/**
* 是否開啟租戶模式
*/
@Value("${tenant.enable}")
private Boolean enable;
public GitEggAuthRequestFactory(AuthRequestFactory authRequestFactory, RedisTemplate redisTemplate, JustAuthProperties justAuthProperties) {
this.authRequestFactory = authRequestFactory;
this.redisTemplate = redisTemplate;
this.justAuthProperties = justAuthProperties;
}
/**
* 返回當(dāng)前Oauth列表
*
* @return Oauth列表
*/
public List<String> oauthList() {
// 合并
return authRequestFactory.oauthList();
}
/**
* 返回AuthRequest對象
*
* @param source {@link AuthSource}
* @return {@link AuthRequest}
*/
public AuthRequest get(String source) {
if (StrUtil.isBlank(source)) {
throw new AuthException(AuthResponseStatus.NO_AUTH_SOURCE);
}
// 組裝多租戶的緩存配置key
String authConfigKey = AuthConstant.SOCIAL_TENANT_CONFIG_KEY;
if (enable) {
authConfigKey += GitEggAuthUtils.getTenantId();
} else {
authConfigKey = AuthConstant.SOCIAL_CONFIG_KEY;
}
// 獲取主配置馋缅,每個租戶只有一個主配置
String sourceConfigStr = (String) redisTemplate.opsForHash().get(authConfigKey, GitEggAuthUtils.getTenantId());
AuthConfig authConfig = null;
JustAuthSource justAuthSource = null;
AuthRequest tenantIdAuthRequest = null;
if (!StringUtils.isEmpty(sourceConfigStr))
{
try {
// 轉(zhuǎn)為系統(tǒng)配置對象
JustAuthConfig justAuthConfig = JsonUtils.jsonToPojo(sourceConfigStr, JustAuthConfig.class);
// 判斷該配置是否開啟了第三方登錄
if (justAuthConfig.getEnabled())
{
// 根據(jù)配置生成StateCache
CacheProperties cacheProperties = new CacheProperties();
if (!StringUtils.isEmpty(justAuthConfig.getCacheType())
&& !StringUtils.isEmpty(justAuthConfig.getCachePrefix())
&& null != justAuthConfig.getCacheTimeout())
{
cacheProperties.setType(CacheProperties.CacheType.valueOf(justAuthConfig.getCacheType().toUpperCase()));
cacheProperties.setPrefix(justAuthConfig.getCachePrefix());
cacheProperties.setTimeout(Duration.ofMinutes(justAuthConfig.getCacheTimeout()));
}
else
{
cacheProperties = justAuthProperties.getCache();
}
GitEggRedisStateCache gitEggRedisStateCache =
new GitEggRedisStateCache(redisTemplate, cacheProperties, enable);
// 組裝多租戶的第三方配置信息key
String authSourceKey = AuthConstant.SOCIAL_TENANT_SOURCE_KEY;
if (enable) {
authSourceKey += GitEggAuthUtils.getTenantId();
} else {
authSourceKey = AuthConstant.SOCIAL_SOURCE_KEY;
}
// 獲取具體的第三方配置信息
String sourceAuthStr = (String)redisTemplate.opsForHash().get(authSourceKey, source.toUpperCase());
if (!StringUtils.isEmpty(sourceAuthStr))
{
// 轉(zhuǎn)為系統(tǒng)配置對象
justAuthSource = JsonUtils.jsonToPojo(sourceAuthStr, JustAuthSource.class);
authConfig = BeanCopierUtils.copyByClass(justAuthSource, AuthConfig.class);
// 組裝scopes,因?yàn)橄到y(tǒng)配置的是逗號分割的字符串
if (!StringUtils.isEmpty(justAuthSource.getScopes()))
{
String[] scopes = justAuthSource.getScopes().split(StrUtil.COMMA);
authConfig.setScopes(Arrays.asList(scopes));
}
// 設(shè)置proxy
if (StrUtil.isAllNotEmpty(justAuthSource.getProxyType(), justAuthSource.getProxyHostName())
&& null != justAuthSource.getProxyPort())
{
JustAuthProperties.JustAuthProxyConfig proxyConfig = new JustAuthProperties.JustAuthProxyConfig();
proxyConfig.setType(justAuthSource.getProxyType());
proxyConfig.setHostname(justAuthSource.getProxyHostName());
proxyConfig.setPort(justAuthSource.getProxyPort());
if (null != proxyConfig) {
HttpConfig httpConfig = HttpConfig.builder().timeout(justAuthSource.getProxyPort()).proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort()))).build();
if (null != justAuthConfig.getHttpTimeout())
{
httpConfig.setTimeout(justAuthConfig.getHttpTimeout());
}
authConfig.setHttpConfig(httpConfig);
}
}
// 組裝好配置后扒腕,從配置生成request,判斷是默認(rèn)的第三方登錄還是自定義第三方登錄
if (SourceTypeEnum.DEFAULT.key.equals(justAuthSource.getSourceType()))
{
tenantIdAuthRequest = this.getDefaultRequest(source, authConfig, gitEggRedisStateCache);
}
else if (!StringUtils.isEmpty(justAuthConfig.getEnumClass()) && SourceTypeEnum.CUSTOM.key.equals(justAuthSource.getSourceType()))
{
try {
Class enumConfigClass = Class.forName(justAuthConfig.getEnumClass());
tenantIdAuthRequest = this.getExtendRequest(enumConfigClass, source, (ExtendProperties.ExtendRequestConfig) authConfig, gitEggRedisStateCache);
} catch (ClassNotFoundException e) {
log.error("初始化自定義第三方登錄時發(fā)生異常:{}", e);
}
}
}
}
} catch (Exception e) {
log.error("獲取第三方登錄時發(fā)生異常:{}", e);
}
}
if (null == tenantIdAuthRequest)
{
tenantIdAuthRequest = authRequestFactory.get(source);
}
return tenantIdAuthRequest;
}
/**
* 獲取單個的request
* @param source
* @return
*/
private AuthRequest getDefaultRequest(String source, AuthConfig authConfig, GitEggRedisStateCache gitEggRedisStateCache) {
AuthDefaultSource authDefaultSource;
try {
authDefaultSource = EnumUtil.fromString(AuthDefaultSource.class, source.toUpperCase());
} catch (IllegalArgumentException var4) {
return null;
}
// 從緩存獲取租戶單獨(dú)配置
switch(authDefaultSource) {
case GITHUB:
return new AuthGithubRequest(authConfig, gitEggRedisStateCache);
case WEIBO:
return new AuthWeiboRequest(authConfig, gitEggRedisStateCache);
case GITEE:
return new AuthGiteeRequest(authConfig, gitEggRedisStateCache);
case DINGTALK:
return new AuthDingTalkRequest(authConfig, gitEggRedisStateCache);
case DINGTALK_ACCOUNT:
return new AuthDingTalkAccountRequest(authConfig, gitEggRedisStateCache);
case BAIDU:
return new AuthBaiduRequest(authConfig, gitEggRedisStateCache);
case CSDN:
return new AuthCsdnRequest(authConfig, gitEggRedisStateCache);
case CODING:
return new AuthCodingRequest(authConfig, gitEggRedisStateCache);
case OSCHINA:
return new AuthOschinaRequest(authConfig, gitEggRedisStateCache);
case ALIPAY:
return new AuthAlipayRequest(authConfig, gitEggRedisStateCache);
case QQ:
return new AuthQqRequest(authConfig, gitEggRedisStateCache);
case WECHAT_OPEN:
return new AuthWeChatOpenRequest(authConfig, gitEggRedisStateCache);
case WECHAT_MP:
return new AuthWeChatMpRequest(authConfig, gitEggRedisStateCache);
case WECHAT_ENTERPRISE:
return new AuthWeChatEnterpriseQrcodeRequest(authConfig, gitEggRedisStateCache);
case WECHAT_ENTERPRISE_WEB:
return new AuthWeChatEnterpriseWebRequest(authConfig, gitEggRedisStateCache);
case TAOBAO:
return new AuthTaobaoRequest(authConfig, gitEggRedisStateCache);
case GOOGLE:
return new AuthGoogleRequest(authConfig, gitEggRedisStateCache);
case FACEBOOK:
return new AuthFacebookRequest(authConfig, gitEggRedisStateCache);
case DOUYIN:
return new AuthDouyinRequest(authConfig, gitEggRedisStateCache);
case LINKEDIN:
return new AuthLinkedinRequest(authConfig, gitEggRedisStateCache);
case MICROSOFT:
return new AuthMicrosoftRequest(authConfig, gitEggRedisStateCache);
case MI:
return new AuthMiRequest(authConfig, gitEggRedisStateCache);
case TOUTIAO:
return new AuthToutiaoRequest(authConfig, gitEggRedisStateCache);
case TEAMBITION:
return new AuthTeambitionRequest(authConfig, gitEggRedisStateCache);
case RENREN:
return new AuthRenrenRequest(authConfig, gitEggRedisStateCache);
case PINTEREST:
return new AuthPinterestRequest(authConfig, gitEggRedisStateCache);
case STACK_OVERFLOW:
return new AuthStackOverflowRequest(authConfig, gitEggRedisStateCache);
case HUAWEI:
return new AuthHuaweiRequest(authConfig, gitEggRedisStateCache);
case GITLAB:
return new AuthGitlabRequest(authConfig, gitEggRedisStateCache);
case KUJIALE:
return new AuthKujialeRequest(authConfig, gitEggRedisStateCache);
case ELEME:
return new AuthElemeRequest(authConfig, gitEggRedisStateCache);
case MEITUAN:
return new AuthMeituanRequest(authConfig, gitEggRedisStateCache);
case TWITTER:
return new AuthTwitterRequest(authConfig, gitEggRedisStateCache);
case FEISHU:
return new AuthFeishuRequest(authConfig, gitEggRedisStateCache);
case JD:
return new AuthJdRequest(authConfig, gitEggRedisStateCache);
case ALIYUN:
return new AuthAliyunRequest(authConfig, gitEggRedisStateCache);
case XMLY:
return new AuthXmlyRequest(authConfig, gitEggRedisStateCache);
case AMAZON:
return new AuthAmazonRequest(authConfig, gitEggRedisStateCache);
case SLACK:
return new AuthSlackRequest(authConfig, gitEggRedisStateCache);
case LINE:
return new AuthLineRequest(authConfig, gitEggRedisStateCache);
case OKTA:
return new AuthOktaRequest(authConfig, gitEggRedisStateCache);
default:
return null;
}
}
private AuthRequest getExtendRequest(Class clazz, String source, ExtendProperties.ExtendRequestConfig extendRequestConfig, GitEggRedisStateCache gitEggRedisStateCache) {
String upperSource = source.toUpperCase();
try {
EnumUtil.fromString(clazz, upperSource);
} catch (IllegalArgumentException var8) {
return null;
}
if (extendRequestConfig != null) {
Class<? extends AuthRequest> requestClass = extendRequestConfig.getRequestClass();
if (requestClass != null) {
return (AuthRequest) ReflectUtil.newInstance(requestClass, new Object[]{extendRequestConfig, gitEggRedisStateCache});
}
}
return null;
}
}
4. 登錄后注冊或綁定用戶
??實(shí)現(xiàn)了第三方登錄功能碳想,我們自己的系統(tǒng)也需要做相應(yīng)的用戶匹配砸泛,通過OAuth2協(xié)議我們可以了解到铅搓,單點(diǎn)登錄成功后可以獲取第三方系統(tǒng)的用戶信息司恳,當(dāng)然扇商,具體獲取到第三方用戶的哪些信息是由第三方系統(tǒng)決定的腿时。所以目前大多數(shù)系統(tǒng)平臺再第三方登錄成功之后傀缩,都會顯示用戶注冊或綁定頁面后德,將第三方用戶和自有系統(tǒng)平臺用戶進(jìn)行綁定。那么在下一次第三方登錄成功之后栖雾,就會自動匹配到自有系統(tǒng)的用戶楞抡,進(jìn)一步的獲取到該用戶在自有系統(tǒng)的權(quán)限、菜單等析藕。
JustAuth官方提供的賬戶整合流程圖:
??我們通常的第三方登錄業(yè)務(wù)流程是點(diǎn)擊登錄召廷,獲取到第三方授權(quán)時,會去查詢自有系統(tǒng)數(shù)據(jù)是否有匹配的用戶账胧,如果有柱恤,則自動登錄到后臺,如果沒有找爱,則跳轉(zhuǎn)到賬號綁定或者注冊頁面梗顺,進(jìn)行賬戶綁定或者注冊。我們將此業(yè)務(wù)流程放到gitegg-oauth微服務(wù)中去實(shí)現(xiàn)车摄,新建SocialController類:
/**
* 第三方登錄
* @author GitEgg
*/
@Slf4j
@RestController
@RequestMapping("/social")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SocialController {
private final GitEggAuthRequestFactory factory;
private final IJustAuthFeign justAuthFeign;
private final IUserFeign userFeign;
private final ISmsFeign smsFeign;
@Value("${system.secret-key}")
private String secretKey;
@Value("${system.secret-key-salt}")
private String secretKeySalt;
private final RedisTemplate redisTemplate;
/**
* 密碼最大嘗試次數(shù)
*/
@Value("${system.maxTryTimes}")
private int maxTryTimes;
/**
* 鎖定時間寺谤,單位 秒
*/
@Value("${system.maxTryTimes}")
private long maxLockTime;
/**
* 第三方登錄緩存時間,單位 秒
*/
@Value("${system.socialLoginExpiration}")
private long socialLoginExpiration;
@GetMapping
public List<String> list() {
return factory.oauthList();
}
/**
* 獲取到對應(yīng)類型的登錄url
* @param type
* @return
*/
@GetMapping("/login/{type}")
public Result login(@PathVariable String type) {
AuthRequest authRequest = factory.get(type);
return Result.data(authRequest.authorize(AuthStateUtils.createState()));
}
/**
* 保存或更新用戶數(shù)據(jù)吮播,并進(jìn)行判斷是否進(jìn)行注冊或綁定
* @param type
* @param callback
* @return
*/
@RequestMapping("/{type}/callback")
public Result login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
if (response.ok())
{
AuthUser authUser = (AuthUser) response.getData();
JustAuthSocialInfoDTO justAuthSocialInfoDTO = BeanCopierUtils.copyByClass(authUser, JustAuthSocialInfoDTO.class);
BeanCopierUtils.copyByObject(authUser.getToken(), justAuthSocialInfoDTO);
// 獲取到第三方用戶信息后变屁,先進(jìn)行保存或更新
Result<Object> createResult = justAuthFeign.userCreateOrUpdate(justAuthSocialInfoDTO);
if(createResult.isSuccess() && null != createResult.getData())
{
Long socialId = Long.parseLong((String)createResult.getData());
// 判斷此第三方用戶是否被綁定到系統(tǒng)用戶
Result<Object> bindResult = justAuthFeign.userBindQuery(socialId);
// 這里需要處理返回消息,前端需要根據(jù)返回是否已經(jīng)綁定好的消息來判斷
// 將socialId進(jìn)行加密返回
DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
// 這里將source+uuid通過des加密作為key返回到前臺
String socialKey = authUser.getSource() + StrPool.UNDERLINE + authUser.getUuid();
// 將socialKey放入緩存意狠,默認(rèn)有效期2個小時粟关,如果2個小時未完成驗(yàn)證,那么操作失效环戈,重新獲取闷板,在system:socialLoginExpiration配置
redisTemplate.opsForValue().set(AuthConstant.SOCIAL_VALIDATION_PREFIX + socialKey, createResult.getData(), socialLoginExpiration,
TimeUnit.SECONDS);
String desSocialKey = des.encryptHex(socialKey);
bindResult.setData(desSocialKey);
// 這里返回的成功是請求成功,里面放置的result是是否有綁定用戶的成功
return Result.data(bindResult);
}
return Result.error("獲取第三方用戶綁定信息失敗");
}
else
{
throw new BusinessException(response.getMsg());
}
}
/**
* 綁定用戶手機(jī)號
* 這里不走手機(jī)號登錄的流程院塞,因?yàn)槿绻謾C(jī)號不存在那么可以直接創(chuàng)建一個用戶并進(jìn)行綁定
*/
@PostMapping("/bind/mobile")
@ApiOperation(value = "綁定用戶手機(jī)號")
public Result<?> bindMobile(@Valid @RequestBody SocialBindMobileDTO socialBind) {
Result<?> smsResult = smsFeign.checkSmsVerificationCode(socialBind.getSmsCode(), socialBind.getPhoneNumber(), socialBind.getCode());
// 判斷短信驗(yàn)證是否成功
if (smsResult.isSuccess() && null != smsResult.getData() && (Boolean)smsResult.getData()) {
// 解密前端傳來的socialId
DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
String desSocialKey = des.decryptStr(socialBind.getSocialKey());
// 將socialKey放入緩存遮晚,默認(rèn)有效期2個小時,如果2個小時未完成驗(yàn)證拦止,那么操作失效县遣,重新獲取,在system:socialLoginExpiration配置
String desSocialId = (String)redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
// 查詢第三方用戶信息
Result<Object> justAuthInfoResult = justAuthFeign.querySocialInfo(Long.valueOf(desSocialId));
if (null == justAuthInfoResult || !justAuthInfoResult.isSuccess() || null == justAuthInfoResult.getData())
{
throw new BusinessException("未查詢到第三方用戶信息汹族,請返回到登錄頁重試");
}
JustAuthSocialInfoDTO justAuthSocialInfoDTO = BeanUtil.copyProperties(justAuthInfoResult.getData(), JustAuthSocialInfoDTO.class);
// 查詢用戶是否存在萧求,如果存在,那么直接調(diào)用綁定接口
Result<Object> result = userFeign.queryUserByPhone(socialBind.getPhoneNumber());
Long userId;
// 判斷返回信息
if (null != result && result.isSuccess() && null != result.getData()) {
GitEggUser gitEggUser = BeanUtil.copyProperties(result.getData(), GitEggUser.class);
userId = gitEggUser.getId();
}
else
{
// 如果用戶不存在顶瞒,那么調(diào)用新建用戶接口夸政,并綁定
UserAddDTO userAdd = new UserAddDTO();
userAdd.setAccount(socialBind.getPhoneNumber());
userAdd.setMobile(socialBind.getPhoneNumber());
userAdd.setNickname(justAuthSocialInfoDTO.getNickname());
userAdd.setPassword(StringUtils.isEmpty(justAuthSocialInfoDTO.getUnionId()) ? justAuthSocialInfoDTO.getUuid() : justAuthSocialInfoDTO.getUnionId());
userAdd.setStatus(GitEggConstant.UserStatus.ENABLE);
userAdd.setAvatar(justAuthSocialInfoDTO.getAvatar());
userAdd.setEmail(justAuthSocialInfoDTO.getEmail());
userAdd.setStreet(justAuthSocialInfoDTO.getLocation());
userAdd.setComments(justAuthSocialInfoDTO.getRemark());
Result<?> resultUserAdd = userFeign.userAdd(userAdd);
if (null != resultUserAdd && resultUserAdd.isSuccess() && null != resultUserAdd.getData())
{
userId = Long.parseLong((String) resultUserAdd.getData());
}
else
{
// 如果添加失敗,則返回失敗信息
return resultUserAdd;
}
}
// 執(zhí)行綁定操作
return justAuthFeign.userBind(Long.valueOf(desSocialId), userId);
}
return smsResult;
}
/**
* 綁定賬號
* 這里只有綁定操作搁拙,沒有創(chuàng)建用戶操作
*/
@PostMapping("/bind/account")
@ApiOperation(value = "綁定用戶賬號")
public Result<?> bindAccount(@Valid @RequestBody SocialBindAccountDTO socialBind) {
// 查詢用戶是否存在秒梳,如果存在法绵,那么直接調(diào)用綁定接口
Result<?> result = userFeign.queryUserByAccount(socialBind.getUsername());
// 判斷返回信息
if (null != result && result.isSuccess() && null != result.getData()) {
GitEggUser gitEggUser = BeanUtil.copyProperties(result.getData(), GitEggUser.class);
// 必須添加次數(shù)驗(yàn)證,和登錄一樣酪碘,超過最大驗(yàn)證次數(shù)那么直接鎖定賬戶
// 從Redis獲取賬號密碼錯誤次數(shù)
Object lockTimes = redisTemplate.boundValueOps(AuthConstant.LOCK_ACCOUNT_PREFIX + gitEggUser.getId()).get();
// 判斷賬號密碼輸入錯誤幾次朋譬,如果輸入錯誤多次,則鎖定賬號
if(null != lockTimes && (int)lockTimes >= maxTryTimes){
throw new BusinessException("密碼嘗試次數(shù)過多兴垦,請使用其他方式綁定");
}
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String password = AuthConstant.BCRYPT + gitEggUser.getAccount() + DigestUtils.md5DigestAsHex(socialBind.getPassword().getBytes());
// 驗(yàn)證賬號密碼是否正確
if ( passwordEncoder.matches(password, gitEggUser.getPassword()))
{
// 解密前端傳來的socialId
DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
String desSocialKey = des.decryptStr(socialBind.getSocialKey());
// 將socialKey放入緩存徙赢,默認(rèn)有效期2個小時,如果2個小時未完成驗(yàn)證探越,那么操作失效狡赐,重新獲取,在system:socialLoginExpiration配置
String desSocialId = (String)redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
// 執(zhí)行綁定操作
return justAuthFeign.userBind(Long.valueOf(desSocialId), gitEggUser.getId());
}
else
{
// 增加鎖定次數(shù)
redisTemplate.boundValueOps(AuthConstant.LOCK_ACCOUNT_PREFIX + gitEggUser.getId()).increment(GitEggConstant.Number.ONE);
redisTemplate.expire(AuthConstant.LOCK_ACCOUNT_PREFIX +gitEggUser.getId(), maxLockTime , TimeUnit.SECONDS);
throw new BusinessException("賬號或密碼錯誤");
}
}
else
{
throw new BusinessException("賬號不存在");
}
}
}
5. 所有的配置和綁定注冊功能實(shí)現(xiàn)之后钦幔,我們還需要實(shí)現(xiàn)關(guān)鍵的一步枕屉,就是自定義實(shí)現(xiàn)OAuth2的第三方登錄模式SocialTokenGranter,在第三方授權(quán)之后鲤氢,通過此模式進(jìn)行登錄搀擂,自定義實(shí)現(xiàn)之后,記得t_oauth_client_details表需增加social授權(quán)卷玉。
SocialTokenGranter.java
/**
* 第三方登錄模式
* @author GitEgg
*/
public class SocialTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "social";
private final AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private IJustAuthFeign justAuthFeign;
private RedisTemplate redisTemplate;
private String captchaType;
private String secretKey;
private String secretKeySalt;
public SocialTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory, RedisTemplate redisTemplate, IJustAuthFeign justAuthFeign,
UserDetailsService userDetailsService, String captchaType, String secretKey, String secretKeySalt) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.redisTemplate = redisTemplate;
this.captchaType = captchaType;
this.secretKey = secretKey;
this.secretKeySalt = secretKeySalt;
this.justAuthFeign = justAuthFeign;
this.userDetailsService = userDetailsService;
}
protected SocialTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
String socialKey = parameters.get(TokenConstant.SOCIAL_KEY);
// Protect from downstream leaks of password
parameters.remove(TokenConstant.SOCIAL_KEY);
// 校驗(yàn)socialId
String socialId;
try {
// 將socialId進(jìn)行加密返回
DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
String desSocialKey = des.decryptStr(socialKey);
// 獲取緩存中的key
socialId = (String) redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
}
catch (Exception e)
{
throw new InvalidGrantException("第三方登錄驗(yàn)證已失效哨颂,請返回登錄頁重新操作");
}
if (StringUtils.isEmpty(socialId))
{
throw new InvalidGrantException("第三方登錄驗(yàn)證已失效,請返回登錄頁重新操作");
}
// 校驗(yàn)userId
String userId;
try {
Result<Object> socialResult = justAuthFeign.userBindQuery(Long.parseLong(socialId));
if (null == socialResult || StringUtils.isEmpty(socialResult.getData())) {
throw new InvalidGrantException("操作失敗相种,請返回登錄頁重新操作");
}
userId = (String) socialResult.getData();
}
catch (Exception e)
{
throw new InvalidGrantException("操作失敗威恼,請返回登錄頁重新操作");
}
if (StringUtils.isEmpty(userId))
{
throw new InvalidGrantException("操作失敗,請返回登錄頁重新操作");
}
// 這里是通過用戶id查詢用戶信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);
Authentication userAuth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
((AbstractAuthenticationToken)userAuth).setDetails(parameters);
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
6. 后臺處理完成之后寝并,前端VUE也需要做回調(diào)處理
??因?yàn)槭乔昂蠖朔蛛x的項目箫措,我們這里需要將第三方回調(diào)接口配置在vue頁面,前端頁面根據(jù)賬戶信息判斷是直接登錄還是進(jìn)行綁定或者注冊等操作食茎。新建SocialCallback.vue用于處理前端第三方登錄授權(quán)后的回調(diào)操作蒂破。
SocialCallback.vue
<template>
<div>
</div>
</template>
<script>
import { socialLoginCallback } from '@/api/login'
import { mapActions } from 'vuex'
export default {
name: 'SocialCallback',
created () {
this.$loading.show({ tip: '登錄中......' })
const query = this.$route.query
const socialType = this.$route.params.socialType
this.socialCallback(socialType, query)
},
methods: {
...mapActions(['Login']),
getUrlKey: function (name) {
// eslint-disable-next-line no-sparse-arrays
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(window.opener.location.href) || [, ''])[1].replace(/\+/g, '%20')) || null
},
socialCallback (socialType, parameter) {
const that = this
socialLoginCallback(socialType, parameter).then(res => {
that.$loading.hide()
const bindResult = res.data
if (bindResult && bindResult !== '') {
if (bindResult.success && bindResult.data) {
// 授權(quán)后發(fā)現(xiàn)已綁定,那么直接調(diào)用第三方登錄
this.socialLogin(bindResult.data)
} else if (bindResult.code === 601) {
// 授權(quán)后沒有綁定則跳轉(zhuǎn)到綁定界面
that.$router.push({ name: 'socialBind', query: { redirect: this.getUrlKey('redirect'), key: bindResult.data } })
} else if (bindResult.code === 602) {
// 該賬號已綁定多個賬號别渔,請聯(lián)系系統(tǒng)管理員,或者到個人中心解綁
this.$notification['error']({
message: '錯誤',
description: ((res.response || {}).data || {}).message || '該賬號已綁定多個賬號惧互,請聯(lián)系系統(tǒng)管理員哎媚,或者到個人中心解綁',
duration: 4
})
} else {
// 提示獲取第三方登錄失敗
this.$notification['error']({
message: '錯誤',
description: '第三方登錄失敗,請稍后再試',
duration: 4
})
}
} else {
// 提示獲取第三方登錄失敗
this.$notification['error']({
message: '錯誤',
description: '第三方登錄失敗喊儡,請稍后再試',
duration: 4
})
}
})
},
// 第三方登錄后回調(diào)
socialLogin (key) {
const { Login } = this
// 執(zhí)行登錄操作
const loginParams = {
grant_type: 'social',
social_key: key
}
this.$loading.show({ tip: '登錄中......' })
Login(loginParams)
.then((res) => this.loginSuccess(res))
.catch(err => this.loginError(err))
.finally(() => {
this.$loading.hide()
if (this.getUrlKey('redirect')) {
window.opener.location.href = window.opener.location.origin + this.getUrlKey('redirect')
} else {
window.opener.location.reload()
}
window.close()
})
},
loginSuccess (res) {
this.$notification['success']({
message: '提示',
description: '第三方登錄成功',
duration: 4
})
},
loginError (err) {
this.$notification['error']({
message: '錯誤',
description: ((err.response || {}).data || {}).message || '請求出現(xiàn)錯誤拨与,請稍后再試',
duration: 4
})
}
}
}
</script>
<style>
</style>
二、登錄和綁定測試
JustAuth官方提供了詳細(xì)的第三方登錄的使用指南艾猜,按照其介紹买喧,到需要的第三方網(wǎng)站申請捻悯,然后進(jìn)行配置即可,這里只展示GitHub的登錄測試步驟淤毛。
1今缚、按照官方提供的注冊申請步驟,獲取到GitHub的client-id和client-secret并配置回調(diào)地址redirect-uri
- Nacos配置
client-id: 59ced49784f3cebfb208
client-secret: 807f52cc33a1aae07f97521b5501adc6f36375c8
redirect-uri: http://192.168.0.2:8000/social/github/callback
ignore-check-state: false
-
或者使用多租戶系統(tǒng)配置 低淡,每個租戶僅允許有一個主配置
image.png
image.png
2姓言、登錄頁添加Github登錄鏈接
<div class="user-login-other">
<span>{{ $t('user.login.sign-in-with') }}</span>
<a @click="openSocialLogin('wechat_open')">
<a-icon class="item-icon"
type="wechat"></a-icon>
</a>
<a @click="openSocialLogin('qq')">
<a-icon class="item-icon"
type="qq"></a-icon>
</a>
<a @click="openSocialLogin('github')">
<a-icon class="item-icon"
type="github"></a-icon>
</a>
<a @click="openSocialLogin('dingtalk')">
<a-icon class="item-icon"
type="dingding"></a-icon>
</a>
<a class="register"
@click="openRegister"
>{{ $t('user.login.signup') }}
</a>
</div>
3、點(diǎn)擊登錄蔗蹋,如果此時GitHub賬號沒有登錄過何荚,則跳轉(zhuǎn)到綁定或者注冊賬號界面
4、輸入手機(jī)號+驗(yàn)證碼或者賬號+密碼猪杭,即可進(jìn)入到登錄前的頁面餐塘。使用手機(jī)號+驗(yàn)證碼的模式,如果系統(tǒng)不存在賬號皂吮,可以直接注冊新賬號并登錄唠倦。
5、JustAuth支持的第三方登錄列表涮较,只需到相應(yīng)第三方登錄申請即可稠鼻,下面圖片取自JustAuth官網(wǎng):
GitEgg-Cloud是一款基于SpringCloud整合搭建的企業(yè)級微服務(wù)應(yīng)用開發(fā)框架,開源項目地址:
Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg
歡迎感興趣的小伙伴Star支持一下狂票。