前面我們詳細介紹了SSO、OAuth2的定義和實現原理,也舉例說明瞭如何在微服務框架中使用spring-security-oauth2實現單點登錄授權伺服器和單點登錄客戶端。目前很多平臺都提供了單點登錄授權伺服器功能,比如我們經常用到的QQ登錄、微信登錄、新浪微博登錄、支付寶登錄等等。 如果我們自己 ...
前面我們詳細介紹了SSO、OAuth2的定義和實現原理,也舉例說明瞭如何在微服務框架中使用spring-security-oauth2實現單點登錄授權伺服器和單點登錄客戶端。目前很多平臺都提供了單點登錄授權伺服器功能,比如我們經常用到的QQ登錄、微信登錄、新浪微博登錄、支付寶登錄等等。
如果我們自己的系統需要調用第三方登錄,那麼我們就需要實現單點登錄客戶端,然後跟需要對接的平臺調試登錄SDK。JustAuth是第三方授權登錄的工具類庫,對接了國外內數十家第三方登錄的SDK,我們在需要實現第三方登錄時,只需要集成JustAuth工具包,然後配置即可實現第三方登錄,省去了需要對接不同SDK的麻煩。
JustAuth官方提供了多種入門指南,集成使用非常方便。但是如果要貼合我們自有開發框架的業務需求,還是需要進行整合優化。下麵根據我們的系統需求,從兩方面進行整合:一是支持多租戶功能,二是和自有系統的用戶進行匹配。
一、JustAuth多租戶系統配置
-
GitEgg多租戶功能實現介紹
GitEgg框架支持多租戶功能,從多租戶的實現來講,目前大多數平臺都是在登錄界面輸入租戶的標識來確定屬於哪個租戶,這種方式簡單有效,但是對於用戶來講體驗不是很好。我們更希望的多租戶功能是能夠讓用戶無感知,且每個租戶有自己不同的界面展示。
GitEgg在實現多租戶功能時,考慮到同一功能變數名稱可以設置多個子功能變數名稱,每個子功能變數名稱可對應不同的租戶。所以,對於多租戶的識別方式,首先是根據瀏覽器當前訪問的功能變數名稱或IP地址和系統配置的多租戶功能變數名稱或IP地址信息進行自動識別,如果是功能變數名稱或IP地址存在多個,或者未找到相關配置時,才會由用戶自己選擇屬於哪個租戶。
-
自定義JustAuth配置文件信息到資料庫和緩存
在JustAuth的官方Demo中,SpringBoot集成JustAuth是將第三方授權信息配置在yml配置文件中的,對於單租戶系統來說,可以這樣配置。但是,對於多租戶系統,我們需要考慮多種情況:一種是整個多租戶系統使用同一套第三方授權,授權之後再由用戶選擇綁定到具體的租戶;另外一種是每個租戶配置自己的第三方授權,更具差異化。
出於功能完整性的考慮,我們兩種情況都實現,當租戶不配置自有的第三方登錄參數時,使用的是系統預設自帶的第三方登錄參數。當租戶配置了自有的第三方登錄參數時,就是使用租戶自己的第三方授權伺服器。我們將JustAuth原本配置在yml配置文件中的第三方授權伺服器信息配置在資料庫中,並增加多租戶標識,這樣在不同租戶調用第三方登錄時就是相互隔離的。
1. JustAuth配置信息表欄位設計
首先我們通過JustAuth官方Demo justauth-spring-boot-starter-demo 瞭解到JustAuth主要的配置參數為:
- JustAuth功能啟用開關
- 自定義第三方登錄的配置信息
- 內置預設第三方登錄的配置信息
- Http請求代理的配置信息
- 緩存的配置信息
justauth:
# JustAuth功能啟用開關
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
# 內置預設第三方登錄的配置信息
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
在對配置文件存儲格式進行設計時,結合對多租戶系統的需求分析,我們需要選擇哪些配置是系統公共配置,哪些是租戶自己的配置。比如自定義第三方登錄的enum-class這個是需要由系統開發的,是整個多租戶系統的功能,這種可以看做是通用配置,但是在這裡,考慮到後續JustAuth系統升級,我們不打算破壞原先配置文件的結構,所以我們仍選擇各租戶隔離配置。
我們將JustAuth配置信息拆分為兩張表存儲,一張是配置JustAuth開關、自定義第三方登錄配置類、緩存配置、Http超時配置等信息的表(t_just_auth_config),這些配置信息的同一特點是與第三方登錄系統無關,不因第三方登錄系統的改變而改變;還有一張表是配置第三方登錄相關的參數、Http代理請求表(t_just_auth_source)。租戶和t_just_auth_config為一對一關係,和t_just_auth_source為一對多關係。
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開關',
`enum_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義擴展第三方登錄的配置類',
`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 '創建時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '創建者',
`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 '第三方登錄類型:預設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:對應各平臺的appKey',
`client_secret` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客戶端Secret:對應各平臺的appSecret',
`redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登錄成功後的回調地址',
`alipay_public_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付寶公鑰:當選擇支付寶登錄時,該值可用',
`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 '企業微信,授權方的網頁應用ID',
`user_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企業微信第三方授權用戶類型,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 '忽略校驗code state}參數,預設不開啟。',
`scopes` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支持自定義授權平臺的 scope 內容',
`device_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '設備ID, 設備唯一標識ID',
`client_os_type` int(11) NULL DEFAULT NULL COMMENT '喜馬拉雅:客戶端操作系統類型,1-iOS系統,2-Android系統,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 模式的平臺,針對無服務應用,不推薦使用隱式授權,推薦使用 PKCE 模式',
`auth_server_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Okta 授權伺服器的 ID, 預設為 default。',
`ignore_check_redirect_uri` tinyint(1) NOT NULL DEFAULT 0 COMMENT '忽略校驗 {@code redirectUri} 參數,預設不開啟。',
`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 '創建時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '創建者',
`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配置信息管理的相關代碼和JustAuth實現業務邏輯的代碼分開,配置信息我們在系統啟動時載入到Redis緩存,JustAuth在調用時,直接調用Redis緩存中的配置。
前面講過如何通過資料庫表設計生成CRUD的前後端代碼,這裡不再贅述,生成好的後臺代碼我們放在gitegg-service-extension工程下,和簡訊、文件存儲等的配置放到同一工程下,作為框架的擴展功能。
基礎配置:
第三方列表:
3. 代碼生成之後,需要做初始化緩存處理,即在第三方配置服務啟動的時候,將多租戶的配置信息初始化到Redis緩存中。
- 初始化的CommandLineRunner類 InitExtensionCacheRunner.java
/**
* 容器啟動完成載入資源許可權數據到緩存
* @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);
// 判斷是否開啟了租戶模式,如果開啟了,那麼角色許可權需要按租戶進行分類存儲
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);
}
});
}
- 第三方登錄參數配置初始化方法
/**
* 初始化配置表列表
* @return
*/
@Override
public void initJustAuthSourceList() {
QueryJustAuthSourceDTO queryJustAuthSourceDTO = new QueryJustAuthSourceDTO();
queryJustAuthSourceDTO.setStatus(GitEggConstant.ENABLE);
List<JustAuthSourceDTO> justAuthSourceInfoList = justAuthSourceMapper.initJustAuthSourceList(queryJustAuthSourceDTO);
// 判斷是否開啟了租戶模式,如果開啟了,那麼角色許可權需要按租戶進行分類存儲
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相關依賴jar包
- 在gitegg-platform-bom工程中引入JustAuth包和版本,JustAuth提供了SpringBoot集成版本justAuth-spring-security-starter,如果簡單使用,可以直接引用SpringBoot集成版本,我們這裡因為需要做相應的定製修改,所以引入JustAuth基礎工具包。
······
<!-- 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工程,用於實現公共自定義代碼,併在pom.xml中引入需要的jar包。
<dependencies>
<!-- gitegg Spring Boot自定義及擴展 -->
<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預設版本-->
<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. 自定義實現獲取和實例化多租戶第三方登錄配置的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;
}
/**
* 返回當前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 {
// 轉為系統配置對象
JustAuthConfig justAuthConfig = JsonUtils.jsonToPojo(sourceConfigStr, JustAuthConfig.class);
// 判斷該配置是否開啟了第三方登錄
if (justAuthConfig.getEnabled())
{
// 根據配置生成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))
{
// 轉為系統配置對象
justAuthSource = JsonUtils.jsonToPojo(sourceAuthStr, JustAuthSource.class);
authConfig = BeanCopierUtils.copyByClass(justAuthSource, AuthConfig.class);
// 組裝scopes,因為系統配置的是逗號分割的字元串
if (!StringUtils.isEmpty(justAuthSource.getScopes()))
{
String[] scopes = justAuthSource.getScopes().split(StrUtil.COMMA);
authConfig.setScopes(Arrays.asList(scopes));
}
// 設置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,判斷是預設的第三方登錄還是自定義第三方登錄
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("初始化自定義第三方登錄時發生異常:{}", e);
}
}
}
}
} catch (Exception e) {
log.error("獲取第三方登錄時發生異常:{}", 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;
}
// 從緩存獲取租戶單獨配置
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. 登錄後註冊或綁定用戶
實現了第三方登錄功能,我們自己的系統也需要做相應的用戶匹配,通過OAuth2協議我們可以瞭解到,單點登錄成功後可以獲取第三方系統的用戶信息,當然,具體獲取到第三方用戶的哪些信息是由第三方系統決定的。所以目前大多數系統平臺再第三方登錄成功之後,都會顯示用戶註冊或綁定頁面,將第三方用戶和自有系統平臺用戶進行綁定。那麼在下一次第三方登錄成功之後,就會自動匹配到自有系統的用戶,進一步的獲取到該用戶在自有系統的許可權、菜單等。
JustAuth官方提供的賬戶整合流程圖:
我們通常的第三方登錄業務流程是點擊登錄,獲取到第三方授權時,會去查詢自有系統數據是否有匹配的用戶,如果有,則自動登錄到後臺,如果沒有,則跳轉到賬號綁定或者註冊頁面,進行賬戶綁定或者註冊。我們將此業務流程放到gitegg-oauth微服務中去實現,新建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;
/**
* 密碼最大嘗試次數
*/
@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();
}
/**
* 獲取到對應類型的登錄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()));
}
/**
* 保存或更新用戶數據,併進行判斷是否進行註冊或綁定
* @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);
// 獲取到第三方用戶信息後,先進行保存或更新
Result<Object> createResult = justAuthFeign.userCreateOrUpdate(justAuthSocialInfoDTO);
if(createResult.isSuccess() && null != createResult.getData())
{
Long socialId = Long.parseLong((String)createResult.getData());
// 判斷此第三方用戶是否被綁定到系統用戶
Result<Object> bindResult = justAuthFeign.userBindQuery(socialId);
// 這裡需要處理返回消息,前端需要根據返回是否已經綁定好的消息來判斷
// 將socialId進行加密返回
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放入緩存,預設有效期2個小時,如果2個小時未完成驗證,那麼操作失效,重新獲取,在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());
}
}
/**
* 綁定用戶手機號
* 這裡不走手機號登錄的流程,因為如果手機號不存在那麼可以直接創建一個用戶併進行綁定
*/
@PostMapping("/bind/mobile")
@ApiOperation(value = "綁定用戶手機號")
public Result<?> bindMobile(@Valid @RequestBody SocialBindMobileDTO socialBind) {
Result<?> smsResult = smsFeign.checkSmsVerificationCode(socialBind.getSmsCode(), socialBind.getPhoneNumber(), socialBind.getCode());
// 判斷簡訊驗證是否成功
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放入緩存,預設有效期2個小時,如果2個小時未完成驗證,那麼操作失效,重新獲取,在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);
// 查詢用戶是否存在,如果存在,那麼直接調用綁定介面
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
{
// 如果用戶不存在,那麼調用新建用戶介面,並綁定
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;
}
}
// 執行綁定操作
return justAuthFeign.userBind(Long.valueOf(desSocialId), userId);
}
return smsResult;
}
/**
* 綁定賬號
* 這裡只有綁定操作,沒有創建用戶操作
*/
@PostMapping("/bind/account")
@ApiOperation(value = "綁定用戶賬號")
public Result<?> bindAccount(@Valid @RequestBody SocialBindAccountDTO socialBind) {
// 查詢用戶是否存在,如果存在,那麼直接調用綁定介面
Result<?> result = userFeign.queryUserByAccount(socialBind.getUsername());
// 判斷返回信息
if (null != result && result.isSuccess() && null != result.getData()) {
GitEggUser gitEggUser = BeanUtil.copyProperties(result.getData(), GitEggUser.class);
// 必須添加次數驗證,和登錄一樣,超過最大驗證次數那麼直接鎖定賬戶
// 從Redis獲取賬號密碼錯誤次數
Object lockTimes = redisTemplate.boundValueOps(AuthConstant.LOCK_ACCOUNT_PREFIX + gitEggUser.getId()).get();
// 判斷賬號密碼輸入錯誤幾次,如果輸入錯誤多次,則鎖定賬號
if(null != lockTimes && (int)lockTimes >= maxTryTimes){
throw new BusinessException("密碼嘗試次數過多,請使用其他方式綁定");
}
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String password = AuthConstant.BCRYPT + gitEggUser.getAccount() + DigestUtils.md5DigestAsHex(socialBind.getPassword().getBytes());
// 驗證賬號密碼是否正確
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放入緩存,預設有效期2個小時,如果2個小時未完成驗證,那麼操作失效,重新獲取,在system:socialLoginExpiration配置
String desSocialId = (String)redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
// 執行綁定操作
return justAuthFeign.userBind(Long.valueOf(desSocialId), gitEggUser.getId());
}
else
{
// 增加鎖定次數
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. 所有的配置和綁定註冊功能實現之後,我們還需要實現關鍵的一步,就是自定義實現OAuth2的第三方登錄模式SocialTokenGranter,在第三方授權之後,通過此模式進行登錄,自定義實現之後,記得t_oauth_client_details表需增加social授權。
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);
// 校驗socialId
String socialId;
try {
// 將socialId進行加密返回
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("第三方登錄驗證已失效,請返回登錄頁重新操作");
}
if (StringUtils.isEmpty(socialId))
{
throw new InvalidGrantException("第三方登錄驗證已失效,請返回登錄頁重新操作");
}
// 校驗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也需要做回調處理
因為是前後端分離的項目,我們這裡需要將第三方回調介面配置在vue頁面,前端頁面根據賬戶信息判斷是直接登錄還是進行綁定或者註冊等操作。新建SocialCallback.vue用於處理前端第三方登錄授權後的回調操作。
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) {
// 授權後發現已綁定,那麼直接調用第三方登錄
this.socialLogin(bindResult.data)
} else if (bindResult.code === 601) {
// 授權後沒有綁定則跳轉到綁定界面
that.$router.push({ name: 'socialBind', query: { redirect: this.getUrlKey('redirect'), key: bindResult.data } })
} else if (bindResult.code === 602) {
// 該賬號已綁定多個賬號,請聯繫系統管理員,或者到個人中心解綁
this.$notification['error']({
message: '錯誤',
description: ((res.response || {}).data || {}).message || '該賬號已綁定多個賬號,請聯繫系統管理員,或者到個人中心解綁',
duration: 4
})
} else {
// 提示獲取第三方登錄失敗
this.$notification['error']({
message: '錯誤',
description: '第三方登錄失敗,請稍後再試',
duration: 4
})
}
} else {
// 提示獲取第三方登錄失敗
this.$notification['error']({
message: '錯誤',
description: '第三方登錄失敗,請稍後再試',
duration: 4
})
}
})
},
// 第三方登錄後回調
socialLogin (key) {
const { Login } = this
// 執行登錄操作
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 || '請求出現錯誤,請稍後再試',
duration: 4
})
}
}
}
</script>
<style>
</style>
二、登錄和綁定測試
JustAuth官方提供了詳細的第三方登錄的使用指南,按照其介紹,到需要的第三方網站申請,然後進行配置即可,這裡只展示GitHub的登錄測試步驟。
1、按照官方提供的註冊申請步驟,獲取到GitHub的client-id和client-secret並配置回調地址redirect-uri
- Nacos配置
client-id: 59ced49784f3cebfb208
client-secret: 807f52cc33a1aae07f97521b5501adc6f36375c8
redirect-uri: http://192.168.0.2:8000/social/github/callback
ignore-check-state: false
- 或者使用多租戶系統配置 ,每個租戶僅允許有一個主配置
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、點擊登錄,如果此時GitHub賬號沒有登錄過,則跳轉到綁定或者註冊賬號界面
4、輸入手機號+驗證碼或者賬號+密碼,即可進入到登錄前的頁面。使用手機號+驗證碼的模式,如果系統不存在賬號,可以直接註冊新賬號並登錄。
5、JustAuth支持的第三方登錄列表,只需到相應第三方登錄申請即可,下麵圖片取自JustAuth官網:
源碼地址:
Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg