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

来源:https://www.cnblogs.com/FullStackProgrammer/archive/2022/06/09/16359101.html
-Advertisement-
Play Games

前面我們詳細介紹了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工程下,和簡訊、文件存儲等的配置放到同一工程下,作為框架的擴展功能。

基礎配置:
image.png
第三方列表:
image.png

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官方提供的賬戶整合流程圖:
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

image.png

  • Nacos配置
      client-id: 59ced49784f3cebfb208
      client-secret: 807f52cc33a1aae07f97521b5501adc6f36375c8
      redirect-uri: http://192.168.0.2:8000/social/github/callback
      ignore-check-state: false
  • 或者使用多租戶系統配置 ,每個租戶僅允許有一個主配置
    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、點擊登錄,如果此時GitHub賬號沒有登錄過,則跳轉到綁定或者註冊賬號界面
image.png

4、輸入手機號+驗證碼或者賬號+密碼,即可進入到登錄前的頁面。使用手機號+驗證碼的模式,如果系統不存在賬號,可以直接註冊新賬號並登錄。

5、JustAuth支持的第三方登錄列表,只需到相應第三方登錄申請即可,下麵圖片取自JustAuth官網:
932e4b2a80e72e310db33ece19caf80.png

源碼地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 業務背景 近些年來,隨著前端工程架構發展,使得前端項目中也能擁有如後端工程的模塊能力。正所謂 “能力(越)越大(來),責任(越)越大(捲)”,現在的前端工程不僅僅要滿足業務需求,還伴隨更多複雜的環境適配問題,例如: 1.api請求的功能變數名稱會 ...
  • StackOverflow和StackExchange等等都是程式員常用的問題反饋和解決平臺,相當於是專業性更強的知乎。但是很多類似的網站界面打開後總是有一個privacy收集的視窗無法關閉,在很大情況下影響了我們平時的閱讀。因此我們通過前端的屏蔽方法,可以取消privacy位置收集視窗的固定,從而... ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • 時間的獲取及時間各格式間的轉換是比較常用的操作,但一是多種語言經常容易弄混,二是同一種語言同一個功能可能有不同的實現函數,導致每次處理時間經常要百度所以來記錄一下。 另外個人真不喜歡同樣功能有多種寫法的形式,從理想角度說多種實現方式讓不同的人都能以其喜歡的方式進行編寫;但實際上當你忘記的時候,你就總 ...
  • 沙包和打傘的故事 美國在1961年到1972年組織實施的一系列載人登月飛行任務。目的是實現載人登月飛行和人對月球的實地考察,為載人行星飛行和探測進行技術準備,它是世界航天史上具有劃時代意義的一項成就。阿波羅計劃始於1961年5月,至1972年12月第6次登月成功結束,歷時約11年,耗資255億美元。 ...
  • 一、分詞 - jieba 優秀的中文分詞庫,依靠中文詞庫,利用詞庫確定漢子之間關聯的概率,形成分詞結果 import jieba word = '偉大的中華人民共和國' jieba.cut(word) jieba.lcut(word) 二、詞雲庫 - wordcloud 對數據中出現頻率較高的 關鍵 ...
  • pom版本 <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.7</version> </dependency> 1.自定義合併單元格 在某些業務場景中可能會有合併單 ...
  • 主要內容 1·學習java日誌體系及日誌工具的演進 2·瞭解日誌採集、處理、分析等各階段的常用中間件 3·學會搭建完整的elk日誌平臺 4·學習日誌打點,切麵,日誌文件等輸出手段 5·項目實戰,完成一訪問日誌鏈路的跟蹤 1、Java日誌體系 1.1 體系概述 1.1.1 日誌介面 JCL:Apach ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...