SpringCloud微服務(wù)實(shí)戰(zhàn)——搭建企業(yè)級開發(fā)框架(四十一):擴(kuò)展JustAuth+SpringSecurity+Vue實(shí)現(xiàn)多租戶系統(tǒng)微信掃碼、釘釘掃碼等第三方登錄

??前面我們詳細(xì)介紹了SSO株搔、OAuth2的定義和實(shí)現(xiàn)原理总滩,也舉例說明了如何在微服務(wù)框架中使用spring-security-oauth2實(shí)現(xiàn)單點(diǎn)登錄授權(quán)服務(wù)器和單點(diǎn)登錄客戶端。目前很多平臺都提供了單點(diǎn)登錄授權(quán)服務(wù)器功能幸逆,比如我們經(jīng)常用到的QQ登錄棍辕、微信登錄、新浪微博登錄秉颗、支付寶登錄等等痢毒。
??如果我們自己的系統(tǒng)需要調(diào)用第三方登錄,那么我們就需要實(shí)現(xiàn)單點(diǎn)登錄客戶端蚕甥,然后跟需要對接的平臺調(diào)試登錄SDK哪替。JustAuth是第三方授權(quán)登錄的工具類庫,對接了國外內(nèi)數(shù)十家第三方登錄的SDK菇怀,我們在需要實(shí)現(xiàn)第三方登錄時凭舶,只需要集成JustAuth工具包,然后配置即可實(shí)現(xiàn)第三方登錄爱沟,省去了需要對接不同SDK的麻煩帅霜。
??JustAuth官方提供了多種入門指南,集成使用非常方便呼伸。但是如果要貼合我們自有開發(fā)框架的業(yè)務(wù)需求身冀,還是需要進(jìn)行整合優(yōu)化钝尸。下面根據(jù)我們的系統(tǒng)需求,從兩方面進(jìn)行整合:一是支持多租戶功能搂根,二是和自有系統(tǒng)的用戶進(jìn)行匹配珍促。

一、JustAuth多租戶系統(tǒng)配置

  • GitEgg多租戶功能實(shí)現(xiàn)介紹

??GitEgg框架支持多租戶功能剩愧,從多租戶的實(shí)現(xiàn)來講猪叙,目前大多數(shù)平臺都是在登錄界面輸入租戶的標(biāo)識來確定屬于哪個租戶,這種方式簡單有效仁卷,但是對于用戶來講體驗(yàn)不是很好穴翩。我們更希望的多租戶功能是能夠讓用戶無感知,且每個租戶有自己不同的界面展示锦积。
??GitEgg在實(shí)現(xiàn)多租戶功能時芒帕,考慮到同一域名可以設(shè)置多個子域名,每個子域名可對應(yīng)不同的租戶充包。所以副签,對于多租戶的識別方式遥椿,首先是根據(jù)瀏覽器當(dāng)前訪問的域名或IP地址和系統(tǒng)配置的多租戶域名或IP地址信息進(jìn)行自動識別基矮,如果是域名或IP地址存在多個,或者未找到相關(guān)配置時冠场,才會由用戶自己選擇屬于哪個租戶家浇。

  • 自定義JustAuth配置文件信息到數(shù)據(jù)庫和緩存

??在JustAuth的官方Demo中,SpringBoot集成JustAuth是將第三方授權(quán)信息配置在yml配置文件中的碴裙,對于單租戶系統(tǒng)來說钢悲,可以這樣配置。但是舔株,對于多租戶系統(tǒng)莺琳,我們需要考慮多種情況:一種是整個多租戶系統(tǒng)使用同一套第三方授權(quán),授權(quán)之后再由用戶選擇綁定到具體的租戶载慈;另外一種是每個租戶配置自己的第三方授權(quán)惭等,更具差異化。
??出于功能完整性的考慮办铡,我們兩種情況都實(shí)現(xiàn)辞做,當(dāng)租戶不配置自有的第三方登錄參數(shù)時,使用的是系統(tǒng)默認(rèn)自帶的第三方登錄參數(shù)寡具。當(dāng)租戶配置了自有的第三方登錄參數(shù)時秤茅,就是使用租戶自己的第三方授權(quán)服務(wù)器。我們將JustAuth原本配置在yml配置文件中的第三方授權(quán)服務(wù)器信息配置在數(shù)據(jù)庫中童叠,并增加多租戶標(biāo)識框喳,這樣在不同租戶調(diào)用第三方登錄時就是相互隔離的。

1. JustAuth配置信息表字段設(shè)計

??首先我們通過JustAuth官方Demo justauth-spring-boot-starter-demo 了解到JustAuth主要的配置參數(shù)為:

  • JustAuth功能啟用開關(guān)
  • 自定義第三方登錄的配置信息
  • 內(nèi)置默認(rèn)第三方登錄的配置信息
  • Http請求代理的配置信息
  • 緩存的配置信息
justauth:
  # JustAuth功能啟用開關(guān)
  enabled: true
  # 自定義第三方登錄的配置信息
  extend:
    enum-class: com.xkcoding.justauthspringbootstarterdemo.extend.ExtendSource
    config:
      TEST:
        request-class: com.xkcoding.justauthspringbootstarterdemo.extend.ExtendTestRequest
        client-id: xxxxxx
        client-secret: xxxxxxxx
        redirect-uri: http://oauth.xkcoding.com/demo/oauth/test/callback
      MYGITLAB:
        request-class: com.xkcoding.justauthspringbootstarterdemo.extend.ExtendMyGitlabRequest
        client-id: xxxxxx
        client-secret: xxxxxxxx
        redirect-uri: http://localhost:8443/oauth/mygitlab/callback
  # 內(nèi)置默認(rèn)第三方登錄的配置信息
  type:
    GOOGLE:
      client-id: xxxxxx
      client-secret: xxxxxxxx
      redirect-uri: http://localhost:8443/oauth/google/callback
      ignore-check-state: false
      scopes:
        - profile
        - email
        - openid
  # Http請求代理的配置信息
  http-config:
    timeout: 30000
    proxy:
      GOOGLE:
        type: HTTP
        hostname: 127.0.0.1
        port: 10080
      MYGITLAB:
        type: HTTP
        hostname: 127.0.0.1
        port: 10080
  # 緩存的配置信息
  cache:
    type: default
    prefix: 'demo::'
    timeout: 1h

