JustAuth扩展:支持自动获得回调域名、使用redission作为Cache

当前使用的版本:

复制代码
just_auth_version = '1.16.6'
just_auth_starter_version = '1.4.0'
复制代码
"me.zhyd.oauth:JustAuth:${just_auth_version}", //多渠道登录
"com.xkcoding.justauth:justauth-spring-boot-starter:${just_auth_starter_version}" //启动器

在JustAuth整合过程中,遇到两个功能扩展:

1)JustAuth默认使用sping-data-redis作为缓存接口,当前系统没有使用该redis驱动,由于已经使用了redission,不希望引入更多的架构。故需要扩展JustAuthCache接口

2)JustAuth配置的登录回调地址必须写完整的域名。本着从哪里来,回哪里去的原则,回调的域名地址,95%需求都会跟请求的域名地址一致。这样只需要在请求时获得请求的域名地址即可,不用在配置文件里额外配置。让配置文件的redirect-uri真正成为URI而不是URL

自定义缓存

自定义缓存按照文档的扩展即可,项目使用redission,如下定义一个CacheBean

java 复制代码
package org.ccframe.commons.auth;

import me.zhyd.oauth.cache.AuthStateCache;
import org.ccframe.config.GlobalEx;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;

import java.util.concurrent.TimeUnit;

/**
 * JustAuth三方登录缓存管理.
 * @author Jim 2024/03/08
 */
public class CcAuthStateCache implements AuthStateCache {

    private RedissonClient redissonClient;

    public CcAuthStateCache(RedissonClient redissonClient){
        this.redissonClient = redissonClient;
    }

    /**
     * 存入缓存
     *
     * @param key   缓存key
     * @param value 缓存内容
     */
    @Override
    public void cache(String key, String value) {
        RBucket<String> bucket = redissonClient.getBucket(GlobalEx.CACHEREGION_THIRD_AUTH + key);
        bucket.set(value, 10, TimeUnit.MINUTES); // 10分钟还无法三方绑定的登录,则无法进行绑定
    }

    /**
     * 存入缓存
     *
     * @param key     缓存key
     * @param value   缓存内容
     * @param timeout 指定缓存过期时间(毫秒)
     */
    @Override
    public void cache(String key, String value, long timeout) {
        RBucket<String> bucket = redissonClient.getBucket(GlobalEx.CACHEREGION_THIRD_AUTH + key, StringCodec.INSTANCE);
        bucket.set(value, timeout, TimeUnit.MILLISECONDS);
    }

    /**
     * 获取缓存内容
     *
     * @param key 缓存key
     * @return 缓存内容
     */
    @Override
    public String get(String key) {
        RBucket<String> bucket = redissonClient.getBucket(GlobalEx.CACHEREGION_THIRD_AUTH + key, StringCodec.INSTANCE);
        return bucket.get();
    }

    /**
     * 是否存在key,如果对应key的value值已过期,也返回false
     *
     * @param key 缓存key
     * @return true:存在key,并且value没过期;false:key不存在或者已过期
     */
    @Override
    public boolean containsKey(String key) {
        return redissonClient.getBucket(GlobalEx.CACHEREGION_THIRD_AUTH + key).isExists();
    }
}

实现真正的REDIRECT URI

先看看实现后的效果,整合后,CcFrame的公共配置只需要如下配置:

java 复制代码
justauth:
  enabled: false #原来的关闭掉,因为自己写了个新的CcAuthRequestFactory,使用下面的开关
  cc-enabled: true
  cache:
    type: custom
  type:
    QQ: #前台登录
      client-id: ${app.third-oauth.QQ.client-id:}
      client-secret: ${app.third-oauth.QQ.client-secret:}
      redirect-uri: /api/common/thirdOauthCallback/qq
      union-id: false
    GITEE: #后台登录
      client-id: ${app.third-oauth.GITEE.client-id:}
      client-secret: ${app.third-oauth.GITEE.client-secret:}
      redirect-uri: /admin/common/thirdOauthCallback/gitee
    DINGTALK: #后台登录
      client-id: ${app.third-oauth.DINGTALK.client-id:}
      client-secret: ${app.third-oauth.DINGTALK.client-secret:}
      redirect-uri: /admin/common/thirdOauthCallback/dingtalk

