【更新】底座增加山信通登录支持

dev
xuyuxiang 2025-09-18 22:15:10 +08:00
parent 96e496009c
commit 890456efa3
13 changed files with 324 additions and 19 deletions

View File

@ -44,6 +44,7 @@
"js-pinyin": "0.2.7",
"lodash-es": "4.17.21",
"nprogress": "0.2.0",
"path-to-regexp": "8.2.0",
"pinia": "2.2.2",
"screenfull": "6.0.2",
"qs": "6.13.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 KiB

View File

@ -23,6 +23,7 @@ import { NextLoading } from '@/utils/loading'
import { useMenuStore } from '@/store/menu'
import { useUserStore } from '@/store/user'
import { useDictStore } from '@/store/dict'
import { pathToRegexp } from 'path-to-regexp'
// 进度条配置
NProgress.configure({ showSpinner: false, speed: 500 })
@ -51,7 +52,14 @@ const isGetRouter = ref(false)
// 白名单校验
const exportWhiteListFromRouter = (router) => {
const res = []
for (const item of router) res.push(item.path)
for (const item of router) {
// 生成路由的路径正则表达式(解构出正则表达式对象)
const { regexp } = pathToRegexp(item.path)
res.push({
path: item.path,
regex: regexp // 使用解构后的正则表达式
})
}
return res
}
const whiteList = exportWhiteListFromRouter(whiteListRouters)
@ -66,7 +74,7 @@ router.beforeEach(async (to, from, next) => {
: `${sysBaseConfig.SNOWY_SYS_NAME}`
// 过滤白名单
if (whiteList.includes(to.path)) {
if (whiteList.some(currentRoute => currentRoute.regex.test(to.path))) {
next()
// NProgress.done()
return false

View File

@ -146,8 +146,8 @@
argLength += 1
params[key] = value
})
//
if (argLength < 2) {
//
if (argLength === 0) {
showError(proxy.$t('login.paramError'), true)
return
}

View File

@ -2,6 +2,9 @@
<a-divider>{{ $t('login.signInOther') }}</a-divider>
<div class="login-oauth layout-center">
<a-space align="start">
<a @click="getLoginRenderUrl('iam')">
<img style="width: 32px; height: 32px;" src="/src/assets/images/snowy-iam.png" alt="" />
</a>
<a @click="getLoginRenderUrl('gitee')">
<GiteeIcon />
</a>

View File

@ -0,0 +1,120 @@
<template>
<a-spin :spinning="loadSpinning">
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
:label-col="{ ...layout.labelCol, offset: 0 }"
:wrapper-col="{ ...layout.wrapperCol, offset: 0 }"
>
<a-row :gutter="8">
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="IAM认证地址" name="SNOWY_THIRD_IAM_AUTHORIZE_URL">
<a-input v-model:value="formData.SNOWY_THIRD_IAM_AUTHORIZE_URL" placeholder="请输入IAM认证地址" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="IAM获取token地址" name="SNOWY_THIRD_IAM_ACCESS_TOKEN_URL">
<a-input v-model:value="formData.SNOWY_THIRD_IAM_ACCESS_TOKEN_URL" placeholder="请输入IAM获取token地址" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="IAM获取用户信息地址" name="SNOWY_THIRD_IAM_USER_INFO_URL">
<a-input v-model:value="formData.SNOWY_THIRD_IAM_USER_INFO_URL" placeholder="请输入IAM获取用户信息地址" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="IAM客户端ID" name="SNOWY_THIRD_IAM_CLIENT_ID">
<a-input v-model:value="formData.SNOWY_THIRD_IAM_CLIENT_ID" placeholder="请输入IAM客户端ID" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="IAM客户端SECRET" name="SNOWY_THIRD_IAM_CLIENT_SECRET">
<a-input v-model:value="formData.SNOWY_THIRD_IAM_CLIENT_SECRET" placeholder="请输入IAM客户端SECRET" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="重定向URL" name="SNOWY_THIRD_IAM_REDIRECT_URL">
<a-input v-model:value="formData.SNOWY_THIRD_IAM_REDIRECT_URL" placeholder="请输入重定向URL" />
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item>
<a-space>
<a-button type="primary" :loading="submitLoading" @click="onSubmit()"></a-button>
<a-button @click="() => formRef.resetFields()">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</template>
<script setup name="giteeThirdForm">
import { cloneDeep } from 'lodash-es'
import { required } from '@/utils/formRules'
import { message } from 'ant-design-vue'
import configApi from '@/api/dev/configApi'
const formRef = ref()
const formData = ref({})
const submitLoading = ref(false)
const loadSpinning = ref(true)
// ,
const param = {
category: 'THIRD_IAM'
}
configApi.configList(param).then((data) => {
loadSpinning.value = false
if (data) {
data.forEach((item) => {
formData.value[item.configKey] = item.configValue
})
} else {
message.warning('表单项不存在,请初始化数据库')
}
})
//
const formRules = {
SNOWY_THIRD_IAM_AUTHORIZE_URL: [required('请输入IAM认证地址')],
SNOWY_THIRD_IAM_ACCESS_TOKEN_URL: [required('请输入IAM获取token地址')],
SNOWY_THIRD_IAM_USER_INFO_URL: [required('请输入IAM获取用户信息地址')],
SNOWY_THIRD_IAM_CLIENT_ID: [required('请输入IAM客户端ID')],
SNOWY_THIRD_IAM_CLIENT_SECRET: [required('请输入IAM客户端SECRET')],
SNOWY_THIRD_IAM_REDIRECT_URL: [required('请输入重定向URL')]
}
//
const onSubmit = () => {
formRef.value
.validate()
.then(() => {
submitLoading.value = true
let submitParam = cloneDeep(formData.value)
const param = Object.entries(submitParam).map((item) => {
return {
configKey: item[0],
configValue: item[1]
}
})
configApi
.configEditForm(param)
.then(() => {})
.finally(() => {
submitLoading.value = false
})
})
.catch(() => {})
}
const layout = {
labelCol: {
span: 24
},
wrapperCol: {
span: 12
}
}
</script>

View File

@ -1,16 +1,20 @@
<template>
<a-tabs v-model:activeKey="activeKey" tab-position="left">
<a-tab-pane key="wechatThird" tab="微信">
<wechatThirdForm />
<a-tab-pane key="iamThird" tab="IAM">
<iamThirdForm />
</a-tab-pane>
<a-tab-pane key="giteeThird" tab="GITEE">
<giteeThirdForm />
</a-tab-pane>
<a-tab-pane key="wechatThird" tab="微信">
<wechatThirdForm />
</a-tab-pane>
</a-tabs>
</template>
<script setup name="thirdConfig">
import IamThirdForm from './iamThirdForm.vue'
import WechatThirdForm from './wechatThirdForm.vue'
import GiteeThirdForm from './giteeThirdForm.vue'
const activeKey = ref('wechatThird')
const activeKey = ref('iamThird')
</script>

View File

@ -24,6 +24,11 @@ import vip.xiaonuo.common.exception.CommonException;
@Getter
public enum AuthThirdPlatformEnum {
/**
* IAM
*/
IAM("IAM"),
/**
* GITEE
*/
@ -41,7 +46,7 @@ public enum AuthThirdPlatformEnum {
}
public static void validate(String value) {
boolean flag = GITEE.getValue().equals(value) || WECHAT.getValue().equals(value);
boolean flag = IAM.getValue().equals(value) || GITEE.getValue().equals(value) || WECHAT.getValue().equals(value);
if(!flag) {
throw new CommonException("不支持的第三方平台:{}", value);
}

View File

@ -38,7 +38,6 @@ public class AuthThirdCallbackParam {
private String code;
/** 第三方回调state */
@Schema(description = "第三方回调state", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "state不能为空")
@Schema(description = "第三方回调state")
private String state;
}

View File

@ -0,0 +1,45 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.auth.modular.third.request;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
*
*
* @author xuyuxiang
* @date 2025/2/6 17:07
**/
public record AuthThirdIamCommonSource(String authorizeUrl, String accessTokenUrl, String userInfoUrl) implements AuthSource {
@Override
public String authorize() {
return this.authorizeUrl;
}
@Override
public String accessToken() {
return this.accessTokenUrl;
}
@Override
public String userInfo() {
return this.userInfoUrl;
}
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthThirdIamRequest.class;
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.auth.modular.third.request;
import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.enums.AuthUserGender;
import me.zhyd.oauth.enums.scope.AuthGiteeScope;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import me.zhyd.oauth.utils.AuthScopeUtils;
import me.zhyd.oauth.utils.UrlBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
import java.util.Map;
/**
*
*
* @author xuyuxiang
* @date 2025/1/24 15:09
**/
@Getter
public class AuthThirdIamRequest extends AuthDefaultRequest {
private final Map<String, String> authSourceOidcBaseJson;
static {
Security.addProvider(new BouncyCastleProvider());
}
public AuthThirdIamRequest(AuthConfig config, Map<String, String> authSourceOidcBaseJson) {
super(config, new AuthThirdIamCommonSource(authSourceOidcBaseJson.get("authorizeUrl"),
authSourceOidcBaseJson.get("accessTokenUrl"),
authSourceOidcBaseJson.get("userInfoUrl")));
this.authSourceOidcBaseJson = authSourceOidcBaseJson;
}
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String response = this.doPostAuthorizationCode(authCallback.getCode());
com.alibaba.fastjson.JSONObject accessTokenObject = com.alibaba.fastjson.JSONObject.parseObject(response);
this.checkResponse(accessTokenObject);
return AuthToken.builder().accessToken(accessTokenObject.getString("access_token")).refreshToken(accessTokenObject.getString("refresh_token")).scope(accessTokenObject.getString("scope")).tokenType(accessTokenObject.getString("token_type")).expireIn(accessTokenObject.getIntValue("expires_in")).build();
}
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String userInfo = this.doGetUserInfo(authToken);
com.alibaba.fastjson.JSONObject userInfoObject = com.alibaba.fastjson.JSONObject.parseObject(userInfo);
this.checkResponse(userInfoObject);
return AuthUser.builder().rawUserInfo(userInfoObject).uuid(userInfoObject.getString("sub")).nickname(userInfoObject.getString("name")).username(userInfoObject.getString("account")).avatar(userInfoObject.getString("picture")).email(userInfoObject.getString("email")).gender(AuthUserGender.UNKNOWN).token(authToken).source(this.source.toString()).build();
}
private void checkResponse(JSONObject object) {
if (object.getIntValue("code") != 200) {
throw new AuthException(object.getString("msg"));
}
}
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(super.authorize(state)).queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthGiteeScope.values()))).build();
}
}