??在對配置文件存儲格式進(jìn)行設(shè)計時,結(jié)合對多租戶系統(tǒng)的需求分析五垮,我們需要選擇哪些配置是系統(tǒng)公共配置撰豺,哪些是租戶自己的配置。比如自定義第三方登錄的enum-class這個是需要由系統(tǒng)開發(fā)的拼余,是整個多租戶系統(tǒng)的功能污桦,這種可以看做是通用配置,但是在這里匙监,考慮到后續(xù)JustAuth系統(tǒng)升級凡橱,我們不打算破壞原先配置文件的結(jié)構(gòu),所以我們?nèi)赃x擇各租戶隔離配置亭姥。
??我們將JustAuth配置信息拆分為兩張表存儲稼钩,一張是配置JustAuth開關(guān)、自定義第三方登錄配置類达罗、緩存配置坝撑、Http超時配置等信息的表(t_just_auth_config),這些配置信息的同一特點(diǎn)是與第三方登錄系統(tǒng)無關(guān)粮揉,不因第三方登錄系統(tǒng)的改變而改變巡李;還有一張表是配置第三方登錄相關(guān)的參數(shù)、Http代理請求表(t_just_auth_source)扶认。租戶和t_just_auth_config為一對一關(guān)系侨拦,和t_just_auth_source為一對多關(guān)系。

t_just_auth_config(租戶第三方登錄功能配置表)表定義:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_just_auth_config
-- ----------------------------
DROP TABLE IF EXISTS `t_just_auth_config`;
CREATE TABLE `t_just_auth_config`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
  `enabled` tinyint(1) NULL DEFAULT NULL COMMENT 'JustAuth開關(guān)',
  `enum_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義擴(kuò)展第三方登錄的配置類',
  `http_timeout` bigint(20) NULL DEFAULT NULL COMMENT 'Http請求的超時時間',
  `cache_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '緩存類型',
  `cache_prefix` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '緩存前綴',
  `cache_timeout` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '緩存超時時間',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '創(chuàng)建時間',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '創(chuàng)建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '租戶第三方登錄功能配置表' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

