From 7ccbeb8cf6a629b3aaf70b61a16f927a873fc0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=9E=E5=AE=9D=E5=B1=B1?= <1253070437@qq.com> Date: Wed, 11 Feb 2026 18:18:01 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=8D=87=E7=BA=A7=E3=80=91=E5=B5=8C?= =?UTF-8?q?=E5=85=A5=E6=A8=A1=E5=BC=8F=E5=AE=8C=E6=88=90=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=EF=BC=8C=E5=8F=AF=E4=BB=A5=E5=B0=86=E6=9C=AC?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=B5=8C=E5=85=A5=E5=88=B0=E4=B8=89=E6=96=B9?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=86=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- snowy-admin-web/src/api/auth/loginApi.js | 4 + snowy-admin-web/src/router/index.js | 30 +++++- snowy-admin-web/src/views/auth/login/util.js | 8 +- .../login/controller/AuthController.java | 15 ++- .../login/param/AuthThirdTokenLoginParam.java | 34 +++++++ .../login/prop/AuthThirdClientProperties.java | 45 +++++++++ .../modular/login/service/AuthService.java | 10 ++ .../login/service/impl/AuthServiceImpl.java | 92 +++++++++++++++++++ .../src/main/resources/application.properties | 4 + 9 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthThirdTokenLoginParam.java create mode 100644 snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/prop/AuthThirdClientProperties.java diff --git a/snowy-admin-web/src/api/auth/loginApi.js b/snowy-admin-web/src/api/auth/loginApi.js index 24bcdbea..a1222807 100644 --- a/snowy-admin-web/src/api/auth/loginApi.js +++ b/snowy-admin-web/src/api/auth/loginApi.js @@ -61,5 +61,9 @@ export default { // B端判断是否登录 isLogin(data) { return request('isLogin', data, 'get') + }, + // B端第三方Token交换登录(iframe嵌入免登) + doLoginByThirdToken(data) { + return request('doLoginByThirdToken', data, 'post', false) } } diff --git a/snowy-admin-web/src/router/index.js b/snowy-admin-web/src/router/index.js index 2aac8595..6133e334 100644 --- a/snowy-admin-web/src/router/index.js +++ b/snowy-admin-web/src/router/index.js @@ -24,6 +24,8 @@ import { useMenuStore } from '@/store/menu' import { useUserStore } from '@/store/user' import { useDictStore } from '@/store/dict' import { pathToRegexp } from 'path-to-regexp' +import loginApi from '@/api/auth/loginApi' +import { afterLogin } from '@/views/auth/login/util' // 进度条配置 NProgress.configure({ showSpinner: false, speed: 500 }) @@ -57,7 +59,7 @@ const exportWhiteListFromRouter = (router) => { const { regexp } = pathToRegexp(item.path) res.push({ path: item.path, - regex: regexp // 使用解构后的正则表达式 + regex: regexp // 使用解构后的正则表达式 }) } return res @@ -74,11 +76,35 @@ router.beforeEach(async (to, from, next) => { : `${sysBaseConfig.SNOWY_SYS_NAME}` // 过滤白名单 - if (whiteList.some(currentRoute => currentRoute.regex.test(to.path))) { + if (whiteList.some((currentRoute) => currentRoute.regex.test(to.path))) { next() // NProgress.done() return false } + + // ========== iframe嵌入免登:检测URL中的第三方accessToken ========== + const thirdAccessToken = to.query.accessToken + if (thirdAccessToken && !tool.data.get('TOKEN')) { + // 调用第三方Token交换登录接口 + loginApi + .doLoginByThirdToken({ + accessToken: thirdAccessToken + }) + .then(async (loginToken) => { + // 复用登录后流程,传入目标路径 + await afterLogin(loginToken, to.path) + }) + return false + } + // 如果已有TOKEN且URL带accessToken,直接放行(避免重复登录) + if (thirdAccessToken && tool.data.get('TOKEN')) { + // 去掉URL中的accessToken参数,保持路由干净 + const query = { ...to.query } + delete query.accessToken + next({ path: to.path, query, replace: true }) + return false + } + // C端检验逻辑 if (to.path.includes('/front/client/')) { return validateClientAccess(to.path).valid ? next() : next({ path: validateClientAccess(to.path).redirectPath }) diff --git a/snowy-admin-web/src/views/auth/login/util.js b/snowy-admin-web/src/views/auth/login/util.js index ac0d83e6..412bf431 100644 --- a/snowy-admin-web/src/views/auth/login/util.js +++ b/snowy-admin-web/src/views/auth/login/util.js @@ -8,7 +8,7 @@ import { useMenuStore } from '@/store/menu' import { useUserStore } from '@/store/user' import { globalStore } from '@/store' -export const afterLogin = async (loginToken) => { +export const afterLogin = async (loginToken, targetPath) => { const route = router.currentRoute.value const menuStore = useMenuStore() const userStore = useUserStore() @@ -51,6 +51,12 @@ export const afterLogin = async (loginToken) => { tool.data.set('DICT_TYPE_TREE_DATA', data) }) + // 第三方Token登录:直接跳转到指定目标路径 + if (targetPath) { + router.replace({ path: targetPath }).then(() => {}) + return + } + // 此处判断是否存在跳转页面,如存在则跳转,否则走原来逻辑 if (route.query.redirect_uri) { // 跳转到回调页 diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthController.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthController.java index 8a48a4ec..3a13388e 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthController.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthController.java @@ -182,13 +182,26 @@ public class AuthController { return CommonResult.data(authService.doLoginByOtp(authOtpLoginParam, SaClientTypeEnum.B.getValue())); } + /** + * B端第三方Token交换登录(用于iframe嵌入免登) + * + * @author yubaoshan + * @date 2026/2/11 + **/ + @ApiOperationSupport(order = 11) + @Operation(summary = "B端第三方Token交换登录(iframe嵌入免登)") + @PostMapping("/auth/b/doLoginByThirdToken") + public CommonResult doLoginByThirdToken(@RequestBody @Valid AuthThirdTokenLoginParam authThirdTokenLoginParam) { + return CommonResult.data(authService.doLoginByThirdToken(authThirdTokenLoginParam)); + } + /** * B端判断是否登录 * * @author xuyuxiang * @date 2021/10/15 13:12 **/ - @ApiOperationSupport(order = 11) + @ApiOperationSupport(order = 12) @Operation(summary = "B端判断是否登录") @GetMapping("/auth/b/isLogin") public CommonResult isLogin() { diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthThirdTokenLoginParam.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthThirdTokenLoginParam.java new file mode 100644 index 00000000..636d867c --- /dev/null +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthThirdTokenLoginParam.java @@ -0,0 +1,34 @@ +/* + * Copyright [2022] [https://www.xiaonuo.vip] + * + * Snowy采用APACHE 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.login.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * 第三方Token交换登录参数(用于iframe嵌入免登) + * + * @author yubaoshan + * @date 2026/2/11 + **/ +@Getter +@Setter +public class AuthThirdTokenLoginParam { + + /** 第三方系统的accessToken */ + @Schema(description = "第三方系统的accessToken", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "accessToken不能为空") + private String accessToken; +} diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/prop/AuthThirdClientProperties.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/prop/AuthThirdClientProperties.java new file mode 100644 index 00000000..e6242c38 --- /dev/null +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/prop/AuthThirdClientProperties.java @@ -0,0 +1,45 @@ +/* + * Copyright [2022] [https://www.xiaonuo.vip] + * + * Snowy采用APACHE 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.login.prop; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 第三方系统Token交换配置属性(用于iframe嵌入免登) + * + * 对接规范:甲方提供一个用户信息接口,GET请求,Header传 Authorization: Bearer {accessToken} + * 必须返回如下JSON格式: + * { + * "code": 200, + * "data": { + * "account": "zhangsan", + * "email": "zhangsan@xxx.com", + * "phone": "13800138000" + * } + * } + * + * @author yubaoshan + * @date 2026/2/11 + **/ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "snowy.third-auth") +public class AuthThirdClientProperties { + + /** 第三方用户信息接口地址(甲方提供),配置了即代表开启,不配置即关闭 */ + private String userInfoUrl; +} diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/AuthService.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/AuthService.java index f71f6b04..c79471c1 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/AuthService.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/AuthService.java @@ -145,6 +145,16 @@ public interface AuthService { **/ void validValidCode(String phoneOrEmail, String validCode, String validCodeReqNo); + /** + * 通过第三方Token交换登录(用于iframe嵌入免登) + * + * @param authThirdTokenLoginParam 第三方Token登录参数 + * @return 本系统Token + * @author yubaoshan + * @date 2026/2/11 + */ + String doLoginByThirdToken(AuthThirdTokenLoginParam authThirdTokenLoginParam); + /** * 获取B端验证码是否开启 * diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java index bc76d77b..9b6b3f90 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/service/impl/AuthServiceImpl.java @@ -22,6 +22,8 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.PhoneUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.toolkit.IdWorker; @@ -54,6 +56,7 @@ import vip.xiaonuo.dev.api.DevConfigApi; import vip.xiaonuo.dev.api.DevEmailApi; import vip.xiaonuo.dev.api.DevSmsApi; import vip.xiaonuo.sys.api.SysUserApi; +import vip.xiaonuo.auth.modular.login.prop.AuthThirdClientProperties; import java.util.List; import java.util.stream.Collectors; @@ -175,6 +178,9 @@ public class AuthServiceImpl implements AuthService { @Resource private ClientUserApi clientUserApi; + @Resource + private AuthThirdClientProperties authThirdClientProperties; + @Override public AuthPicValidCodeResult getPicCaptcha(String type) { // 生成验证码,随机4位字符 @@ -1178,6 +1184,92 @@ public class AuthServiceImpl implements AuthService { } } + @Override + public String doLoginByThirdToken(AuthThirdTokenLoginParam authThirdTokenLoginParam) { + // 校验是否配置了第三方用户信息接口地址(配置了即开启) + String userInfoUrl = authThirdClientProperties.getUserInfoUrl(); + if(ObjectUtil.isEmpty(userInfoUrl)) { + throw new CommonException("未配置第三方用户信息接口地址,无法使用Token交换登录"); + } + // 用accessToken调用第三方用户信息接口 + JSONObject thirdUserInfo = requestThirdUserInfo(userInfoUrl, authThirdTokenLoginParam.getAccessToken()); + // 从第三方返回中解析用户信息(固定字段:account、email、phone) + String thirdAccount = thirdUserInfo.getStr("account"); + String thirdEmail = thirdUserInfo.getStr("email"); + String thirdPhone = thirdUserInfo.getStr("phone"); + // 三级降级匹配本地用户:account → email → phone + SaBaseLoginUser saBaseLoginUser = matchLocalUser(thirdAccount, thirdEmail, thirdPhone); + // 执行B端登录 + return execLoginB(saBaseLoginUser, AuthDeviceTypeEnum.PC.getValue()); + } + + /** + * 调用第三方用户信息接口 + * 规范:GET请求,Header传Authorization: Bearer xxx + * 返回格式:{code:200, data:{account,email,phone}} + */ + private JSONObject requestThirdUserInfo(String userInfoUrl, String accessToken) { + try { + HttpResponse response = HttpRequest.get(userInfoUrl) + .header("Authorization", "Bearer " + accessToken) + .timeout(10000) + .execute(); + if(!response.isOk()) { + throw new CommonException("调用第三方用户信息接口失败,HTTP状态码:{}", response.getStatus()); + } + String body = response.body(); + if(ObjectUtil.isEmpty(body)) { + throw new CommonException("第三方用户信息接口返回为空"); + } + JSONObject resultJson = JSONUtil.parseObj(body); + // 校验code + Integer code = resultJson.getInt("code"); + if(code == null || code != 200) { + throw new CommonException("第三方用户信息接口返回业务异常,code={}", code); + } + // 提取data + JSONObject dataJson = resultJson.getJSONObject("data"); + if(ObjectUtil.isEmpty(dataJson)) { + throw new CommonException("第三方用户信息接口返回数据中未找到data字段"); + } + return dataJson; + } catch (CommonException e) { + throw e; + } catch (Exception e) { + throw new CommonException("调用第三方用户信息接口异常:{}", e.getMessage()); + } + } + + /** + * 三级降级匹配本地用户:account → email → phone + */ + private SaBaseLoginUser matchLocalUser(String thirdAccount, String thirdEmail, String thirdPhone) { + // 优先通过account查找 + if(StrUtil.isNotBlank(thirdAccount)) { + SaBaseLoginUser user = loginUserApi.getUserByAccount(thirdAccount); + if(ObjectUtil.isNotEmpty(user)) { + return user; + } + } + // 其次通过email查找 + if(StrUtil.isNotBlank(thirdEmail)) { + SaBaseLoginUser user = loginUserApi.getUserByEmail(thirdEmail); + if(ObjectUtil.isNotEmpty(user)) { + return user; + } + } + // 最后通过phone查找 + if(StrUtil.isNotBlank(thirdPhone)) { + SaBaseLoginUser user = loginUserApi.getUserByPhone(thirdPhone); + if(ObjectUtil.isNotEmpty(user)) { + return user; + } + } + // 三种方式都找不到 + throw new CommonException("第三方用户(account={}, email={}, phone={})在本系统中未找到对应用户,请联系管理员同步用户", + thirdAccount, thirdEmail, thirdPhone); + } + /** * 校验是否开启注册 * diff --git a/snowy-web-app/src/main/resources/application.properties b/snowy-web-app/src/main/resources/application.properties index 28790a8d..932af559 100644 --- a/snowy-web-app/src/main/resources/application.properties +++ b/snowy-web-app/src/main/resources/application.properties @@ -192,3 +192,7 @@ sa-token.sso-client.auth-url= sa-token.sso-client.signout-url= sa-token.sso-client.push-url= sa-token.sso-client.secret-key= +######################################### +# third-auth configuration iframe login +######################################### +snowy.third-auth.user-info-url=