View File

@ -13,6 +13,7 @@
package vip.xiaonuo.auth.modular.third.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
@ -45,6 +46,7 @@ import vip.xiaonuo.auth.modular.third.param.AuthThirdBindAccountParam;
import vip.xiaonuo.auth.modular.third.param.AuthThirdCallbackParam;
import vip.xiaonuo.auth.modular.third.param.AuthThirdRenderParam;
import vip.xiaonuo.auth.modular.third.param.AuthThirdUserPageParam;
import vip.xiaonuo.auth.modular.third.request.AuthThirdIamRequest;
import vip.xiaonuo.auth.modular.third.result.AuthThirdRenderResult;
import vip.xiaonuo.auth.modular.third.service.AuthThirdService;
import vip.xiaonuo.common.cache.CommonCacheOperator;
@ -53,6 +55,8 @@ import vip.xiaonuo.common.exception.CommonException;
import vip.xiaonuo.common.page.CommonPageRequest;
import vip.xiaonuo.dev.api.DevConfigApi;
import java.util.Map;
/**
* Service
*
@ -65,6 +69,13 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
/** 缓存前缀 */
private static final String CONFIG_CACHE_KEY = "auth-third-state:";
private static final String SNOWY_THIRD_IAM_AUTHORIZE_URL_KEY = "SNOWY_THIRD_IAM_AUTHORIZE_URL";
private static final String SNOWY_THIRD_IAM_ACCESS_TOKEN_URL_KEY = "SNOWY_THIRD_IAM_ACCESS_TOKEN_URL";
private static final String SNOWY_THIRD_IAM_USER_INFO_URL_KEY = "SNOWY_THIRD_IAM_USER_INFO_URL";
private static final String SNOWY_THIRD_IAM_CLIENT_ID_KEY = "SNOWY_THIRD_IAM_CLIENT_ID";
private static final String SNOWY_THIRD_IAM_CLIENT_SECRET_KEY = "SNOWY_THIRD_IAM_CLIENT_SECRET";
private static final String SNOWY_THIRD_IAM_REDIRECT_URL_KEY = "SNOWY_THIRD_IAM_REDIRECT_URL";
private static final String SNOWY_THIRD_GITEE_CLIENT_ID_KEY = "SNOWY_THIRD_GITEE_CLIENT_ID";
private static final String SNOWY_THIRD_GITEE_CLIENT_SECRET_KEY = "SNOWY_THIRD_GITEE_CLIENT_SECRET";
private static final String SNOWY_THIRD_GITEE_REDIRECT_URL_KEY = "SNOWY_THIRD_GITEE_REDIRECT_URL";
@ -120,16 +131,23 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
AuthRequest authRequest = this.getAuthRequest(authThirdCallbackParam.getPlatform());
// 获取state
String state = authThirdCallbackParam.getState();
// 获取缓存值
Object stateCacheValueObj = commonCacheOperator.get(CONFIG_CACHE_KEY + state);
// 判断是否为空
if(ObjectUtil.isEmpty(stateCacheValueObj)){
throw new CommonException("state已失效");
// 定义登录端类型
String clientType;
if(ObjectUtil.isNotEmpty(state)) {
// 获取缓存值
Object stateCacheValueObj = commonCacheOperator.get(CONFIG_CACHE_KEY + state);
// 判断是否为空
if(ObjectUtil.isEmpty(stateCacheValueObj)){
throw new CommonException("state已失效");
}
// 获取登录端类型
clientType = JSONUtil.parseObj(stateCacheValueObj).getStr("clientType");
// 移除缓存
commonCacheOperator.remove(CONFIG_CACHE_KEY + state);
} else {
// 默认B端登录
clientType = SaClientTypeEnum.B.getValue();
}
// 获取登录端类型
String clientType = JSONUtil.parseObj(stateCacheValueObj).getStr("clientType");
// 移除缓存
commonCacheOperator.remove(CONFIG_CACHE_KEY + state);
// 执行请求
AuthResponse<AuthUser> authResponse = authRequest.login(authCallback);
if (authResponse.ok()) {
@ -240,12 +258,26 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
source = source.toUpperCase();
HttpUtil.setHttp(new HutoolImpl());
AuthThirdPlatformEnum.validate(source);
if (source.equals(AuthThirdPlatformEnum.IAM.getValue())) {
// 山信通登录
authRequest = new AuthThirdIamRequest(AuthConfig.builder()
.clientId(devConfigApi.getValueByKey(SNOWY_THIRD_IAM_CLIENT_ID_KEY))
.clientSecret(devConfigApi.getValueByKey(SNOWY_THIRD_IAM_CLIENT_SECRET_KEY))
.redirectUri(devConfigApi.getValueByKey(SNOWY_THIRD_IAM_REDIRECT_URL_KEY))
.ignoreCheckState(true)
.scopes(CollectionUtil.newArrayList("profile", "account", "email", "phone"))
.build(), Map.of(
"authorizeUrl", devConfigApi.getValueByKey(SNOWY_THIRD_IAM_AUTHORIZE_URL_KEY),
"accessTokenUrl", devConfigApi.getValueByKey(SNOWY_THIRD_IAM_ACCESS_TOKEN_URL_KEY),
"userInfoUrl", devConfigApi.getValueByKey(SNOWY_THIRD_IAM_USER_INFO_URL_KEY)));
}
if (source.equals(AuthThirdPlatformEnum.GITEE.getValue())) {
// GITEE登录
authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId(devConfigApi.getValueByKey(SNOWY_THIRD_GITEE_CLIENT_ID_KEY))
.clientSecret(devConfigApi.getValueByKey(SNOWY_THIRD_GITEE_CLIENT_SECRET_KEY))
.redirectUri(devConfigApi.getValueByKey(SNOWY_THIRD_GITEE_REDIRECT_URL_KEY))
.ignoreCheckState(true)
.build());
}
if(source.equals(AuthThirdPlatformEnum.WECHAT.getValue())){
@ -254,6 +286,7 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
.clientId(devConfigApi.getValueByKey(SNOWY_THIRD_WECHAT_CLIENT_ID_KEY))
.clientSecret(devConfigApi.getValueByKey(SNOWY_THIRD_WECHAT_CLIENT_SECRET_KEY))
.redirectUri(devConfigApi.getValueByKey(SNOWY_THIRD_WECHAT_REDIRECT_URL_KEY))
.ignoreCheckState(true)
.build());
}
return authRequest;