redirect-uri不关心回调地址从哪里来,这样便省去了不同项目域名地址之间差异,我只关心URI

而项目里的配置则更简单:

java 复制代码
app:
  third-oauth:
    GITEE:
      client-id: ??
      client-secret: ??

即可针对不同的项目及环境实现接入。

扩展点如下:

针对不支持URI的问题,本打算extend AuthRequestFactory的,结果发现大部分方法都private了,不方便扩展。再检查下用法发现引入的地方是在用户代码部分,因此索性复制了重写了

java 复制代码
/*
 * Copyright (c) 2019-2029, xkcoding & Yangkai.Shen & 沈扬凯 (237497819@qq.com & xkcoding.com).
 * <p>
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl.html
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.ccframe.commons.auth;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.xkcoding.http.config.HttpConfig;
import com.xkcoding.justauth.autoconfigure.ExtendProperties;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.request.*;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * private引入太多,直接复制了改了.
 *
 * <p>
 * AuthRequest工厂类
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-07-22 14:21
 */
@Slf4j
@RequiredArgsConstructor
public class CcAuthRequestFactory {
    private final JustAuthProperties properties;
    private final AuthStateCache authStateCache;

    /**
     * 返回当前Oauth列表
     *
     * @return Oauth列表
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public List<String> oauthList() {
        // 默认列表
        List<String> defaultList = new ArrayList<>(properties.getType().keySet());
        // 扩展列表
        List<String> extendList = new ArrayList<>();
        ExtendProperties extend = properties.getExtend();
        if (null != extend) {
            Class enumClass = extend.getEnumClass();
            List<String> names = EnumUtil.getNames(enumClass);
            // 扩展列表
            extendList = extend.getConfig()
                    .keySet()
                    .stream()
                    .filter(x -> names.contains(x.toUpperCase()))
                    .map(String::toUpperCase)
                    .collect(Collectors.toList());
        }

        // 合并
        return (List<String>) CollUtil.addAll(defaultList, extendList);
    }

    /**
     * 返回AuthRequest对象
     *
     * @param source {@link AuthSource}
     * @param requestBaseUrl redirectUri 请求URI的前缀,这样配置里只需要写URI了
     * @return {@link AuthRequest}
     */
    public AuthRequest get(String source, String requestBaseUrl) {
        if (StrUtil.isBlank(source)) {
            throw new AuthException(AuthResponseStatus.NO_AUTH_SOURCE);
        }

        // 获取 JustAuth 中已存在的
        AuthRequest authRequest = getDefaultRequest(source, requestBaseUrl);

        // 如果获取不到则尝试取自定义的
        if (authRequest == null) {
            authRequest = getExtendRequest(properties.getExtend().getEnumClass(), source);
        }

        if (authRequest == null) {
            throw new AuthException(AuthResponseStatus.UNSUPPORTED);
        }

        return authRequest;
    }

