当前使用的版本:
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
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
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
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();
- }
- }
先看看实现后的效果,整合后,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).
- *
- * 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
- *
- * http://www.gnu.org/licenses/lgpl.html
- *
- * 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引入太多,直接复制了改了.
- *
- *
- * AuthRequest工厂类
- *
- *
- * @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
oauthList() { - // 默认列表
- List
defaultList = new ArrayList<>(properties.getType().keySet()); - // 扩展列表
- List
extendList = new ArrayList<>(); - ExtendProperties extend = properties.getExtend();
- if (null != extend) {
- Class enumClass = extend.getEnumClass();
- List
names = EnumUtil.getNames(enumClass); - // 扩展列表
- extendList = extend.getConfig()
- .keySet()
- .stream()
- .filter(x -> names.contains(x.toUpperCase()))
- .map(String::toUpperCase)
- .collect(Collectors.toList());
- }
-
- // 合并
- return (List
) 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
extendConfig = properties.getExtend().getConfig(); -
- // key 转大写
- Map
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
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
配置省事了,省一点算一点,优秀的系统必须是一点点简化而来的