View File

@ -341,6 +341,13 @@ INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755290', 'SNOWY_SYS_DEFAULT_PASSW
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755291', 'SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_NOTICE_DAYS_FOR_C', '3', 'PASSWORD_STRATEGY_FOR_C', 'C端密码过期前提醒天数', 174, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755292', 'SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B', 'true', 'LOGIN_STRATEGY_FOR_B', 'B端是否允许动态口令登录', 175, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755293', 'SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C', 'true', 'LOGIN_STRATEGY_FOR_C', 'C端是否允许动态口令登录', 176, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755294', 'SNOWY_THIRD_IAM_AUTHORIZE_URL', 'IAM认证地址', 'THIRD_IAM', 'IAM认证地址', 177, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755295', 'SNOWY_THIRD_IAM_ACCESS_TOKEN_URL', 'IAM获取token地址', 'THIRD_IAM', 'IAM获取token地址', 178, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755296', 'SNOWY_THIRD_IAM_USER_INFO_URL', 'IAM获取用户信息地址', 'THIRD_IAM', 'IAM获取用户信息地址', 179, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755297', 'SNOWY_THIRD_IAM_CLIENT_ID', 'IAM客户端ID', 'THIRD_IAM', 'IAM客户端ID', 180, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755298', 'SNOWY_THIRD_IAM_CLIENT_SECRET', 'IAM客户端SECRET', 'THIRD_IAM', 'IAM客户端SECRET', 181, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755299', 'SNOWY_THIRD_IAM_REDIRECT_URL', '重定向URL', 'THIRD_IAM', '重定向URL', 182, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
-- ----------------------------
-- Table structure for DEV_DICT
-- ----------------------------