    /**
     * 获取自定义的 request
     *
     * @param clazz  枚举类 {@link AuthSource}
     * @param source {@link AuthSource}
     * @return {@link AuthRequest}
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private AuthRequest getExtendRequest(Class clazz, String source) {
        String upperSource = source.toUpperCase();
        try {
            EnumUtil.fromString(clazz, upperSource);
        } catch (IllegalArgumentException e) {
            // 无自定义匹配
            return null;
        }

        Map<String, ExtendProperties.ExtendRequestConfig> extendConfig = properties.getExtend().getConfig();

        // key 转大写
        Map<String, ExtendProperties.ExtendRequestConfig> upperConfig = new HashMap<>(6);
        extendConfig.forEach((k, v) -> upperConfig.put(k.toUpperCase(), v));

        ExtendProperties.ExtendRequestConfig extendRequestConfig = upperConfig.get(upperSource);
        if (extendRequestConfig != null) {

            // 配置 http config
            configureHttpConfig(upperSource, extendRequestConfig, properties.getHttpConfig());

            Class<? extends AuthRequest> requestClass = extendRequestConfig.getRequestClass();

            if (requestClass != null) {
                // 反射获取 Request 对象,所以必须实现 2 个参数的构造方法
                return ReflectUtil.newInstance(requestClass, (AuthConfig) extendRequestConfig, authStateCache);
            }
        }

        return null;
    }


    /**
     * 获取默认的 Request
     *
     * @param source {@link AuthSource}
     * @return {@link AuthRequest}
     */
    private AuthRequest getDefaultRequest(String source, String requestBaseUrl) {
        AuthDefaultSource authDefaultSource;

        try {
            authDefaultSource = EnumUtil.fromString(AuthDefaultSource.class, source.toUpperCase());
        } catch (IllegalArgumentException e) {
            // 无自定义匹配
            return null;
        }

        AuthConfig config = properties.getType().get(authDefaultSource.name());
        // 找不到对应关系,直接返回空
        if (config == null) {
            return null;
        }
        if(!config.getRedirectUri().startsWith("http")){ // 克隆并修改回调地址
            AuthConfig newConfig = new AuthConfig();
            BeanUtils.copyProperties(config, newConfig);
            newConfig.setRedirectUri(requestBaseUrl + newConfig.getRedirectUri());
            config = newConfig;
        }

        // 配置 http config
        configureHttpConfig(authDefaultSource.name(), config, properties.getHttpConfig());

        switch (authDefaultSource) {
            case GITHUB:
                return new AuthGithubRequest(config, authStateCache);
            case WEIBO:
                return new AuthWeiboRequest(config, authStateCache);
            case GITEE:
                return new AuthGiteeRequest(config, authStateCache);
            case DINGTALK:
                return new AuthDingTalkRequest(config, authStateCache);
            case DINGTALK_ACCOUNT:
                return new AuthDingTalkAccountRequest(config, authStateCache);
            case BAIDU:
                return new AuthBaiduRequest(config, authStateCache);
            case CSDN:
                return new AuthCsdnRequest(config, authStateCache);
            case CODING:
                return new AuthCodingRequest(config, authStateCache);
            case OSCHINA:
                return new AuthOschinaRequest(config, authStateCache);
            case ALIPAY:
                return new AuthAlipayRequest(config, authStateCache);
            case QQ:
                return new AuthQqRequest(config, authStateCache);
            case WECHAT_OPEN:
                return new AuthWeChatOpenRequest(config, authStateCache);
            case WECHAT_MP:
                return new AuthWeChatMpRequest(config, authStateCache);
            case WECHAT_ENTERPRISE:
                return new AuthWeChatEnterpriseQrcodeRequest(config, authStateCache);
            case WECHAT_ENTERPRISE_WEB:
                return new AuthWeChatEnterpriseWebRequest(config, authStateCache);
            case TAOBAO:
                return new AuthTaobaoRequest(config, authStateCache);
            case GOOGLE:
                return new AuthGoogleRequest(config, authStateCache);
            case FACEBOOK:
                return new AuthFacebookRequest(config, authStateCache);
            case DOUYIN:
                return new AuthDouyinRequest(config, authStateCache);
            case LINKEDIN:
                return new AuthLinkedinRequest(config, authStateCache);
            case MICROSOFT:
                return new AuthMicrosoftRequest(config, authStateCache);
            case MI:
                return new AuthMiRequest(config, authStateCache);
            case TOUTIAO:
                return new AuthToutiaoRequest(config, authStateCache);
            case TEAMBITION:
                return new AuthTeambitionRequest(config, authStateCache);
            case RENREN:
                return new AuthRenrenRequest(config, authStateCache);
            case PINTEREST:
                return new AuthPinterestRequest(config, authStateCache);
            case STACK_OVERFLOW:
                return new AuthStackOverflowRequest(config, authStateCache);
            case HUAWEI:
                return new AuthHuaweiRequest(config, authStateCache);
            case GITLAB:
                return new AuthGitlabRequest(config, authStateCache);
            case KUJIALE:
                return new AuthKujialeRequest(config, authStateCache);
            case ELEME:
                return new AuthElemeRequest(config, authStateCache);
            case MEITUAN:
                return new AuthMeituanRequest(config, authStateCache);
            case TWITTER:
                return new AuthTwitterRequest(config, authStateCache);
            case FEISHU:
                return new AuthFeishuRequest(config, authStateCache);
            case JD:
                return new AuthJdRequest(config, authStateCache);
            case ALIYUN:
                return new AuthAliyunRequest(config, authStateCache);
            case XMLY:
                return new AuthXmlyRequest(config, authStateCache);
            case AMAZON:
                return new AuthAmazonRequest(config, authStateCache);
            case SLACK:
                return new AuthSlackRequest(config, authStateCache);
            case LINE:
                return new AuthLineRequest(config, authStateCache);
            case OKTA:
                return new AuthOktaRequest(config, authStateCache);
            default:
                return null;
        }
    }