t_just_auth_sourc(租戶第三方登錄信息配置表)表定義:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_just_auth_source
-- ----------------------------
DROP TABLE IF EXISTS `t_just_auth_source`;
CREATE TABLE `t_just_auth_source`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
  `source_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第三方登錄的名稱',
  `source_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第三方登錄類型:默認(rèn)default  自定義custom',
  `request_class` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自定義第三方登錄的請求Class',
  `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客戶端id:對應(yīng)各平臺的appKey',
  `client_secret` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客戶端Secret:對應(yīng)各平臺的appSecret',
  `redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登錄成功后的回調(diào)地址',
  `alipay_public_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付寶公鑰:當(dāng)選擇支付寶登錄時辐宾,該值可用',
  `union_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否需要申請unionid狱从,目前只針對qq登錄',
  `stack_overflow_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Stack Overflow Key',
  `agent_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企業(yè)微信,授權(quán)方的網(wǎng)頁應(yīng)用ID',
  `user_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '企業(yè)微信第三方授權(quán)用戶類型叠纹,member|admin',
  `domain_prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '域名前綴 使用 Coding 登錄和 Okta 登錄時季研,需要傳該值。',
  `ignore_check_state` tinyint(1) NOT NULL DEFAULT 0 COMMENT '忽略校驗(yàn)code state}參數(shù)誉察,默認(rèn)不開啟与涡。',
  `scopes` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支持自定義授權(quán)平臺的 scope 內(nèi)容',
  `device_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '設(shè)備ID, 設(shè)備唯一標(biāo)識ID',
  `client_os_type` int(11) NULL DEFAULT NULL COMMENT '喜馬拉雅:客戶端操作系統(tǒng)類型,1-iOS系統(tǒng)冒窍,2-Android系統(tǒng)递沪,3-Web',
  `pack_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '喜馬拉雅:客戶端包名',
  `pkce` tinyint(1) NULL DEFAULT NULL COMMENT ' 是否開啟 PKCE 模式,該配置僅用于支持 PKCE 模式的平臺综液,針對無服務(wù)應(yīng)用款慨,不推薦使用隱式授權(quán),推薦使用 PKCE 模式',
  `auth_server_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Okta 授權(quán)服務(wù)器的 ID谬莹, 默認(rèn)為 default檩奠。',
  `ignore_check_redirect_uri` tinyint(1) NOT NULL DEFAULT 0 COMMENT '忽略校驗(yàn) {@code redirectUri} 參數(shù)桩了,默認(rèn)不開啟。',
  `proxy_type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Http代理類型',
  `proxy_host_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Http代理Host',
  `proxy_port` int(11) NULL DEFAULT NULL COMMENT 'Http代理Port',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '創(chuàng)建時間',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '創(chuàng)建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '租戶第三方登錄信息配置表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
2. 使用GitEgg代碼生成工具生成JustAuth配置信息的CRUD代碼

??我們將JustAuth配置信息管理的相關(guān)代碼和JustAuth實(shí)現(xiàn)業(yè)務(wù)邏輯的代碼分開埠戳,配置信息我們在系統(tǒng)啟動時加載到Redis緩存井誉,JustAuth在調(diào)用時,直接調(diào)用Redis緩存中的配置整胃。
??前面講過如何通過數(shù)據(jù)庫表設(shè)計生成CRUD的前后端代碼颗圣,這里不再贅述,生成好的后臺代碼我們放在gitegg-service-extension工程下屁使,和短信在岂、文件存儲等的配置放到同一工程下,作為框架的擴(kuò)展功能蛮寂。

基礎(chǔ)配置:


image.png

第三方列表:


image.png
3. 代碼生成之后蔽午,需要做初始化緩存處理,即在第三方配置服務(wù)啟動的時候酬蹋,將多租戶的配置信息初始化到Redis緩存中及老。
  • 初始化的CommandLineRunner類 InitExtensionCacheRunner.java
/**
 * 容器啟動完成加載資源權(quán)限數(shù)據(jù)到緩存
 * @author GitEgg
 */
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class InitExtensionCacheRunner implements CommandLineRunner {
    
    private final IJustAuthConfigService justAuthConfigService;
    
    private final IJustAuthSourceService justAuthSourceService;

    @Override
    public void run(String... args) {

        log.info("InitExtensionCacheRunner running");
    
    
        // 初始化第三方登錄主配置
        justAuthConfigService.initJustAuthConfigList();

        // 初始化第三方登錄 第三方配置
        justAuthSourceService.initJustAuthSourceList();


    }
}
  • 第三方登錄主配置初始化方法
    /**
     * 初始化配置表列表
     * @return
     */
    @Override
    public void initJustAuthConfigList() {
        QueryJustAuthConfigDTO queryJustAuthConfigDTO = new QueryJustAuthConfigDTO();
        queryJustAuthConfigDTO.setStatus(GitEggConstant.ENABLE);
        List<JustAuthConfigDTO> justAuthSourceInfoList = justAuthConfigMapper.initJustAuthConfigList(queryJustAuthConfigDTO);
        
        // 判斷是否開啟了租戶模式,如果開啟了范抓,那么角色權(quán)限需要按租戶進(jìn)行分類存儲
        if (enable) {
            Map<Long, List<JustAuthConfigDTO>> authSourceListMap =
                    justAuthSourceInfoList.stream().collect(Collectors.groupingBy(JustAuthConfigDTO::getTenantId));
            authSourceListMap.forEach((key, value) -> {
                String redisKey = AuthConstant.SOCIAL_TENANT_CONFIG_KEY + key;
                redisTemplate.delete(redisKey);
                addJustAuthConfig(redisKey, value);
            });
            
        } else {
            redisTemplate.delete(AuthConstant.SOCIAL_CONFIG_KEY);
            addJustAuthConfig(AuthConstant.SOCIAL_CONFIG_KEY, justAuthSourceInfoList);
        }
    }
    
    private void addJustAuthConfig(String key, List<JustAuthConfigDTO> configList) {
        Map<String, String> authConfigMap = new TreeMap<>();
        Optional.ofNullable(configList).orElse(new ArrayList<>()).forEach(config -> {
            try {
                authConfigMap.put(config.getTenantId().toString(), JsonUtils.objToJson(config));
                redisTemplate.opsForHash().putAll(key, authConfigMap);
            } catch (Exception e) {
                log.error("初始化第三方登錄失斀径瘛:{}" , e);
            }
        });

    }
  • 第三方登錄參數(shù)配置初始化方法
    /**
     * 初始化配置表列表
     * @return
     */
    @Override
    public void initJustAuthSourceList() {
        QueryJustAuthSourceDTO queryJustAuthSourceDTO = new QueryJustAuthSourceDTO();
        queryJustAuthSourceDTO.setStatus(GitEggConstant.ENABLE);
        List<JustAuthSourceDTO> justAuthSourceInfoList = justAuthSourceMapper.initJustAuthSourceList(queryJustAuthSourceDTO);
        
        // 判斷是否開啟了租戶模式,如果開啟了尉咕,那么角色權(quán)限需要按租戶進(jìn)行分類存儲
        if (enable) {
            Map<Long, List<JustAuthSourceDTO>> authSourceListMap =
                    justAuthSourceInfoList.stream().collect(Collectors.groupingBy(JustAuthSourceDTO::getTenantId));
            authSourceListMap.forEach((key, value) -> {
                String redisKey = AuthConstant.SOCIAL_TENANT_SOURCE_KEY + key;
                redisTemplate.delete(redisKey);
                addJustAuthSource(redisKey, value);
            });
            
        } else {
            redisTemplate.delete(AuthConstant.SOCIAL_SOURCE_KEY);
            addJustAuthSource(AuthConstant.SOCIAL_SOURCE_KEY, justAuthSourceInfoList);
        }
    }
    
    private void addJustAuthSource(String key, List<JustAuthSourceDTO> sourceList) {
        Map<String, String> authConfigMap = new TreeMap<>();
        Optional.ofNullable(sourceList).orElse(new ArrayList<>()).forEach(source -> {
            try {
                authConfigMap.put(source.getSourceName(), JsonUtils.objToJson(source));
                redisTemplate.opsForHash().putAll(key, authConfigMap);
            } catch (Exception e) {
                log.error("初始化第三方登錄失數:{}" , e);
            }
        });
    }
4. 引入JustAuth相關(guān)依賴jar包
  • 在gitegg-platform-bom工程中引入JustAuth包和版本璃岳,JustAuth提供了SpringBoot集成版本justAuth-spring-security-starter年缎,如果簡單使用,可以直接引用SpringBoot集成版本铃慷,我們這里因?yàn)樾枰鱿鄳?yīng)的定制修改单芜,所以引入JustAuth基礎(chǔ)工具包。
······
        <!-- JustAuth第三方登錄 -->
        <just.auth.version>1.16.5</just.auth.version>
        <!-- JustAuth SpringBoot集成 -->
        <just.auth.spring.version>1.4.0</just.auth.spring.version>
······
            <!--JustAuth第三方登錄-->
            <dependency>
                <groupId>me.zhyd.oauth</groupId>
                <artifactId>JustAuth</artifactId>
                <version>${just.auth.version}</version>
            </dependency>
            <!--JustAuth SpringBoot集成-->
            <dependency>
                <groupId>com.xkcoding.justauth</groupId>
                <artifactId>justauth-spring-boot-starter</artifactId>
                <version>${just.auth.spring.version}</version>
            </dependency>
······

  • 新建gitegg-platform-justauth工程犁柜,用于實(shí)現(xiàn)公共自定義代碼洲鸠,并在pom.xml中引入需要的jar包。
    <dependencies>
        <!-- gitegg Spring Boot自定義及擴(kuò)展 -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-boot</artifactId>
        </dependency>
        <!--JustAuth第三方登錄-->
        <dependency>
            <groupId>me.zhyd.oauth</groupId>
            <artifactId>JustAuth</artifactId>
        </dependency>
        <!--JustAuth SpringBoot集成-->
        <dependency>
            <groupId>com.xkcoding.justauth</groupId>
            <artifactId>justauth-spring-boot-starter</artifactId>
            <!-- 不使用JustAuth默認(rèn)版本-->
            <exclusions>
                <exclusion>
                    <groupId>me.zhyd.oauth</groupId>
                    <artifactId>JustAuth</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-data-redis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-autoconfigure</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-configuration-processor</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
3. 自定義實(shí)現(xiàn)獲取和實(shí)例化多租戶第三方登錄配置的AuthRequest工廠類GitEggAuthRequestFactory.java
/**
 * GitEggAuthRequestFactory工廠類
 *
 * @author GitEgg
 */
@Slf4j
@RequiredArgsConstructor
public class GitEggAuthRequestFactory {
    
    private final RedisTemplate redisTemplate;
    
    private final AuthRequestFactory authRequestFactory;
    
    private final JustAuthProperties justAuthProperties;
    
    /**
     * 是否開啟租戶模式
     */
    @Value("${tenant.enable}")
    private Boolean enable;

    public GitEggAuthRequestFactory(AuthRequestFactory authRequestFactory, RedisTemplate redisTemplate, JustAuthProperties justAuthProperties) {
        this.authRequestFactory = authRequestFactory;
        this.redisTemplate = redisTemplate;
        this.justAuthProperties = justAuthProperties;
    }

    /**
     * 返回當(dāng)前Oauth列表
     *
     * @return Oauth列表
     */
    public List<String> oauthList() {
        // 合并
        return authRequestFactory.oauthList();
    }

    /**
     * 返回AuthRequest對象
     *
     * @param source {@link AuthSource}
     * @return {@link AuthRequest}
     */
    public AuthRequest get(String source) {
        
        if (StrUtil.isBlank(source)) {
            throw new AuthException(AuthResponseStatus.NO_AUTH_SOURCE);
        }
    
        // 組裝多租戶的緩存配置key
        String authConfigKey = AuthConstant.SOCIAL_TENANT_CONFIG_KEY;
        if (enable) {
            authConfigKey += GitEggAuthUtils.getTenantId();
        } else {
            authConfigKey = AuthConstant.SOCIAL_CONFIG_KEY;
        }
    
        // 獲取主配置馋缅,每個租戶只有一個主配置
        String sourceConfigStr = (String) redisTemplate.opsForHash().get(authConfigKey, GitEggAuthUtils.getTenantId());
        AuthConfig authConfig = null;
        JustAuthSource justAuthSource = null;
        AuthRequest tenantIdAuthRequest = null;
        if (!StringUtils.isEmpty(sourceConfigStr))
        {
            try {
                // 轉(zhuǎn)為系統(tǒng)配置對象
                JustAuthConfig justAuthConfig = JsonUtils.jsonToPojo(sourceConfigStr, JustAuthConfig.class);
                // 判斷該配置是否開啟了第三方登錄
                if (justAuthConfig.getEnabled())
                {
                    // 根據(jù)配置生成StateCache
                    CacheProperties cacheProperties = new CacheProperties();
                    if (!StringUtils.isEmpty(justAuthConfig.getCacheType())
                            && !StringUtils.isEmpty(justAuthConfig.getCachePrefix())
                            && null != justAuthConfig.getCacheTimeout())
                    {
                        cacheProperties.setType(CacheProperties.CacheType.valueOf(justAuthConfig.getCacheType().toUpperCase()));
                        cacheProperties.setPrefix(justAuthConfig.getCachePrefix());
                        cacheProperties.setTimeout(Duration.ofMinutes(justAuthConfig.getCacheTimeout()));
                    }
                    else
                    {
                        cacheProperties = justAuthProperties.getCache();
                    }
                    
    
                    GitEggRedisStateCache gitEggRedisStateCache =
                            new GitEggRedisStateCache(redisTemplate, cacheProperties, enable);
                    
                    // 組裝多租戶的第三方配置信息key
                    String authSourceKey = AuthConstant.SOCIAL_TENANT_SOURCE_KEY;
                    if (enable) {
                        authSourceKey += GitEggAuthUtils.getTenantId();
                    } else {
                        authSourceKey = AuthConstant.SOCIAL_SOURCE_KEY;
                    }
                    // 獲取具體的第三方配置信息
                    String sourceAuthStr = (String)redisTemplate.opsForHash().get(authSourceKey, source.toUpperCase());
                    if (!StringUtils.isEmpty(sourceAuthStr))
                    {
                        // 轉(zhuǎn)為系統(tǒng)配置對象
                        justAuthSource = JsonUtils.jsonToPojo(sourceAuthStr, JustAuthSource.class);
                        authConfig = BeanCopierUtils.copyByClass(justAuthSource, AuthConfig.class);
                        // 組裝scopes,因?yàn)橄到y(tǒng)配置的是逗號分割的字符串
                        if (!StringUtils.isEmpty(justAuthSource.getScopes()))
                        {
                            String[] scopes = justAuthSource.getScopes().split(StrUtil.COMMA);
                            authConfig.setScopes(Arrays.asList(scopes));
                        }
                        // 設(shè)置proxy
                        if (StrUtil.isAllNotEmpty(justAuthSource.getProxyType(), justAuthSource.getProxyHostName())
                                && null !=  justAuthSource.getProxyPort())
                        {
                            JustAuthProperties.JustAuthProxyConfig proxyConfig = new JustAuthProperties.JustAuthProxyConfig();
                            proxyConfig.setType(justAuthSource.getProxyType());
                            proxyConfig.setHostname(justAuthSource.getProxyHostName());
                            proxyConfig.setPort(justAuthSource.getProxyPort());
                            if (null != proxyConfig) {
                                HttpConfig httpConfig = HttpConfig.builder().timeout(justAuthSource.getProxyPort()).proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort()))).build();
                                if (null != justAuthConfig.getHttpTimeout())
                                {
                                    httpConfig.setTimeout(justAuthConfig.getHttpTimeout());
                                }
                                authConfig.setHttpConfig(httpConfig);
                            }
                        }
                        // 組裝好配置后扒腕,從配置生成request,判斷是默認(rèn)的第三方登錄還是自定義第三方登錄
                        if (SourceTypeEnum.DEFAULT.key.equals(justAuthSource.getSourceType()))
                        {
                            tenantIdAuthRequest = this.getDefaultRequest(source, authConfig, gitEggRedisStateCache);
                        }
                        else if (!StringUtils.isEmpty(justAuthConfig.getEnumClass()) && SourceTypeEnum.CUSTOM.key.equals(justAuthSource.getSourceType()))
                        {
                            try {
                                Class enumConfigClass = Class.forName(justAuthConfig.getEnumClass());
                                tenantIdAuthRequest = this.getExtendRequest(enumConfigClass, source, (ExtendProperties.ExtendRequestConfig) authConfig, gitEggRedisStateCache);
                            } catch (ClassNotFoundException e) {
                                log.error("初始化自定義第三方登錄時發(fā)生異常:{}", e);
                            }
                        }
                    }
                }
            } catch (Exception e) {
                log.error("獲取第三方登錄時發(fā)生異常:{}", e);
            }
        }
        
        if (null == tenantIdAuthRequest)
        {
            tenantIdAuthRequest =  authRequestFactory.get(source);
        }

        return tenantIdAuthRequest;
    }
    
    /**
     * 獲取單個的request
     * @param source
     * @return
     */
    private AuthRequest getDefaultRequest(String source, AuthConfig authConfig, GitEggRedisStateCache gitEggRedisStateCache) {
        AuthDefaultSource authDefaultSource;
        try {
            authDefaultSource = EnumUtil.fromString(AuthDefaultSource.class, source.toUpperCase());
        } catch (IllegalArgumentException var4) {
            return null;
        }
        
        // 從緩存獲取租戶單獨(dú)配置
        switch(authDefaultSource) {
            case GITHUB:
                return new AuthGithubRequest(authConfig, gitEggRedisStateCache);
            case WEIBO:
                return new AuthWeiboRequest(authConfig, gitEggRedisStateCache);
            case GITEE:
                return new AuthGiteeRequest(authConfig, gitEggRedisStateCache);
            case DINGTALK:
                return new AuthDingTalkRequest(authConfig, gitEggRedisStateCache);
            case DINGTALK_ACCOUNT:
                return new AuthDingTalkAccountRequest(authConfig, gitEggRedisStateCache);
            case BAIDU:
                return new AuthBaiduRequest(authConfig, gitEggRedisStateCache);
            case CSDN:
                return new AuthCsdnRequest(authConfig, gitEggRedisStateCache);
            case CODING:
                return new AuthCodingRequest(authConfig, gitEggRedisStateCache);
            case OSCHINA:
                return new AuthOschinaRequest(authConfig, gitEggRedisStateCache);
            case ALIPAY:
                return new AuthAlipayRequest(authConfig, gitEggRedisStateCache);
            case QQ:
                return new AuthQqRequest(authConfig, gitEggRedisStateCache);
            case WECHAT_OPEN:
                return new AuthWeChatOpenRequest(authConfig, gitEggRedisStateCache);
            case WECHAT_MP:
                return new AuthWeChatMpRequest(authConfig, gitEggRedisStateCache);
            case WECHAT_ENTERPRISE:
                return new AuthWeChatEnterpriseQrcodeRequest(authConfig, gitEggRedisStateCache);
            case WECHAT_ENTERPRISE_WEB:
                return new AuthWeChatEnterpriseWebRequest(authConfig, gitEggRedisStateCache);
            case TAOBAO:
                return new AuthTaobaoRequest(authConfig, gitEggRedisStateCache);
            case GOOGLE:
                return new AuthGoogleRequest(authConfig, gitEggRedisStateCache);
            case FACEBOOK:
                return new AuthFacebookRequest(authConfig, gitEggRedisStateCache);
            case DOUYIN:
                return new AuthDouyinRequest(authConfig, gitEggRedisStateCache);
            case LINKEDIN:
                return new AuthLinkedinRequest(authConfig, gitEggRedisStateCache);
            case MICROSOFT:
                return new AuthMicrosoftRequest(authConfig, gitEggRedisStateCache);
            case MI:
                return new AuthMiRequest(authConfig, gitEggRedisStateCache);
            case TOUTIAO:
                return new AuthToutiaoRequest(authConfig, gitEggRedisStateCache);
            case TEAMBITION:
                return new AuthTeambitionRequest(authConfig, gitEggRedisStateCache);
            case RENREN:
                return new AuthRenrenRequest(authConfig, gitEggRedisStateCache);
            case PINTEREST:
                return new AuthPinterestRequest(authConfig, gitEggRedisStateCache);
            case STACK_OVERFLOW:
                return new AuthStackOverflowRequest(authConfig, gitEggRedisStateCache);
            case HUAWEI:
                return new AuthHuaweiRequest(authConfig, gitEggRedisStateCache);
            case GITLAB:
                return new AuthGitlabRequest(authConfig, gitEggRedisStateCache);
            case KUJIALE:
                return new AuthKujialeRequest(authConfig, gitEggRedisStateCache);
            case ELEME:
                return new AuthElemeRequest(authConfig, gitEggRedisStateCache);
            case MEITUAN:
                return new AuthMeituanRequest(authConfig, gitEggRedisStateCache);
            case TWITTER:
                return new AuthTwitterRequest(authConfig, gitEggRedisStateCache);
            case FEISHU:
                return new AuthFeishuRequest(authConfig, gitEggRedisStateCache);
            case JD:
                return new AuthJdRequest(authConfig, gitEggRedisStateCache);
            case ALIYUN:
                return new AuthAliyunRequest(authConfig, gitEggRedisStateCache);
            case XMLY:
                return new AuthXmlyRequest(authConfig, gitEggRedisStateCache);
            case AMAZON:
                return new AuthAmazonRequest(authConfig, gitEggRedisStateCache);
            case SLACK:
                return new AuthSlackRequest(authConfig, gitEggRedisStateCache);
            case LINE:
                return new AuthLineRequest(authConfig, gitEggRedisStateCache);
            case OKTA:
                return new AuthOktaRequest(authConfig, gitEggRedisStateCache);
            default:
                return null;
        }
    }
    
    
    private AuthRequest getExtendRequest(Class clazz, String source, ExtendProperties.ExtendRequestConfig extendRequestConfig, GitEggRedisStateCache gitEggRedisStateCache) {
        String upperSource = source.toUpperCase();
        try {
            EnumUtil.fromString(clazz, upperSource);
        } catch (IllegalArgumentException var8) {
            return null;
        }
        if (extendRequestConfig != null) {
            Class<? extends AuthRequest> requestClass = extendRequestConfig.getRequestClass();
            if (requestClass != null) {
                return (AuthRequest) ReflectUtil.newInstance(requestClass, new Object[]{extendRequestConfig, gitEggRedisStateCache});
            }
        }
        return null;
    }
}
4. 登錄后注冊或綁定用戶

??實(shí)現(xiàn)了第三方登錄功能碳想,我們自己的系統(tǒng)也需要做相應(yīng)的用戶匹配砸泛,通過OAuth2協(xié)議我們可以了解到铅搓,單點(diǎn)登錄成功后可以獲取第三方系統(tǒng)的用戶信息司恳,當(dāng)然扇商,具體獲取到第三方用戶的哪些信息是由第三方系統(tǒng)決定的腿时。所以目前大多數(shù)系統(tǒng)平臺再第三方登錄成功之后傀缩,都會顯示用戶注冊或綁定頁面后德,將第三方用戶和自有系統(tǒng)平臺用戶進(jìn)行綁定。那么在下一次第三方登錄成功之后栖雾,就會自動匹配到自有系統(tǒng)的用戶楞抡,進(jìn)一步的獲取到該用戶在自有系統(tǒng)的權(quán)限、菜單等析藕。

JustAuth官方提供的賬戶整合流程圖:


JustAuth整合現(xiàn)有用戶系統(tǒng)

??我們通常的第三方登錄業(yè)務(wù)流程是點(diǎn)擊登錄召廷,獲取到第三方授權(quán)時,會去查詢自有系統(tǒng)數(shù)據(jù)是否有匹配的用戶账胧,如果有柱恤,則自動登錄到后臺,如果沒有找爱,則跳轉(zhuǎn)到賬號綁定或者注冊頁面梗顺,進(jìn)行賬戶綁定或者注冊。我們將此業(yè)務(wù)流程放到gitegg-oauth微服務(wù)中去實(shí)現(xiàn)车摄,新建SocialController類:

/**
 * 第三方登錄
 * @author GitEgg
 */
@Slf4j
@RestController
@RequestMapping("/social")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SocialController {
    
    private final GitEggAuthRequestFactory factory;
    
    private final IJustAuthFeign justAuthFeign;
    
    private final IUserFeign userFeign;
    
    private final ISmsFeign smsFeign;
    
    @Value("${system.secret-key}")
    private String secretKey;
    
    @Value("${system.secret-key-salt}")
    private String secretKeySalt;
    
    private final RedisTemplate redisTemplate;
    
    /**
     * 密碼最大嘗試次數(shù)
     */
    @Value("${system.maxTryTimes}")
    private int maxTryTimes;
    
    /**
     * 鎖定時間寺谤,單位 秒
     */
    @Value("${system.maxTryTimes}")
    private long maxLockTime;
    
    /**
     * 第三方登錄緩存時間,單位 秒
     */
    @Value("${system.socialLoginExpiration}")
    private long socialLoginExpiration;

    @GetMapping
    public List<String> list() {
        return factory.oauthList();
    }
    
    /**
     * 獲取到對應(yīng)類型的登錄url
     * @param type
     * @return
     */
    @GetMapping("/login/{type}")
    public Result login(@PathVariable String type) {
        AuthRequest authRequest = factory.get(type);
        return Result.data(authRequest.authorize(AuthStateUtils.createState()));
    }
    
    /**
     * 保存或更新用戶數(shù)據(jù)吮播,并進(jìn)行判斷是否進(jìn)行注冊或綁定
     * @param type
     * @param callback
     * @return
     */
    @RequestMapping("/{type}/callback")
    public Result login(@PathVariable String type, AuthCallback callback) {
        AuthRequest authRequest = factory.get(type);
        AuthResponse response = authRequest.login(callback);
        if (response.ok())
        {
            AuthUser authUser = (AuthUser) response.getData();
            JustAuthSocialInfoDTO justAuthSocialInfoDTO = BeanCopierUtils.copyByClass(authUser, JustAuthSocialInfoDTO.class);
            BeanCopierUtils.copyByObject(authUser.getToken(), justAuthSocialInfoDTO);
            // 獲取到第三方用戶信息后变屁,先進(jìn)行保存或更新
            Result<Object> createResult = justAuthFeign.userCreateOrUpdate(justAuthSocialInfoDTO);
            if(createResult.isSuccess() && null != createResult.getData())
            {
                Long socialId = Long.parseLong((String)createResult.getData());
                // 判斷此第三方用戶是否被綁定到系統(tǒng)用戶
                Result<Object> bindResult = justAuthFeign.userBindQuery(socialId);
                // 這里需要處理返回消息,前端需要根據(jù)返回是否已經(jīng)綁定好的消息來判斷
                // 將socialId進(jìn)行加密返回
                DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
                // 這里將source+uuid通過des加密作為key返回到前臺
                String socialKey = authUser.getSource() + StrPool.UNDERLINE + authUser.getUuid();
                // 將socialKey放入緩存意狠,默認(rèn)有效期2個小時粟关,如果2個小時未完成驗(yàn)證,那么操作失效环戈,重新獲取闷板,在system:socialLoginExpiration配置
                redisTemplate.opsForValue().set(AuthConstant.SOCIAL_VALIDATION_PREFIX + socialKey, createResult.getData(), socialLoginExpiration,
                        TimeUnit.SECONDS);
                String desSocialKey = des.encryptHex(socialKey);
                bindResult.setData(desSocialKey);
                // 這里返回的成功是請求成功,里面放置的result是是否有綁定用戶的成功
                return Result.data(bindResult);
            }
            return Result.error("獲取第三方用戶綁定信息失敗");
        }
        else
        {
            throw new BusinessException(response.getMsg());
        }
    }
    
    /**
     * 綁定用戶手機(jī)號
     * 這里不走手機(jī)號登錄的流程院塞,因?yàn)槿绻謾C(jī)號不存在那么可以直接創(chuàng)建一個用戶并進(jìn)行綁定
     */
    @PostMapping("/bind/mobile")
    @ApiOperation(value = "綁定用戶手機(jī)號")
    public Result<?> bindMobile(@Valid @RequestBody SocialBindMobileDTO socialBind) {
        Result<?> smsResult = smsFeign.checkSmsVerificationCode(socialBind.getSmsCode(), socialBind.getPhoneNumber(), socialBind.getCode());
        // 判斷短信驗(yàn)證是否成功
        if (smsResult.isSuccess() && null != smsResult.getData() && (Boolean)smsResult.getData()) {
            // 解密前端傳來的socialId
            DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
            String desSocialKey = des.decryptStr(socialBind.getSocialKey());
    
            // 將socialKey放入緩存遮晚,默認(rèn)有效期2個小時,如果2個小時未完成驗(yàn)證拦止,那么操作失效县遣,重新獲取,在system:socialLoginExpiration配置
            String desSocialId = (String)redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
            
            // 查詢第三方用戶信息
            Result<Object> justAuthInfoResult = justAuthFeign.querySocialInfo(Long.valueOf(desSocialId));
    
            if (null == justAuthInfoResult || !justAuthInfoResult.isSuccess() || null == justAuthInfoResult.getData())
            {
                throw new BusinessException("未查詢到第三方用戶信息汹族,請返回到登錄頁重試");
            }
    
            JustAuthSocialInfoDTO justAuthSocialInfoDTO = BeanUtil.copyProperties(justAuthInfoResult.getData(), JustAuthSocialInfoDTO.class);
            
           // 查詢用戶是否存在萧求,如果存在,那么直接調(diào)用綁定接口
           Result<Object> result = userFeign.queryUserByPhone(socialBind.getPhoneNumber());
           Long userId;
           // 判斷返回信息
           if (null != result && result.isSuccess() && null != result.getData()) {
               GitEggUser gitEggUser = BeanUtil.copyProperties(result.getData(), GitEggUser.class);
               userId = gitEggUser.getId();
           }
           else
           {
               // 如果用戶不存在顶瞒,那么調(diào)用新建用戶接口夸政,并綁定
               UserAddDTO userAdd = new UserAddDTO();
               userAdd.setAccount(socialBind.getPhoneNumber());
               userAdd.setMobile(socialBind.getPhoneNumber());
               userAdd.setNickname(justAuthSocialInfoDTO.getNickname());
               userAdd.setPassword(StringUtils.isEmpty(justAuthSocialInfoDTO.getUnionId()) ? justAuthSocialInfoDTO.getUuid() : justAuthSocialInfoDTO.getUnionId());
               userAdd.setStatus(GitEggConstant.UserStatus.ENABLE);
               userAdd.setAvatar(justAuthSocialInfoDTO.getAvatar());
               userAdd.setEmail(justAuthSocialInfoDTO.getEmail());
               userAdd.setStreet(justAuthSocialInfoDTO.getLocation());
               userAdd.setComments(justAuthSocialInfoDTO.getRemark());
               Result<?> resultUserAdd = userFeign.userAdd(userAdd);
               if (null != resultUserAdd && resultUserAdd.isSuccess() && null != resultUserAdd.getData())
               {
                   userId = Long.parseLong((String) resultUserAdd.getData());
               }
               else
               {
                   // 如果添加失敗,則返回失敗信息
                   return resultUserAdd;
               }
           }
            // 執(zhí)行綁定操作
            return justAuthFeign.userBind(Long.valueOf(desSocialId), userId);
        }
        return smsResult;
    }
    
    /**
     * 綁定賬號
     * 這里只有綁定操作搁拙,沒有創(chuàng)建用戶操作
     */
    @PostMapping("/bind/account")
    @ApiOperation(value = "綁定用戶賬號")
    public Result<?> bindAccount(@Valid @RequestBody SocialBindAccountDTO socialBind) {
        // 查詢用戶是否存在秒梳,如果存在法绵,那么直接調(diào)用綁定接口
        Result<?> result = userFeign.queryUserByAccount(socialBind.getUsername());
        // 判斷返回信息
        if (null != result && result.isSuccess() && null != result.getData()) {
            
            GitEggUser gitEggUser = BeanUtil.copyProperties(result.getData(), GitEggUser.class);
            // 必須添加次數(shù)驗(yàn)證,和登錄一樣酪碘,超過最大驗(yàn)證次數(shù)那么直接鎖定賬戶
            // 從Redis獲取賬號密碼錯誤次數(shù)
            Object lockTimes = redisTemplate.boundValueOps(AuthConstant.LOCK_ACCOUNT_PREFIX + gitEggUser.getId()).get();
            // 判斷賬號密碼輸入錯誤幾次朋譬,如果輸入錯誤多次,則鎖定賬號
            if(null != lockTimes && (int)lockTimes >= maxTryTimes){
                throw new BusinessException("密碼嘗試次數(shù)過多兴垦,請使用其他方式綁定");
            }
    
            PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            String password = AuthConstant.BCRYPT + gitEggUser.getAccount() +  DigestUtils.md5DigestAsHex(socialBind.getPassword().getBytes());
            // 驗(yàn)證賬號密碼是否正確
            if ( passwordEncoder.matches(password, gitEggUser.getPassword()))
            {
                // 解密前端傳來的socialId
                DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
                String desSocialKey = des.decryptStr(socialBind.getSocialKey());
                // 將socialKey放入緩存徙赢,默認(rèn)有效期2個小時,如果2個小時未完成驗(yàn)證探越,那么操作失效狡赐,重新獲取,在system:socialLoginExpiration配置
                String desSocialId = (String)redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
          
                // 執(zhí)行綁定操作
                return justAuthFeign.userBind(Long.valueOf(desSocialId), gitEggUser.getId());
            }
            else
            {
                // 增加鎖定次數(shù)
                redisTemplate.boundValueOps(AuthConstant.LOCK_ACCOUNT_PREFIX + gitEggUser.getId()).increment(GitEggConstant.Number.ONE);
                redisTemplate.expire(AuthConstant.LOCK_ACCOUNT_PREFIX +gitEggUser.getId(), maxLockTime , TimeUnit.SECONDS);
                throw new BusinessException("賬號或密碼錯誤");
            }
        }
        else
        {
            throw new BusinessException("賬號不存在");
        }
    }

}
5. 所有的配置和綁定注冊功能實(shí)現(xiàn)之后钦幔,我們還需要實(shí)現(xiàn)關(guān)鍵的一步枕屉,就是自定義實(shí)現(xiàn)OAuth2的第三方登錄模式SocialTokenGranter,在第三方授權(quán)之后鲤氢,通過此模式進(jìn)行登錄搀擂,自定義實(shí)現(xiàn)之后,記得t_oauth_client_details表需增加social授權(quán)卷玉。

SocialTokenGranter.java

/**
 * 第三方登錄模式
 * @author GitEgg
 */
public class SocialTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "social";

    private final AuthenticationManager authenticationManager;

    private UserDetailsService userDetailsService;
    
    private IJustAuthFeign justAuthFeign;

    private RedisTemplate redisTemplate;

    private String captchaType;
    
    private String secretKey;
    
    private String secretKeySalt;

    public SocialTokenGranter(AuthenticationManager authenticationManager,
                              AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                              OAuth2RequestFactory requestFactory, RedisTemplate redisTemplate, IJustAuthFeign justAuthFeign,
                              UserDetailsService userDetailsService, String captchaType, String secretKey, String secretKeySalt) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.redisTemplate = redisTemplate;
        this.captchaType = captchaType;
        this.secretKey = secretKey;
        this.secretKeySalt = secretKeySalt;
        this.justAuthFeign = justAuthFeign;
        this.userDetailsService = userDetailsService;
    }

    protected SocialTokenGranter(AuthenticationManager authenticationManager,
                                 AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                                 OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());

        String socialKey = parameters.get(TokenConstant.SOCIAL_KEY);
        // Protect from downstream leaks of password
        parameters.remove(TokenConstant.SOCIAL_KEY);
    
        // 校驗(yàn)socialId
        String socialId;
        try {
            // 將socialId進(jìn)行加密返回
            DES des = new DES(Mode.CTS, Padding.PKCS5Padding, secretKey.getBytes(), secretKeySalt.getBytes());
            String desSocialKey = des.decryptStr(socialKey);
            // 獲取緩存中的key
            socialId = (String) redisTemplate.opsForValue().get(AuthConstant.SOCIAL_VALIDATION_PREFIX + desSocialKey);
        }
        catch (Exception e)
        {
            throw new InvalidGrantException("第三方登錄驗(yàn)證已失效哨颂,請返回登錄頁重新操作");
        }
        
        if (StringUtils.isEmpty(socialId))
        {
            throw new InvalidGrantException("第三方登錄驗(yàn)證已失效,請返回登錄頁重新操作");
        }
    
        // 校驗(yàn)userId
        String userId;
        try {
            Result<Object> socialResult = justAuthFeign.userBindQuery(Long.parseLong(socialId));
            if (null == socialResult || StringUtils.isEmpty(socialResult.getData())) {
                throw new InvalidGrantException("操作失敗相种,請返回登錄頁重新操作");
            }
            userId = (String) socialResult.getData();
        }
        catch (Exception e)
        {
            throw new InvalidGrantException("操作失敗威恼,請返回登錄頁重新操作");
        }
    
        if (StringUtils.isEmpty(userId))
        {
            throw new InvalidGrantException("操作失敗,請返回登錄頁重新操作");
        }
        
        // 這里是通過用戶id查詢用戶信息
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);

        Authentication userAuth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }

}
6. 后臺處理完成之后寝并,前端VUE也需要做回調(diào)處理

??因?yàn)槭乔昂蠖朔蛛x的項目箫措,我們這里需要將第三方回調(diào)接口配置在vue頁面,前端頁面根據(jù)賬戶信息判斷是直接登錄還是進(jìn)行綁定或者注冊等操作食茎。新建SocialCallback.vue用于處理前端第三方登錄授權(quán)后的回調(diào)操作蒂破。
SocialCallback.vue

<template>
  <div>
  </div>
</template>
<script>
import { socialLoginCallback } from '@/api/login'
import { mapActions } from 'vuex'
export default {
  name: 'SocialCallback',
  created () {
    this.$loading.show({ tip: '登錄中......' })
    const query = this.$route.query
    const socialType = this.$route.params.socialType
    this.socialCallback(socialType, query)
  },
  methods: {
    ...mapActions(['Login']),
    getUrlKey: function (name) {
       // eslint-disable-next-line no-sparse-arrays
       return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(window.opener.location.href) || [, ''])[1].replace(/\+/g, '%20')) || null
    },
    socialCallback (socialType, parameter) {
      const that = this
      socialLoginCallback(socialType, parameter).then(res => {
        that.$loading.hide()
        const bindResult = res.data
        if (bindResult && bindResult !== '') {
          if (bindResult.success && bindResult.data) {
            // 授權(quán)后發(fā)現(xiàn)已綁定,那么直接調(diào)用第三方登錄
            this.socialLogin(bindResult.data)
          } else if (bindResult.code === 601) {
            // 授權(quán)后沒有綁定則跳轉(zhuǎn)到綁定界面
            that.$router.push({ name: 'socialBind', query: { redirect: this.getUrlKey('redirect'), key: bindResult.data } })
          } else if (bindResult.code === 602) {
            // 該賬號已綁定多個賬號别渔,請聯(lián)系系統(tǒng)管理員,或者到個人中心解綁
            this.$notification['error']({
              message: '錯誤',
              description: ((res.response || {}).data || {}).message || '該賬號已綁定多個賬號惧互,請聯(lián)系系統(tǒng)管理員哎媚,或者到個人中心解綁',
              duration: 4
            })
          } else {
            // 提示獲取第三方登錄失敗
            this.$notification['error']({
              message: '錯誤',
              description: '第三方登錄失敗,請稍后再試',
              duration: 4
            })
          }
        } else {
          // 提示獲取第三方登錄失敗
            this.$notification['error']({
              message: '錯誤',
              description: '第三方登錄失敗喊儡,請稍后再試',
              duration: 4
            })
        }
      })
    },
    // 第三方登錄后回調(diào)
    socialLogin (key) {
      const { Login } = this
      // 執(zhí)行登錄操作
      const loginParams = {
        grant_type: 'social',
        social_key: key
      }
      this.$loading.show({ tip: '登錄中......' })
      Login(loginParams)
        .then((res) => this.loginSuccess(res))
        .catch(err => this.loginError(err))
        .finally(() => {
           this.$loading.hide()
           if (this.getUrlKey('redirect')) {
              window.opener.location.href = window.opener.location.origin + this.getUrlKey('redirect')
           } else {
              window.opener.location.reload()
           }
           window.close()
       })
    },
    loginSuccess (res) {
      this.$notification['success']({
         message: '提示',
         description: '第三方登錄成功',
         duration: 4
      })
    },
    loginError (err) {
      this.$notification['error']({
        message: '錯誤',
        description: ((err.response || {}).data || {}).message || '請求出現(xiàn)錯誤拨与,請稍后再試',
        duration: 4
      })
    }
  }
}
</script>
<style>
</style>

二、登錄和綁定測試

JustAuth官方提供了詳細(xì)的第三方登錄的使用指南艾猜,按照其介紹买喧,到需要的第三方網(wǎng)站申請捻悯,然后進(jìn)行配置即可,這里只展示GitHub的登錄測試步驟淤毛。
1今缚、按照官方提供的注冊申請步驟,獲取到GitHub的client-id和client-secret并配置回調(diào)地址redirect-uri

image.png
  • Nacos配置
      client-id: 59ced49784f3cebfb208
      client-secret: 807f52cc33a1aae07f97521b5501adc6f36375c8
      redirect-uri: http://192.168.0.2:8000/social/github/callback
      ignore-check-state: false
  • 或者使用多租戶系統(tǒng)配置 低淡,每個租戶僅允許有一個主配置


    image.png

    image.png

    2姓言、登錄頁添加Github登錄鏈接

      <div class="user-login-other">
        <span>{{ $t('user.login.sign-in-with') }}</span>
        <a @click="openSocialLogin('wechat_open')">
          <a-icon class="item-icon"
                  type="wechat"></a-icon>
        </a>
        <a @click="openSocialLogin('qq')">
          <a-icon class="item-icon"
                  type="qq"></a-icon>
        </a>
        <a @click="openSocialLogin('github')">
          <a-icon class="item-icon"
                  type="github"></a-icon>
        </a>
        <a @click="openSocialLogin('dingtalk')">
          <a-icon class="item-icon"
                  type="dingding"></a-icon>
        </a>
        <a class="register"
           @click="openRegister"
        >{{ $t('user.login.signup') }}
        </a>
      </div>
登錄頁

3、點(diǎn)擊登錄蔗蹋,如果此時GitHub賬號沒有登錄過何荚,則跳轉(zhuǎn)到綁定或者注冊賬號界面


image.png

4、輸入手機(jī)號+驗(yàn)證碼或者賬號+密碼猪杭,即可進(jìn)入到登錄前的頁面餐塘。使用手機(jī)號+驗(yàn)證碼的模式,如果系統(tǒng)不存在賬號皂吮,可以直接注冊新賬號并登錄唠倦。
5、JustAuth支持的第三方登錄列表涮较,只需到相應(yīng)第三方登錄申請即可稠鼻,下面圖片取自JustAuth官網(wǎng):


932e4b2a80e72e310db33ece19caf80.png
GitEgg-Cloud是一款基于SpringCloud整合搭建的企業(yè)級微服務(wù)應(yīng)用開發(fā)框架,開源項目地址:

Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg
歡迎感興趣的小伙伴Star支持一下狂票。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末候齿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子闺属,更是在濱河造成了極大的恐慌慌盯,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掂器,死亡現(xiàn)場離奇詭異亚皂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)国瓮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門灭必,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乃摹,你說我怎么就攤上這事禁漓。” “怎么了孵睬?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵播歼,是天一觀的道長。 經(jīng)常有香客問我掰读,道長秘狞,這世上最難降的妖魔是什么叭莫? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮烁试,結(jié)果婚禮上雇初,老公的妹妹穿的比我還像新娘。我一直安慰自己廓潜,他們只是感情好抵皱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辩蛋,像睡著了一般呻畸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悼院,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天伤为,我揣著相機(jī)與錄音,去河邊找鬼据途。 笑死绞愚,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颖医。 我是一名探鬼主播位衩,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼熔萧!你這毒婦竟也來了糖驴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤佛致,失蹤者是張志新(化名)和其女友劉穎贮缕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俺榆,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡感昼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罐脊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定嗓。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖爹殊,靈堂內(nèi)的尸體忽然破棺而出蜕乡,到底是詐尸還是另有隱情,我是刑警寧澤梗夸,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站号醉,受9級特大地震影響反症,放射性物質(zhì)發(fā)生泄漏辛块。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一铅碍、第九天 我趴在偏房一處隱蔽的房頂上張望润绵。 院中可真熱鬧,春花似錦胞谈、人聲如沸尘盼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卿捎。三九已至,卻和暖如春径密,著一層夾襖步出監(jiān)牢的瞬間午阵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工享扔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留底桂,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓惧眠,卻偏偏與公主長得像籽懦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子氛魁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容