mirror of
https://gitee.com/xiaonuobase/snowy.git
synced 2026-03-22 02:37:16 +08:00
【升级】嵌入模式完成登录交互,可以将本系统嵌入到三方系统内
This commit is contained in:
@@ -61,5 +61,9 @@ export default {
|
|||||||
// B端判断是否登录
|
// B端判断是否登录
|
||||||
isLogin(data) {
|
isLogin(data) {
|
||||||
return request('isLogin', data, 'get')
|
return request('isLogin', data, 'get')
|
||||||
|
},
|
||||||
|
// B端第三方Token交换登录(iframe嵌入免登)
|
||||||
|
doLoginByThirdToken(data) {
|
||||||
|
return request('doLoginByThirdToken', data, 'post', false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import { useMenuStore } from '@/store/menu'
|
|||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { useDictStore } from '@/store/dict'
|
import { useDictStore } from '@/store/dict'
|
||||||
import { pathToRegexp } from 'path-to-regexp'
|
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 })
|
NProgress.configure({ showSpinner: false, speed: 500 })
|
||||||
@@ -57,7 +59,7 @@ const exportWhiteListFromRouter = (router) => {
|
|||||||
const { regexp } = pathToRegexp(item.path)
|
const { regexp } = pathToRegexp(item.path)
|
||||||
res.push({
|
res.push({
|
||||||
path: item.path,
|
path: item.path,
|
||||||
regex: regexp // 使用解构后的正则表达式
|
regex: regexp // 使用解构后的正则表达式
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
@@ -74,11 +76,35 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
: `${sysBaseConfig.SNOWY_SYS_NAME}`
|
: `${sysBaseConfig.SNOWY_SYS_NAME}`
|
||||||
|
|
||||||
// 过滤白名单
|
// 过滤白名单
|
||||||
if (whiteList.some(currentRoute => currentRoute.regex.test(to.path))) {
|
if (whiteList.some((currentRoute) => currentRoute.regex.test(to.path))) {
|
||||||
next()
|
next()
|
||||||
// NProgress.done()
|
// NProgress.done()
|
||||||
return false
|
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端检验逻辑
|
// C端检验逻辑
|
||||||
if (to.path.includes('/front/client/')) {
|
if (to.path.includes('/front/client/')) {
|
||||||
return validateClientAccess(to.path).valid ? next() : next({ path: validateClientAccess(to.path).redirectPath })
|
return validateClientAccess(to.path).valid ? next() : next({ path: validateClientAccess(to.path).redirectPath })
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useMenuStore } from '@/store/menu'
|
|||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { globalStore } from '@/store'
|
import { globalStore } from '@/store'
|
||||||
|
|
||||||
export const afterLogin = async (loginToken) => {
|
export const afterLogin = async (loginToken, targetPath) => {
|
||||||
const route = router.currentRoute.value
|
const route = router.currentRoute.value
|
||||||
const menuStore = useMenuStore()
|
const menuStore = useMenuStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -51,6 +51,12 @@ export const afterLogin = async (loginToken) => {
|
|||||||
tool.data.set('DICT_TYPE_TREE_DATA', data)
|
tool.data.set('DICT_TYPE_TREE_DATA', data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 第三方Token登录:直接跳转到指定目标路径
|
||||||
|
if (targetPath) {
|
||||||
|
router.replace({ path: targetPath }).then(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 此处判断是否存在跳转页面,如存在则跳转,否则走原来逻辑
|
// 此处判断是否存在跳转页面,如存在则跳转,否则走原来逻辑
|
||||||
if (route.query.redirect_uri) {
|
if (route.query.redirect_uri) {
|
||||||
// 跳转到回调页
|
// 跳转到回调页
|
||||||
|
|||||||
@@ -182,13 +182,26 @@ public class AuthController {
|
|||||||
return CommonResult.data(authService.doLoginByOtp(authOtpLoginParam, SaClientTypeEnum.B.getValue()));
|
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<String> doLoginByThirdToken(@RequestBody @Valid AuthThirdTokenLoginParam authThirdTokenLoginParam) {
|
||||||
|
return CommonResult.data(authService.doLoginByThirdToken(authThirdTokenLoginParam));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* B端判断是否登录
|
* B端判断是否登录
|
||||||
*
|
*
|
||||||
* @author xuyuxiang
|
* @author xuyuxiang
|
||||||
* @date 2021/10/15 13:12
|
* @date 2021/10/15 13:12
|
||||||
**/
|
**/
|
||||||
@ApiOperationSupport(order = 11)
|
@ApiOperationSupport(order = 12)
|
||||||
@Operation(summary = "B端判断是否登录")
|
@Operation(summary = "B端判断是否登录")
|
||||||
@GetMapping("/auth/b/isLogin")
|
@GetMapping("/auth/b/isLogin")
|
||||||
public CommonResult<Boolean> isLogin() {
|
public CommonResult<Boolean> isLogin() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -145,6 +145,16 @@ public interface AuthService {
|
|||||||
**/
|
**/
|
||||||
void validValidCode(String phoneOrEmail, String validCode, String validCodeReqNo);
|
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端验证码是否开启
|
* 获取B端验证码是否开启
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import cn.hutool.core.util.ObjectUtil;
|
|||||||
import cn.hutool.core.util.PhoneUtil;
|
import cn.hutool.core.util.PhoneUtil;
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
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.JSONObject;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
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.DevEmailApi;
|
||||||
import vip.xiaonuo.dev.api.DevSmsApi;
|
import vip.xiaonuo.dev.api.DevSmsApi;
|
||||||
import vip.xiaonuo.sys.api.SysUserApi;
|
import vip.xiaonuo.sys.api.SysUserApi;
|
||||||
|
import vip.xiaonuo.auth.modular.login.prop.AuthThirdClientProperties;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -175,6 +178,9 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
@Resource
|
@Resource
|
||||||
private ClientUserApi clientUserApi;
|
private ClientUserApi clientUserApi;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AuthThirdClientProperties authThirdClientProperties;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthPicValidCodeResult getPicCaptcha(String type) {
|
public AuthPicValidCodeResult getPicCaptcha(String type) {
|
||||||
// 生成验证码,随机4位字符
|
// 生成验证码,随机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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验是否开启注册
|
* 校验是否开启注册
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -192,3 +192,7 @@ sa-token.sso-client.auth-url=
|
|||||||
sa-token.sso-client.signout-url=
|
sa-token.sso-client.signout-url=
|
||||||
sa-token.sso-client.push-url=
|
sa-token.sso-client.push-url=
|
||||||
sa-token.sso-client.secret-key=
|
sa-token.sso-client.secret-key=
|
||||||
|
#########################################
|
||||||
|
# third-auth configuration iframe login
|
||||||
|
#########################################
|
||||||
|
snowy.third-auth.user-info-url=
|
||||||
|
|||||||
Reference in New Issue
Block a user