SpringCloud微服務實戰——搭建企業級開發框架(四十一):擴充套件JustAuth+SpringSecurity+Vue實現多租戶系統微信掃碼、釘釘掃碼等第三方登入

2022-06-09 15:00:42

  前面我們詳細介紹了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