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

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的公共配置只需要如下配置:
 

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

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

app:
  third-oauth:
    GITEE:
      client-id: ??
      client-secret: ??

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

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

/*
 * 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请求自动从前端请求抓取而来:
 

    @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

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

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 中,也可以使用数组来实现 LRU 缓存淘汰策略。具体实现步骤如下: 1. 创建一个 Entry 类用于存储缓存中的键值对。 ```java class Entry<K, V> { K key; V value; Entry(K key, V value) { this.key = key; this.value = value; } } ``` 2. 在缓存类中创建一个数组以及一个计数器,用于存储缓存中的元素以及当前缓存中的元素个数。 ```java class LRUCache<K, V> { private final Entry<K, V>[] cache; private int count; LRUCache(int capacity) { // 创建一个长度为 capacity 的数组 cache = new Entry[capacity]; count = 0; } } ``` 3. 在 get 方法中查找缓存中的元素。如果找到了该元素,将其移到数组的最后一位,以表示最近访问过。 ```java public V get(K key) { for (int i = 0; i < count; i++) { if (cache[i].key.equals(key)) { // 找到了元素,将其移到数组的最后一位 Entry<K, V> entry = cache[i]; for (int j = i; j < count - 1; j++) { cache[j] = cache[j + 1]; } cache[count - 1] = entry; return entry.value; } } return null; } ``` 4. 在 put 方法中插入新元素,并在缓存满时删除最老的元素。如果缓存中已存在该元素,则将其移到数组的最后一位。 ```java public void put(K key, V value) { // 查找缓存中是否已存在该元素 for (int i = 0; i < count; i++) { if (cache[i].key.equals(key)) { // 找到了元素,将其移到数组的最后一位 Entry<K, V> entry = cache[i]; for (int j = i; j < count - 1; j++) { cache[j] = cache[j + 1]; } cache[count - 1] = entry; entry.value = value; return; } } // 插入新元素 if (count < cache.length) { // 缓存未满,直接插入到数组的最后一位 cache[count] = new Entry<>(key, value); count++; } else { // 缓存已满,删除最老的元素,将新元素插入到数组的最后一位 for (int i = 0; i < count - 1; i++) { cache[i] = cache[i + 1]; } cache[count - 1] = new Entry<>(key, value); } } ``` 下面是一个完整的示例代码: ```java class Entry<K, V> { K key; V value; Entry(K key, V value) { this.key = key; this.value = value; } } class LRUCache<K, V> { private final Entry<K, V>[] cache; private int count; LRUCache(int capacity) { cache = new Entry[capacity]; count = 0; } public V get(K key) { for (int i = 0; i < count; i++) { if (cache[i].key.equals(key)) { // 找到了元素,将其移到数组的最后一位 Entry<K, V> entry = cache[i]; for (int j = i; j < count - 1; j++) { cache[j] = cache[j + 1]; } cache[count - 1] = entry; return entry.value; } } return null; } public void put(K key, V value) { // 查找缓存中是否已存在该元素 for (int i = 0; i < count; i++) { if (cache[i].key.equals(key)) { // 找到了元素,将其移到数组的最后一位 Entry<K, V> entry = cache[i]; for (int j = i; j < count - 1; j++) { cache[j] = cache[j + 1]; } cache[count - 1] = entry; entry.value = value; return; } } // 插入新元素 if (count < cache.length) { // 缓存未满,直接插入到数组的最后一位 cache[count] = new Entry<>(key, value); count++; } else { // 缓存已满,删除最老的元素,将新元素插入到数组的最后一位 for (int i = 0; i < count - 1; i++) { cache[i] = cache[i + 1]; } cache[count - 1] = new Entry<>(key, value); } } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(2); cache.put(1, "one"); cache.put(2, "two"); System.out.println(Arrays.toString(cache.cache)); // 输出 [{1=one}, {2=two}] cache.get(1); cache.put(3, "three"); System.out.println(Arrays.toString(cache.cache)); // 输出 [{2=two}, {3=three}] } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值