    /**
     * 配置 http 相关的配置
     *
     * @param authSource {@link AuthSource}
     * @param authConfig {@link AuthConfig}
     */
    private void configureHttpConfig(String authSource, AuthConfig authConfig, JustAuthProperties.JustAuthHttpConfig httpConfig) {
        if (null == httpConfig) {
            return;
        }
        Map<String, JustAuthProperties.JustAuthProxyConfig> proxyConfigMap = httpConfig.getProxy();
        if (CollectionUtils.isEmpty(proxyConfigMap)) {
            return;
        }
        JustAuthProperties.JustAuthProxyConfig proxyConfig = proxyConfigMap.get(authSource);

        if (null == proxyConfig) {
            return;
        }

        authConfig.setHttpConfig(HttpConfig.builder()
                .timeout(httpConfig.getTimeout())
                .proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort())))
                .build());
    }
}

这样一来,实际调用的时候,Autowire CcAuthRequestFactory而不是AuthRequestFactory。而在get方法的时候,增加了requestBaseUrl调用用来自动添加前缀地址。requestBaseUrl则是从Controller请求自动从前端请求抓取而来:

java 复制代码
    @Autowired
    private CcAuthRequestFactory factory;


    @GetMapping(value = "thirdOauth")
    @ApiOperation(value = "三方登录并跳转")
    @SneakyThrows
    public void thirdOauth(String type,@ApiIgnore RequestSite adminSite, HttpServletResponse response){ //跳转三方登录,换取code等
        AuthRequest authRequest = factory.get(type, adminSite.getRequestBaseUrl());
        response.setStatus(302); //支持HTTP重定向到HTTPS
        response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
    }

BTW.三方登录的请求开关也被改了,使用cc-enabled而不是enabled,避免将原来的RequestFactory也自动初始化

至于RequestSite adminSite如何自动注入的请看我的另一篇文章,你注入ServletHttpRequest去实现一样。

这样便支持了前面配置文件里的直接书写URI,URL则是调用Controller的请求域名及IP:

redirect-uri: /api/common/thirdOauthCallback/qq

配置省事了,省一点算一点,优秀的系统必须是一点点简化而来的

相关推荐
考虑考虑9 小时前
Jpa使用union all
java·spring boot·后端
用户37215742613510 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊11 小时前
Java学习第22天 - 云原生与容器化
java
渣哥13 小时前
原来 Java 里线程安全集合有这么多种
java
间彧13 小时前
Spring Boot集成Spring Security完整指南
java
间彧13 小时前
Spring Secutiy基本原理及工作流程
java
Java水解14 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆16 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学17 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole17 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端