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

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

相关推荐
阿伟*rui10 分钟前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7895 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~5 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust