diff --git a/pom.xml b/pom.xml index d1d2fe36..6ff7de3d 100644 --- a/pom.xml +++ b/pom.xml @@ -252,6 +252,27 @@ 0.16.4 + + + com.google.zxing + core + 3.5.3 + + + + + org.bouncycastle + bcprov-jdk15on + 1.70 + + + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + + cn.dev33 @@ -287,6 +308,13 @@ 1.44.0 + + + cn.dev33 + sa-token-forest + 1.44.0 + + me.zhyd.oauth diff --git a/snowy-admin-web/src/api/auth/loginApi.js b/snowy-admin-web/src/api/auth/loginApi.js index 850f9ce0..24bcdbea 100644 --- a/snowy-admin-web/src/api/auth/loginApi.js +++ b/snowy-admin-web/src/api/auth/loginApi.js @@ -53,5 +53,13 @@ export default { // 注册用户 register(data) { return request('register', data) + }, + // B端动态口令登录 + loginByOtp(data) { + return request('doLoginByOtp', data, 'post', false) + }, + // B端判断是否登录 + isLogin(data) { + return request('isLogin', data, 'get') } } diff --git a/snowy-admin-web/src/api/auth/oauthApi.js b/snowy-admin-web/src/api/auth/ssoApi.js similarity index 77% rename from snowy-admin-web/src/api/auth/oauthApi.js rename to snowy-admin-web/src/api/auth/ssoApi.js index bb0c2198..cd32c2c2 100644 --- a/snowy-admin-web/src/api/auth/oauthApi.js +++ b/snowy-admin-web/src/api/auth/ssoApi.js @@ -10,7 +10,7 @@ */ import { baseRequest } from '@/utils/request' -const request = (url, ...arg) => baseRequest(`/auth/third/` + url, ...arg) +const request = (url, ...arg) => baseRequest(`/auth/sso/b/` + url, ...arg) /** * 三方登录 * @@ -18,12 +18,13 @@ const request = (url, ...arg) => baseRequest(`/auth/third/` + url, ...arg) * @date 2022-09-22 22:33:20 */ export default { - // 第三方登录页面渲染 - thirdRender(data) { - return request('render', data, 'get') + + // B端获取认证中心地址 + getSsoAuthUrl(data) { + return request('getSsoAuthUrl', data, 'get') }, - // 第三方登录授权回调 - thirdCallback(data) { - return request('callback', data, 'get') + // B端根据ticket执行单点登录 + doLoginByTicket(data) { + return request('doLoginByTicket', data) } } diff --git a/snowy-admin-web/src/api/auth/thirdApi.js b/snowy-admin-web/src/api/auth/thirdApi.js index 1a01c440..fa69de6f 100644 --- a/snowy-admin-web/src/api/auth/thirdApi.js +++ b/snowy-admin-web/src/api/auth/thirdApi.js @@ -29,5 +29,9 @@ export default { // 第三方登录授权回调 thirdCallback(data) { return request('callback', data, 'get') + }, + // 第三方登录绑定账号 + thirdBindAccount(data) { + return request('bindAccount', data) } } diff --git a/snowy-admin-web/src/api/sys/userCenterApi.js b/snowy-admin-web/src/api/sys/userCenterApi.js index 1dcd42a4..b763cac3 100644 --- a/snowy-admin-web/src/api/sys/userCenterApi.js +++ b/snowy-admin-web/src/api/sys/userCenterApi.js @@ -155,5 +155,21 @@ export default { // 获取修改密码验证方式及配置 userGetUpdatePasswordValidConfig(data) { return request('getUpdatePasswordValidConfig', data, 'get') - } + }, + // 获取动态口令绑定状态 + userCenterGetOtpInfoBindStatus(data) { + return request('getOtpInfoBindStatus', data, 'get') + }, + // 获取动态口令信息 + userCenterGetOtpInfo(data) { + return request('getOtpInfo', data, 'get') + }, + // 绑定动态口令 + userCenterBindOtp(data) { + return request('bindOtp', data) + }, + // 解绑动态口令 + userCenterUnBindOtp(data) { + return request('unBindOtp', data) + }, } diff --git a/snowy-admin-web/src/locales/lang/en.js b/snowy-admin-web/src/locales/lang/en.js index ec2e7b45..05623d83 100644 --- a/snowy-admin-web/src/locales/lang/en.js +++ b/snowy-admin-web/src/locales/lang/en.js @@ -38,11 +38,14 @@ export default { accountError: 'Please input a user account', PWPlaceholder: 'Please input a password', PWError: 'Please input a password', - validLaceholder: 'Please input a valid', + validPlaceholder: 'Please input a valid', validError: 'Please input a valid', accountPassword: 'Account Password', - phoneSms: 'Phone SMS', + phoneLogin: 'Phone Login', emailLogin: 'Email Login', + otpLogin: 'OTP Login', + thirdLogin: 'Third Login', + bindAccount: 'Bind Account', phonePlaceholder: 'Please input a phone', phoneInputNumberPlaceholder: 'Please input a phone 11-digit', smsCodePlaceholder: 'Please input a SMS code', @@ -56,6 +59,7 @@ export default { emailPlaceholder: 'Please input a correct email', emailCodePlaceholder: 'Please input a Email code', emailValidPlaceholder: 'Please input a email', + otpCodePlaceholder: 'Please input a OTP code', restPhoneType: 'For phone rest', restEmailType: 'For email rest', register: 'Register', @@ -63,7 +67,10 @@ export default { notAccountPleaseRegister: 'Not Account? Register!', haveAccountPleaseLogin: 'Have Account? Go Login!', enterAgainPassword: 'Please re-enter your password', - enteredPasswordsDiffer: 'Entered passwords differ' + enteredPasswordsDiffer: 'Entered passwords differ', + paramError: 'Param Error', + thirdLoginError: 'Third Login Error', + frontLogin: 'Front Login', }, user: { userStatus: 'User Status', @@ -76,7 +83,7 @@ export default { exportUserInfo: 'Export UserInfo', placeholderNameAndSearchKey: 'Please enter your name or keyword', placeholderUserStatus: 'Please select status', - popconfirmDeleteUser: 'Are you sure you want to delete it?', - popconfirmResatUserPwd: 'Are you sure you want to reset?' + popConfirmDeleteUser: 'Are you sure you want to delete it?', + popConfirmResatUserPwd: 'Are you sure you want to reset?' } } diff --git a/snowy-admin-web/src/locales/lang/zh-cn.js b/snowy-admin-web/src/locales/lang/zh-cn.js index ae67b01a..a1fe7e1f 100644 --- a/snowy-admin-web/src/locales/lang/zh-cn.js +++ b/snowy-admin-web/src/locales/lang/zh-cn.js @@ -40,11 +40,14 @@ export default { accountError: '请输入账号', PWPlaceholder: '请输入密码', PWError: '请输入密码', - validLaceholder: '请输入验证码', + validPlaceholder: '请输入验证码', validError: '请输入验证码', accountPassword: '账号密码', - phoneSms: '手机号登录', - emailLogin: '邮箱号登录', + phoneLogin: '手机号登录', + emailLogin: '邮箱登录', + otpLogin: '动态口令登录', + thirdLogin: '三方登录', + bindAccount: '绑定账号', phonePlaceholder: '请输入手机号', phoneInputNumberPlaceholder: '请输入11位手机号', smsCodePlaceholder: '请输入短信验证码', @@ -55,9 +58,10 @@ export default { newPwdPlaceholder: '请输入新密码', backLogin: '返回登录', restPassword: '重置密码', - emailPlaceholder: '请输入邮箱号', + emailPlaceholder: '请输入邮箱', emailCodePlaceholder: '请输入邮件验证码', emailValidPlaceholder: '请输入正确的邮箱号', + otpCodePlaceholder: '请输入动态口令', restPhoneType: '手机号找回', restEmailType: '邮箱找回', register: '注册', @@ -65,7 +69,10 @@ export default { notAccountPleaseRegister: '没有账号?前往注册!', haveAccountPleaseLogin: '已有账号?去登录!', enterAgainPassword: '请再次输入密码', - enteredPasswordsDiffer: '两次输入密码不一致' + enteredPasswordsDiffer: '两次输入密码不一致', + paramError: '参数错误', + thirdLoginError: '登录失败', + frontLogin: '前台登录', }, user: { userStatus: '用户状态', @@ -78,7 +85,7 @@ export default { exportUserInfo: '导出信息', placeholderNameAndSearchKey: '请输入姓名或关键词', placeholderUserStatus: '请选择状态', - popconfirmDeleteUser: '确定要删除吗?', - popconfirmResatUserPwd: '确定要重置吗?' + popConfirmDeleteUser: '确定要删除吗?', + popConfirmResatUserPwd: '确定要重置吗?' } } diff --git a/snowy-admin-web/src/router/systemRouter.js b/snowy-admin-web/src/router/systemRouter.js index 86f8620d..19072a62 100644 --- a/snowy-admin-web/src/router/systemRouter.js +++ b/snowy-admin-web/src/router/systemRouter.js @@ -13,11 +13,11 @@ import tool from '@/utils/tool' import routerUtil from '@/utils/routerUtil' const Layout = () => import('@/layout/index.vue') +const Sso = () => import('@/views/auth/sso/index.vue') const Login = () => import('@/views/auth/login/login.vue') const FindPwd = () => import('@/views/auth/findPwd/index.vue') const Callback = () => import('@/views/auth/login/callback.vue') const Register = () => import('@/views/auth/login/register.vue') - // 系统路由 const routes = [ { @@ -27,6 +27,13 @@ const routes = [ redirect: tool.data.get('MENU') ? routerUtil.getIndexMenu(tool.data.get('MENU')).path : config.DASHBOARD_URL, children: [] }, + { + path: '/sso', + component: Sso, + meta: { + title: '单点登录' + } + }, { path: '/login', component: Login, @@ -49,12 +56,12 @@ const routes = [ } }, { - path: '/callback', + path: '/callback/:platform', component: Callback, meta: { - title: '三方登录' + title: '三方登录回调' } - } + }, ] export default routes diff --git a/snowy-admin-web/src/router/whiteList.js b/snowy-admin-web/src/router/whiteList.js index f324358e..803ec426 100644 --- a/snowy-admin-web/src/router/whiteList.js +++ b/snowy-admin-web/src/router/whiteList.js @@ -9,6 +9,9 @@ * 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip */ const constRouters = [ + { + path: '/sso' + }, { path: '/findpwd' }, @@ -16,7 +19,7 @@ const constRouters = [ path: '/register' }, { - path: '/callback' + path: '/callback/:platform' }, { path: '/other', diff --git a/snowy-admin-web/src/views/auth/login/callback.vue b/snowy-admin-web/src/views/auth/login/callback.vue index fe274855..aa025aca 100644 --- a/snowy-admin-web/src/views/auth/login/callback.vue +++ b/snowy-admin-web/src/views/auth/login/callback.vue @@ -21,13 +21,68 @@ - 三方登录 + {{tipText}} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('login.signIn') }} + + + {{ $t('login.bindAccount') }} + + + @@ -36,20 +91,53 @@ diff --git a/snowy-admin-web/src/views/auth/login/phoneLoginForm.vue b/snowy-admin-web/src/views/auth/login/phoneLoginForm.vue index 54bd4e62..c8a73822 100644 --- a/snowy-admin-web/src/views/auth/login/phoneLoginForm.vue +++ b/snowy-admin-web/src/views/auth/login/phoneLoginForm.vue @@ -51,7 +51,7 @@ diff --git a/snowy-admin-web/src/views/auth/login/threeLogin.vue b/snowy-admin-web/src/views/auth/login/threeLogin.vue index c3ab83a8..c0bfd9db 100644 --- a/snowy-admin-web/src/views/auth/login/threeLogin.vue +++ b/snowy-admin-web/src/views/auth/login/threeLogin.vue @@ -2,23 +2,32 @@ {{ $t('login.signInOther') }} - - - - + + + + + + + diff --git a/snowy-admin-web/src/views/auth/login/threeLoginForApp.vue b/snowy-admin-web/src/views/auth/login/threeLoginForApp.vue new file mode 100644 index 00000000..19e9eb30 --- /dev/null +++ b/snowy-admin-web/src/views/auth/login/threeLoginForApp.vue @@ -0,0 +1,66 @@ + + {{ $t('login.signInOther') }} + + + + + + + + + + + diff --git a/snowy-admin-web/src/views/auth/login/util.js b/snowy-admin-web/src/views/auth/login/util.js index 28b3b30c..937d4f1a 100644 --- a/snowy-admin-web/src/views/auth/login/util.js +++ b/snowy-admin-web/src/views/auth/login/util.js @@ -8,6 +8,7 @@ import { useMenuStore } from '@/store/menu' import { useUserStore } from '@/store/user' export const afterLogin = async (loginToken) => { + const route = router.currentRoute.value const menuStore = useMenuStore() tool.data.set('TOKEN', loginToken) // 初始化用户信息 @@ -20,7 +21,7 @@ export const afterLogin = async (loginToken) => { // 重置系统默认应用 tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id) - message.success('登录成功') + if (tool.data.get('LAST_VIEWS_PATH')) { // 如果有缓存,将其登录跳转到最后访问的路由 indexMenu = tool.data.get('LAST_VIEWS_PATH') @@ -44,13 +45,40 @@ export const afterLogin = async (loginToken) => { // 设置字典到store中 tool.data.set('DICT_TYPE_TREE_DATA', data) }) - await router.replace({ - path: indexMenu - }) - // 判断用户密码是否过期 - userCenterApi.userCenterIsUserPasswordExpired().then((expired) => { - if (expired) { - message.warning('当前登录密码已过期,请及时更改!') - } - }) + + // 此处判断是否存在跳转页面,如存在则跳转,否则走原来逻辑 + if(route.query.redirect_uri) { + // 跳转到回调页 + message.success('登录成功,即将跳转...') + setTimeout(function () { + window.location.href = route.query.redirect_uri; + }, 500); + } else if(route.query.redirect) { + // 跳转到回调页 + message.success('登录成功,即将跳转...') + setTimeout(function () { + window.location.href = route.query.redirect; + }, 500); + } else if(route.query.back) { + // 跳转到回调页 + message.success('登录成功,即将跳转...') + setTimeout(function () { + window.location.href = route.query.back; + }, 500); + } else { + message.success('登录成功,即将跳转...') + setTimeout(function () { + // 跳转到首页 + router.replace({ + path: indexMenu + }).then(() => { + // 判断用户密码是否过期 + userCenterApi.userCenterIsUserPasswordExpired().then((expired) => { + if (expired) { + message.warning('当前登录密码已过期,请及时更改!') + } + }) + }) + }, 500); + } } diff --git a/snowy-admin-web/src/views/auth/sso/index.vue b/snowy-admin-web/src/views/auth/sso/index.vue new file mode 100644 index 00000000..6d8c2d75 --- /dev/null +++ b/snowy-admin-web/src/views/auth/sso/index.vue @@ -0,0 +1,204 @@ + + + + + + + + + {{ tipText }} + + + + {{ tipText }} + 重试 + + + + + + + + + + diff --git a/snowy-admin-web/src/views/biz/user/index.vue b/snowy-admin-web/src/views/biz/user/index.vue index 43106869..a28037a6 100644 --- a/snowy-admin-web/src/views/biz/user/index.vue +++ b/snowy-admin-web/src/views/biz/user/index.vue @@ -105,7 +105,7 @@ {{ $t('common.editButton') }} - + {{ $t('common.removeButton') }} @@ -123,7 +123,7 @@ diff --git a/snowy-admin-web/src/views/dev/config/loginConfig/bForm.vue b/snowy-admin-web/src/views/dev/config/loginConfig/bForm.vue index ad9e85ed..0b2a8ff1 100644 --- a/snowy-admin-web/src/views/dev/config/loginConfig/bForm.vue +++ b/snowy-admin-web/src/views/dev/config/loginConfig/bForm.vue @@ -85,7 +85,14 @@ placeholder="请选择邮箱无对应用户时策略" /> - + + + 保存 formRef.resetFields()">重置 @@ -166,7 +173,8 @@ SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B: [required('请选择是否允许手机号登录')], SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B: [required('请选择手机号无对应用户时策略')], SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B: [required('请选择是否允许邮箱登录')], - SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B: [required('请选择邮箱无对应用户时策略')] + SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B: [required('请选择邮箱无对应用户时策略')], + SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B: [required('请选择是否允许动态口令登录')] } // 验证并提交数据 const onSubmit = () => { diff --git a/snowy-admin-web/src/views/dev/config/loginConfig/cForm.vue b/snowy-admin-web/src/views/dev/config/loginConfig/cForm.vue index f9bd38e6..697f6bd9 100644 --- a/snowy-admin-web/src/views/dev/config/loginConfig/cForm.vue +++ b/snowy-admin-web/src/views/dev/config/loginConfig/cForm.vue @@ -85,6 +85,14 @@ placeholder="请选择邮箱无对应用户时策略" /> + + + 保存 formRef.resetFields()">重置 @@ -165,7 +173,8 @@ SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C: [required('请选择是否允许手机号登录')], SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_C: [required('请选择手机号无对应用户时策略')], SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C: [required('请选择是否允许邮箱登录')], - SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C: [required('请选择邮箱无对应用户时策略')] + SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C: [required('请选择邮箱无对应用户时策略')], + SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C: [required('请选择是否允许动态口令登录')] } // 验证并提交数据 const onSubmit = () => { diff --git a/snowy-admin-web/src/views/sys/user/index.vue b/snowy-admin-web/src/views/sys/user/index.vue index 49ffcc4a..8b535cd1 100644 --- a/snowy-admin-web/src/views/sys/user/index.vue +++ b/snowy-admin-web/src/views/sys/user/index.vue @@ -98,7 +98,7 @@ {{ $t('common.editButton') }} - + {{ $t('common.removeButton') }} @@ -113,7 +113,7 @@ diff --git a/snowy-admin-web/src/views/sys/user/userCenter.vue b/snowy-admin-web/src/views/sys/user/userCenter.vue index f47806a4..2fc7ce9a 100644 --- a/snowy-admin-web/src/views/sys/user/userCenter.vue +++ b/snowy-admin-web/src/views/sys/user/userCenter.vue @@ -58,7 +58,7 @@ - + diff --git a/snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue b/snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue new file mode 100644 index 00000000..1eb5108e --- /dev/null +++ b/snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue @@ -0,0 +1,120 @@ + + + + + + + + 1.打开Google Authenticator或者Okta Verify等动态口令应用。 + 2.点击“扫一扫”或者“手动输入”,将动态口令应用中的二维码扫描到应用中。 + 3.在下方输入框中输入动态口令,即可完成绑定/解绑。 + + + + + + + + + {{otpInfo.otpInfo.issuer}} + {{otpInfo.otpInfo.account}} + {{otpInfo.otpInfo.secretKey}} + {{otpInfo.otpInfo.algorithm}} + {{otpInfo.otpInfo.digits}} + {{otpInfo.otpInfo.period}} + + + + + + + + + + + 关闭 + 保存 + + + + + + diff --git a/snowy-common/pom.xml b/snowy-common/pom.xml index 963cf0ac..0bb6addd 100644 --- a/snowy-common/pom.xml +++ b/snowy-common/pom.xml @@ -135,5 +135,23 @@ com.github.wnameless.json json-flattener + + + + com.google.zxing + core + + + + + org.bouncycastle + bcprov-jdk15on + + + + + org.bouncycastle + bcpkix-jdk15on + diff --git a/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java b/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java index a5b845e1..a54b2b29 100644 --- a/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java +++ b/snowy-common/src/main/java/vip/xiaonuo/common/prop/CommonProperties.java @@ -29,6 +29,9 @@ import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "snowy.config.common") public class CommonProperties { + /** 前端地址 */ + private String frontUrl; + /** 后端地址 */ private String backendUrl; } diff --git a/snowy-common/src/main/java/vip/xiaonuo/common/util/CommonOtpUtil.java b/snowy-common/src/main/java/vip/xiaonuo/common/util/CommonOtpUtil.java new file mode 100644 index 00000000..7fcb084e --- /dev/null +++ b/snowy-common/src/main/java/vip/xiaonuo/common/util/CommonOtpUtil.java @@ -0,0 +1,144 @@ +/* + * 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.common.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base32; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +/** + * 动态口令工具类 + * + * @author xuyuxiang + * @date 2021/12/23 21:51 + */ +@Slf4j +public class CommonOtpUtil { + + /** + * 生成动态口令密钥 + * + * @return 动态口令密钥 + */ + public static String generateSecretKey() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[20]; // 必须为 20 字节(160 位) + random.nextBytes(key); + Base32 base32 = new Base32(); + return base32.encodeToString(key).replace("=", ""); // 移除填充符 + } + + /** + * 获取动态口令 URI + * + * @param secretKey 密钥 + * @param issuer 发行者 + * @param account 账号 + * @return 动态口令 URI + */ + public static String getTotUri(String secretKey, String issuer, String account) { + return String.format( + "otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30", + urlEncode(issuer), + urlEncode(account), + secretKey, + urlEncode(issuer) + ); + } + + /** + * URL 编码 + * + * @param value 待编码的值 + * @return 编码后的值 + */ + public static String urlEncode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + /** + * 校验动态口令 + * + * @param secretKey 密钥 + * @param code 动态口令 + * @param timeWindow 时间窗口 + * @return 是否校验通过 + */ + public static boolean validateCode(String secretKey, String code, int timeWindow) { + try { + byte[] key = decodeSecretKey(secretKey); + long time = System.currentTimeMillis() / 1000 / 30; + + for (int i = -timeWindow; i <= timeWindow; i++) { + String calculatedCode = getOtpCode(key, time + i); + if (calculatedCode.equals(code)) { + return true; + } + } + } catch (Exception e) { + log.error(">>> 校验出现异常:", e); + } + return false; + } + + /** + * 解码密钥 + * + * @param secretKey 密钥 + * @return 解码后的密钥 + */ + public static byte[] decodeSecretKey(String secretKey) { + Base32 base32 = new Base32(); + // 手动补全 Base32 填充符"=" + int padding = (8 - (secretKey.length() % 8)) % 8; + return base32.decode(secretKey + "=".repeat(padding)); + } + + /** + * 获取动态口令 + * + * @param key 密钥 + * @param time 时间 + * @return 动态口令 + */ + public static String getOtpCode(byte[] key, long time) { + byte[] counter = new byte[8]; + for (int i = 7; i >= 0; i--) { + counter[i] = (byte) (time & 0xFF); + time >>= 8; + } + + Mac mac; + try { + mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(key, "HmacSHA1")); + } catch (Exception e) { + throw new RuntimeException(e); + } + byte[] hash = mac.doFinal(counter); + + int offset = hash[hash.length - 1] & 0xF; + int binary = ((hash[offset] & 0x7F) << 24) | + ((hash[offset + 1] & 0xFF) << 16) | + ((hash[offset + 2] & 0xFF) << 8) | + (hash[offset + 3] & 0xFF); + + int otp = binary % 1000000; + return String.format("%06d", otp); + } +} diff --git a/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/AuthApi.java b/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/AuthApi.java index 05ee04ea..69080282 100644 --- a/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/AuthApi.java +++ b/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/AuthApi.java @@ -109,4 +109,36 @@ public interface AuthApi { * @date 2024/7/18 17:35 */ String doLoginByAccountForC(String account, String device); + + /** + * B端手机号登录 + * + * @author yubaoshan + * @date 2024/7/18 17:35 + */ + String doLoginByPhoneForB(String phone, String device); + + /** + * C端手机号登录 + * + * @author yubaoshan + * @date 2024/7/18 17:35 + */ + String doLoginByPhoneForC(String phone, String device); + + /** + * B端邮箱登录 + * + * @author yubaoshan + * @date 2024/7/18 17:35 + */ + String doLoginByEmailForB(String email, String device); + + /** + * C端邮箱登录 + * + * @author yubaoshan + * @date 2024/7/18 17:35 + */ + String doLoginByEmailForC(String email, String device); } diff --git a/snowy-plugin-api/snowy-plugin-client-api/src/main/java/vip/xiaonuo/client/ClientUserApi.java b/snowy-plugin-api/snowy-plugin-client-api/src/main/java/vip/xiaonuo/client/ClientUserApi.java new file mode 100644 index 00000000..436bb257 --- /dev/null +++ b/snowy-plugin-api/snowy-plugin-client-api/src/main/java/vip/xiaonuo/client/ClientUserApi.java @@ -0,0 +1,74 @@ +/* + * 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.client; + +import cn.hutool.json.JSONObject; + +import java.util.List; + +/** + * 用户Api + * + * @author xuyuxiang + * @date 2022/6/6 11:33 + **/ +public interface ClientUserApi { + + /** + * 根据用户id获取用户对象,没有则返回null + * + * @author xuyuxiang + * @date 2022/6/20 18:19 + **/ + JSONObject getUserByIdWithoutException(String userId); + + /** + * 根据用户id获取用户对象列表,没有的则为空,都没有则返回空集合 + * + * @author xuyuxiang + * @date 2022/6/20 18:19 + **/ + List getUserListByIdListWithoutException(List userIdList); + + /** + * 根据用户id获取用户对象,没有则抛出异常 + * + * @author xuyuxiang + * @date 2022/6/20 18:19 + **/ + JSONObject getUserByIdWithException(String userId); + + /** + * 根据用户id获取用户对象列表,只要有一个没有则抛出异常 + * + * @author xuyuxiang + * @date 2022/6/20 18:19 + **/ + List getUserListByIdWithException(List userIdList); + + /** + * 获取用户列表(排除当前用户) + * + * @author chengchuanyao + * @date 2024/7/19 9:54 + */ + List listUserWithoutCurrent(); + + /** + * 获取用户扩展信息,没有则创建 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + JSONObject getOrCreateClientUserExt(String userId); +} diff --git a/snowy-plugin-api/snowy-plugin-sys-api/src/main/java/vip/xiaonuo/sys/api/SysUserApi.java b/snowy-plugin-api/snowy-plugin-sys-api/src/main/java/vip/xiaonuo/sys/api/SysUserApi.java index a7c8ba1e..a869fcbd 100644 --- a/snowy-plugin-api/snowy-plugin-sys-api/src/main/java/vip/xiaonuo/sys/api/SysUserApi.java +++ b/snowy-plugin-api/snowy-plugin-sys-api/src/main/java/vip/xiaonuo/sys/api/SysUserApi.java @@ -128,4 +128,12 @@ public interface SysUserApi { * @date 2022/6/20 18:19 **/ List getPositionListByUserId(String userId); + + /** + * 获取用户扩展信息,没有则创建 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + JSONObject getOrCreateSysUserExt(String userId); } diff --git a/snowy-plugin/snowy-plugin-auth/pom.xml b/snowy-plugin/snowy-plugin-auth/pom.xml index 22970636..9486d299 100644 --- a/snowy-plugin/snowy-plugin-auth/pom.xml +++ b/snowy-plugin/snowy-plugin-auth/pom.xml @@ -22,6 +22,18 @@ snowy-plugin-auth-api + + + vip.xiaonuo + snowy-plugin-sys-api + + + + + vip.xiaonuo + snowy-plugin-client-api + + vip.xiaonuo @@ -52,6 +64,12 @@ sa-token-sso + + + cn.dev33 + sa-token-forest + + me.zhyd.oauth diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/auth/AuthApiProvider.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/auth/AuthApiProvider.java index 0b292f50..63e48370 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/auth/AuthApiProvider.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/auth/AuthApiProvider.java @@ -26,6 +26,7 @@ import vip.xiaonuo.auth.api.AuthApi; import vip.xiaonuo.auth.core.enums.SaClientTypeEnum; import vip.xiaonuo.auth.core.util.StpClientUtil; import vip.xiaonuo.auth.modular.login.enums.AuthDeviceTypeEnum; +import vip.xiaonuo.auth.modular.login.enums.AuthStrategyWhenNoUserWithPhoneOrEmailEnum; import vip.xiaonuo.auth.modular.login.param.AuthAccountPasswordLoginParam; import vip.xiaonuo.auth.modular.login.service.AuthService; import vip.xiaonuo.auth.modular.third.service.AuthThirdService; @@ -137,4 +138,28 @@ public class AuthApiProvider implements AuthApi { public String doLoginByAccountForC(String account, String device) { return authService.doLoginByAccount(account, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.C.getValue()); } + + @Override + public String doLoginByPhoneForB(String phone, String device) { + return authService.doLoginByPhone(phone, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), + SaClientTypeEnum.B.getValue(), AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue()); + } + + @Override + public String doLoginByPhoneForC(String phone, String device) { + return authService.doLoginByPhone(phone, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), + SaClientTypeEnum.C.getValue(), AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue()); + } + + @Override + public String doLoginByEmailForB(String email, String device) { + return authService.doLoginByEmail(email, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), + SaClientTypeEnum.B.getValue(), AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue()); + } + + @Override + public String doLoginByEmailForC(String email, String device) { + return authService.doLoginByEmail(email, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), + SaClientTypeEnum.C.getValue(), AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue()); + } } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthClientController.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthClientController.java index 75a13127..a77a2afe 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthClientController.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/controller/AuthClientController.java @@ -168,4 +168,30 @@ public class AuthClientController { authService.register(authRegisterParam, SaClientTypeEnum.C.getValue()); return CommonResult.ok(); } + + /** + * C端动态口令登录 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + @ApiOperationSupport(order = 10) + @Operation(summary = "C端动态口令登录") + @PostMapping("/auth/c/doLoginByOtp") + public CommonResult doLoginByOtp(@RequestBody @Valid AuthOtpLoginParam authOtpLoginParam) { + return CommonResult.data(authService.doLoginByOtp(authOtpLoginParam, SaClientTypeEnum.C.getValue())); + } + + /** + * C端判断是否登录 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + @ApiOperationSupport(order = 11) + @Operation(summary = "C端判断是否登录") + @GetMapping("/auth/c/isLogin") + public CommonResult isLogin() { + return CommonResult.data(StpClientUtil.isLogin()); + } } 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 9a8287fe..257c0e9b 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 @@ -168,4 +168,30 @@ public class AuthController { authService.register(authRegisterParam, SaClientTypeEnum.B.getValue()); return CommonResult.ok(); } + + /** + * B端动态口令登录 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + @ApiOperationSupport(order = 10) + @Operation(summary = "B端动态口令登录") + @PostMapping("/auth/b/doLoginByOtp") + public CommonResult doLoginByOtp(@RequestBody @Valid AuthOtpLoginParam authOtpLoginParam) { + return CommonResult.data(authService.doLoginByOtp(authOtpLoginParam, SaClientTypeEnum.B.getValue())); + } + + /** + * B端判断是否登录 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + @ApiOperationSupport(order = 11) + @Operation(summary = "B端判断是否登录") + @GetMapping("/auth/b/isLogin") + public CommonResult isLogin() { + return CommonResult.data(StpUtil.isLogin()); + } } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthOtpLoginParam.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthOtpLoginParam.java new file mode 100644 index 00000000..e98549d7 --- /dev/null +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/login/param/AuthOtpLoginParam.java @@ -0,0 +1,51 @@ +/* + * 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; + +/** + * 动态口令登录参数 + * + * @author xuyuxiang + * @date 2022/7/7 16:46 + **/ +@Getter +@Setter +public class AuthOtpLoginParam { + + /** 账号 */ + @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "account不能为空") + private String account; + + /** 动态口令 */ + @Schema(description = "动态口令", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "otpCode不能为空") + private String otpCode; + + /** 设备 */ + @Schema(description = "设备") + private String device; + + /** 验证码 */ + @Schema(description = "验证码") + private String validCode; + + /** 验证码请求号 */ + @Schema(description = "验证码请求号") + private String validCodeReqNo; +} 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 f690293d..f71f6b04 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 @@ -105,6 +105,22 @@ public interface AuthService { */ String doLoginByAccount(String account, String device, String type); + /** + * 手机号登录 + * + * @author xuyuxiang + * @date 2021/12/28 14:46 + **/ + String doLoginByPhone(String phone, String device, String type, String strategy); + + /** + * 邮箱登录 + * + * @author xuyuxiang + * @date 2021/12/28 14:46 + **/ + String doLoginByEmail(String email, String device, String type, String strategy); + /** * C端注册 * @@ -113,6 +129,14 @@ public interface AuthService { */ void register(AuthRegisterParam authRegisterParam, String type); + /** + * B端动态口令登录 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + String doLoginByOtp(AuthOtpLoginParam authOtpLoginParam, String type); + /** * 校验验证码 * 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 4e8c2b05..bc76d77b 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 @@ -42,15 +42,18 @@ import vip.xiaonuo.auth.modular.login.enums.AuthStrategyWhenNoUserWithPhoneOrEma import vip.xiaonuo.auth.modular.login.param.*; import vip.xiaonuo.auth.modular.login.result.AuthPicValidCodeResult; import vip.xiaonuo.auth.modular.login.service.AuthService; +import vip.xiaonuo.client.ClientUserApi; import vip.xiaonuo.common.cache.CommonCacheOperator; import vip.xiaonuo.common.consts.CacheConstant; import vip.xiaonuo.common.exception.CommonException; import vip.xiaonuo.common.util.CommonCryptogramUtil; import vip.xiaonuo.common.util.CommonEmailUtil; +import vip.xiaonuo.common.util.CommonOtpUtil; import vip.xiaonuo.common.util.CommonTimeFormatUtil; 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 java.util.List; import java.util.stream.Collectors; @@ -118,6 +121,12 @@ public class AuthServiceImpl implements AuthService { /** C端邮箱登录是否开启 */ private static final String SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C_KEY = "SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C"; + /** B端动态口令登录是否开启 */ + private static final String SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B_KEY = "SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B"; + + /** C端动态口令登录是否开启 */ + private static final String SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C_KEY = "SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C"; + /** B端手机号无对应用户时策略 */ private static final String SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B_KEY = "SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B"; @@ -160,6 +169,12 @@ public class AuthServiceImpl implements AuthService { @Resource private CommonCacheOperator commonCacheOperator; + @Resource + private SysUserApi sysUserApi; + + @Resource + private ClientUserApi clientUserApi; + @Override public AuthPicValidCodeResult getPicCaptcha(String type) { // 生成验证码,随机4位字符 @@ -243,6 +258,8 @@ public class AuthServiceImpl implements AuthService { if(!Convert.toBool(allowPhoneLoginFlag)) { throw new CommonException("管理员未开启手机号登录"); } + } else { + throw new CommonException("管理员未开启手机号登录"); } } @@ -308,6 +325,8 @@ public class AuthServiceImpl implements AuthService { if(!Convert.toBool(allowEmailLoginFlag)) { throw new CommonException("管理员未开启邮箱登录"); } + } else { + throw new CommonException("管理员未开启邮箱登录"); } } @@ -519,62 +538,8 @@ public class AuthServiceImpl implements AuthService { authPhoneValidCodeLoginParam.getValidCodeReqNo(), type); // 设备 String device = authPhoneValidCodeLoginParam.getDevice(); - // 默认指定为PC,如在小程序跟移动端的情况下,自行指定即可 - if(ObjectUtil.isEmpty(device)) { - device = AuthDeviceTypeEnum.PC.getValue(); - } else { - AuthDeviceTypeEnum.validate(device); - } - // 根据手机号获取用户信息,根据B端或C端判断 - if(SaClientTypeEnum.B.getValue().equals(type)) { - // 判断手机号无对应用户时的策略,如果为空则直接抛出异常 - if(ObjectUtil.isEmpty(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("手机号码:{}不存在对应用户", phone); - } else { - // 如果不允许登录,则抛出异常 - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("手机号码:{}不存在对应用户", phone); - } else { - // 定义B端用户 - SaBaseLoginUser saBaseLoginUser; - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 允许登录,即用户存在 - saBaseLoginUser = loginUserApi.getUserByPhone(phone); - }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 根据手机号自动创建B端用户 - saBaseLoginUser = loginUserApi.createUserWithPhone(phone); - } else { - throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategyWhenNoUserWithPhoneOrEmail); - } - // 执行B端登录 - return execLoginB(saBaseLoginUser, device); - } - } - } else { - // 判断手机号无对应用户时的策略,如果为空则直接抛出异常 - if(ObjectUtil.isEmpty(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("手机号码:{}不存在对应用户", phone); - } else { - // 如果不允许登录,则抛出异常 - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("手机号码:{}不存在对应用户", phone); - } else { - // 定义C端用户 - SaBaseClientLoginUser saBaseClientLoginUser; - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 允许登录,即用户存在 - saBaseClientLoginUser = clientLoginUserApi.getClientUserByPhone(phone); - }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 根据手机号自动创建B端用户 - saBaseClientLoginUser = clientLoginUserApi.createClientUserWithPhone(phone); - } else { - throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategyWhenNoUserWithPhoneOrEmail); - } - // 执行C端登录 - return execLoginC(saBaseClientLoginUser, device); - } - } - } + // 执行登录 + return this.doLoginByPhone(phone, device, type, strategyWhenNoUserWithPhoneOrEmail); } @Override @@ -589,62 +554,8 @@ public class AuthServiceImpl implements AuthService { authEmailValidCodeLoginParam.getValidCodeReqNo(), type); // 设备 String device = authEmailValidCodeLoginParam.getDevice(); - // 默认指定为PC,如在小程序跟移动端的情况下,自行指定即可 - if(ObjectUtil.isEmpty(device)) { - device = AuthDeviceTypeEnum.PC.getValue(); - } else { - AuthDeviceTypeEnum.validate(device); - } - // 根据邮箱获取用户信息,根据B端或C端判断 - if(SaClientTypeEnum.B.getValue().equals(type)) { - // 判断邮箱无对应用户时的策略,如果为空则直接抛出异常 - if(ObjectUtil.isEmpty(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("邮箱:{}不存在对应用户", email); - } else { - // 如果不允许登录,则抛出异常 - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("邮箱:{}不存在对应用户", email); - } else { - // 定义B端用户 - SaBaseLoginUser saBaseLoginUser; - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 允许登录,即用户存在 - saBaseLoginUser = loginUserApi.getUserByEmail(email); - }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 根据邮箱自动创建B端用户 - saBaseLoginUser = loginUserApi.createUserWithEmail(email); - } else { - throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategyWhenNoUserWithPhoneOrEmail); - } - // 执行B端登录 - return execLoginB(saBaseLoginUser, device); - } - } - } else { - // 判断邮箱无对应用户时的策略,如果为空则直接抛出异常 - if(ObjectUtil.isEmpty(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("邮箱:{}不存在对应用户", email); - } else { - // 如果不允许登录,则抛出异常 - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - throw new CommonException("邮箱:{}不存在对应用户", email); - } else { - // 定义C端用户 - SaBaseClientLoginUser saBaseClientLoginUser; - if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 允许登录,即用户存在 - saBaseClientLoginUser = clientLoginUserApi.getClientUserByEmail(email); - }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategyWhenNoUserWithPhoneOrEmail)) { - // 根据邮箱自动创建B端用户 - saBaseClientLoginUser = loginUserApi.createClientUserWithEmail(email); - } else { - throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategyWhenNoUserWithPhoneOrEmail); - } - // 执行C端登录 - return execLoginC(saBaseClientLoginUser, device); - } - } - } + // 执行登录 + return this.doLoginByEmail(email, device, type, strategyWhenNoUserWithPhoneOrEmail); } /** @@ -954,6 +865,179 @@ public class AuthServiceImpl implements AuthService { } } + @Override + public String doLoginByPhone(String phone, String device, String type, String strategy) { + // 默认指定为PC,如在小程序跟移动端的情况下,自行指定即可 + if(ObjectUtil.isEmpty(device)) { + device = AuthDeviceTypeEnum.PC.getValue(); + } else { + AuthDeviceTypeEnum.validate(device); + } + // 根据手机号获取用户信息,根据B端或C端判断 + if(SaClientTypeEnum.B.getValue().equals(type)) { + // 判断手机号无对应用户时的策略,如果为空则直接抛出异常 + if(ObjectUtil.isEmpty(strategy)) { + throw new CommonException("手机号码:{}不存在对应用户", phone); + } else { + // 如果不允许登录,则抛出异常 + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategy)) { + // 依然先查询该用户是否存在 + SaBaseLoginUser saBaseLoginUser = loginUserApi.getUserByPhone(phone); + // 如果不存在则抛出异常 + if(ObjectUtil.isEmpty(saBaseLoginUser)) { + throw new CommonException("手机号码:{}不存在对应用户", phone); + } + // 执行B端登录 + return execLoginB(saBaseLoginUser, device); + } else { + // 定义B端用户 + SaBaseLoginUser saBaseLoginUser; + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategy)) { + // 允许登录,即用户存在 + saBaseLoginUser = loginUserApi.getUserByPhone(phone); + // 如果用户不存在,则抛出异常 + if(ObjectUtil.isEmpty(saBaseLoginUser)) { + throw new CommonException("手机号码:{}不存在对应用户", phone); + } + }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategy)) { + // 依然先查询该用户是否存在 + saBaseLoginUser = loginUserApi.getUserByPhone(phone); + // 如果不存在则创建 + if(ObjectUtil.isEmpty(saBaseLoginUser)) { + // 根据手机号自动创建B端用户 + saBaseLoginUser = loginUserApi.createUserWithPhone(phone); + } + } else { + throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategy); + } + // 执行B端登录 + return execLoginB(saBaseLoginUser, device); + } + } + } else { + // 判断手机号无对应用户时的策略,如果为空则直接抛出异常 + if(ObjectUtil.isEmpty(strategy)) { + throw new CommonException("手机号码:{}不存在对应用户", phone); + } else { + // 如果不允许登录,则抛出异常 + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategy)) { + // /依然先查询该用户是否存在 + SaBaseClientLoginUser saBaseClientLoginUser = clientLoginUserApi.getClientUserByPhone(phone); + // 如果不存在则抛出异常 + if(ObjectUtil.isEmpty(saBaseClientLoginUser)) { + throw new CommonException("手机号码:{}不存在对应用户", phone); + } + // 执行C端登录 + return execLoginC(saBaseClientLoginUser, device); + } else { + // 定义C端用户 + SaBaseClientLoginUser saBaseClientLoginUser; + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategy)) { + // 允许登录,即用户存在 + saBaseClientLoginUser = clientLoginUserApi.getClientUserByPhone(phone); + // 如果用户不存在,则抛出异常 + if(ObjectUtil.isEmpty(saBaseClientLoginUser)) { + throw new CommonException("手机号码:{}不存在对应用户", phone); + } + }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategy)) { + // 依然先查询该用户是否存在 + saBaseClientLoginUser = clientLoginUserApi.getClientUserByPhone(phone); + // 如果不存在则创建 + if(ObjectUtil.isEmpty(saBaseClientLoginUser)) { + // 根据手机号自动创建C端用户 + saBaseClientLoginUser = clientLoginUserApi.createClientUserWithPhone(phone); + } + } else { + throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategy); + } + // 执行C端登录 + return execLoginC(saBaseClientLoginUser, device); + } + } + } + } + + @Override + public String doLoginByEmail(String email, String device, String type, String strategy) { + // 默认指定为PC,如在小程序跟移动端的情况下,自行指定即可 + if(ObjectUtil.isEmpty(device)) { + device = AuthDeviceTypeEnum.PC.getValue(); + } else { + AuthDeviceTypeEnum.validate(device); + } + // 根据邮箱获取用户信息,根据B端或C端判断 + if(SaClientTypeEnum.B.getValue().equals(type)) { + // 判断邮箱无对应用户时的策略,如果为空则直接抛出异常 + if(ObjectUtil.isEmpty(strategy)) { + throw new CommonException("邮箱:{}不存在对应用户", email); + } else { + // 如果不允许登录,则抛出异常 + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategy)) { + // 依然先查询该用户是否存在 + SaBaseLoginUser saBaseLoginUser = loginUserApi.getUserByEmail(email); + // 如果不存在则抛出异常 + if(ObjectUtil.isEmpty(saBaseLoginUser)) { + throw new CommonException("邮箱:{}不存在对应用户", email); + } + // 执行B端登录 + return execLoginB(saBaseLoginUser, device); + } else { + // 定义B端用户 + SaBaseLoginUser saBaseLoginUser; + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategy)) { + // 允许登录,即用户存在 + saBaseLoginUser = loginUserApi.getUserByEmail(email); + // 如果用户不存在,则抛出异常 + if(ObjectUtil.isEmpty(saBaseLoginUser)) { + throw new CommonException("邮箱:{}不存在对应用户", email); + } + }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategy)) { + // 依然先查询该用户是否存在 + saBaseLoginUser = loginUserApi.getUserByEmail(email); + // 如果不存在则创建 + if(ObjectUtil.isEmpty(saBaseLoginUser)) { + // 根据邮箱自动创建B端用户 + saBaseLoginUser = loginUserApi.createUserWithEmail(email); + } + } else { + throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategy); + } + // 执行B端登录 + return execLoginB(saBaseLoginUser, device); + } + } + } else { + // 判断邮箱无对应用户时的策略,如果为空则直接抛出异常 + if(ObjectUtil.isEmpty(strategy)) { + throw new CommonException("邮箱:{}不存在对应用户", email); + } else { + // 如果不允许登录,则抛出异常 + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue().equals(strategy)) { + throw new CommonException("邮箱:{}不存在对应用户", email); + } else { + // 定义C端用户 + SaBaseClientLoginUser saBaseClientLoginUser; + if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.ALLOW_LOGIN.getValue().equals(strategy)) { + // 允许登录,即用户存在 + saBaseClientLoginUser = clientLoginUserApi.getClientUserByEmail(email); + }else if(AuthStrategyWhenNoUserWithPhoneOrEmailEnum.AUTO_CREATE_USER.getValue().equals(strategy)) { + // 依然先查询该用户是否存在 + saBaseClientLoginUser = clientLoginUserApi.getClientUserByEmail(email); + // 如果不存在则创建 + if(ObjectUtil.isEmpty(saBaseClientLoginUser)) { + // 根据邮箱自动创建C端用户 + saBaseClientLoginUser = clientLoginUserApi.createClientUserWithEmail(email); + } + } else { + throw new CommonException("不支持的手机号或邮箱无对应用户时策略类型:{}", strategy); + } + // 执行C端登录 + return execLoginC(saBaseClientLoginUser, device); + } + } + } + } + @Override public void register(AuthRegisterParam authRegisterParam, String type) { // 校验是否允许注册 @@ -995,6 +1079,105 @@ public class AuthServiceImpl implements AuthService { } } + @Override + public String doLoginByOtp(AuthOtpLoginParam authOtpLoginParam, String type) { + // 校验是否允许动态口令登录 + this.checkAllowOtpLoginFlag(type); + // 定义验证码是否打开 + boolean defaultCaptchaOpen; + if(SaClientTypeEnum.B.getValue().equals(type)) { + defaultCaptchaOpen = this.getDefaultCaptchaOpenForB(); + } else { + defaultCaptchaOpen = this.getDefaultCaptchaOpenForC(); + } + // 获取验证码 + String validCode = authOtpLoginParam.getValidCode(); + // 获取请求号 + String validCodeReqNo = authOtpLoginParam.getValidCodeReqNo(); + // 验证码不能为空校验 + if(defaultCaptchaOpen) { + if(ObjectUtil.hasEmpty(validCode, validCodeReqNo)) { + throw new CommonException("验证码不能为空"); + } + // 校验验证码 + this.validValidCode(null, authOtpLoginParam.getValidCode(), authOtpLoginParam.getValidCodeReqNo()); + } + // 获取账号 + String account = authOtpLoginParam.getAccount(); + // 定义用户id + String userId; + // 根据id获取用户信息,根据B端或C端判断 + if(SaClientTypeEnum.B.getValue().equals(type)) { + SaBaseLoginUser saBaseLoginUser = loginUserApi.getUserByAccount(account); + if (ObjectUtil.isEmpty(saBaseLoginUser)) { + throw new CommonException("账号错误"); + } + userId = saBaseLoginUser.getId(); + } else { + SaBaseClientLoginUser saBaseClientLoginUser = clientLoginUserApi.getClientUserByAccount(account); + if (ObjectUtil.isEmpty(saBaseClientLoginUser)) { + throw new CommonException("账号错误"); + } + userId = saBaseClientLoginUser.getId(); + } + // 获取用户扩展信息 + String otpSecretKey; + if(SaClientTypeEnum.B.getValue().equals(type)) { + JSONObject sysUserExtJsonObject = sysUserApi.getOrCreateSysUserExt(userId); + if(!sysUserExtJsonObject.getBool("hasBindOtp")) { + throw new CommonException("该账号未绑定动态口令"); + } + // 解密密钥 + otpSecretKey = CommonCryptogramUtil.doSm4CbcDecrypt(sysUserExtJsonObject.getStr("otpSecretKey")); + } else { + JSONObject clientUserExtJsonObject = clientUserApi.getOrCreateClientUserExt(userId); + if(!clientUserExtJsonObject.getBool("hasBindOtp")) { + throw new CommonException("该账号未绑定动态口令"); + } + // 解密密钥 + otpSecretKey = CommonCryptogramUtil.doSm4CbcDecrypt(clientUserExtJsonObject.getStr("otpSecretKey")); + } + + // 获取动态口令 + String otpCode = authOtpLoginParam.getOtpCode(); + // 校验动态口令 + boolean isValid = CommonOtpUtil.validateCode(otpSecretKey, otpCode, 1); + if(!isValid){ + throw new CommonException("动态口令错误"); + } + // 获取设备 + String device = authOtpLoginParam.getDevice(); + // 执行登录 + if(SaClientTypeEnum.B.getValue().equals(type)) { + return this.doLoginByAccount(account, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.B.getValue()); + } else { + return this.doLoginByAccount(account, ObjectUtil.isNotEmpty(device)?device:AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.C.getValue()); + } + } + + /** + * 校验是否允许动态口令登录 + * + * @author xuyuxiang + * @date 2022/8/25 15:16 + **/ + private void checkAllowOtpLoginFlag(String type) { + // 是否允许邮箱登录 + String allowOtpLoginFlag; + if(SaClientTypeEnum.B.getValue().equals(type)) { + allowOtpLoginFlag = devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B_KEY); + } else { + allowOtpLoginFlag = devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C_KEY); + } + if(ObjectUtil.isNotEmpty(allowOtpLoginFlag)) { + if(!Convert.toBool(allowOtpLoginFlag)) { + throw new CommonException("管理员未开启动态口令登录"); + } + } else { + throw new CommonException("管理员未开启动态口令登录"); + } + } + /** * 校验是否开启注册 * @@ -1013,6 +1196,8 @@ public class AuthServiceImpl implements AuthService { if(!Convert.toBool(allowRegisterFlag)) { throw new CommonException("管理员未开启注册"); } + } else { + throw new CommonException("管理员未开启注册"); } } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/config/AuthSsoConfigure.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/config/AuthSsoConfigure.java new file mode 100644 index 00000000..2e4b43f6 --- /dev/null +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/config/AuthSsoConfigure.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.sso.config; + +import cn.dev33.satoken.sso.config.SaSsoClientConfig; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import vip.xiaonuo.common.prop.CommonProperties; + +/** + * 单点登录客户端配置 + * + * @author xuyuxiang + * @date 2021/10/9 14:24 + **/ +@Configuration +public class AuthSsoConfigure { + + @Resource + private CommonProperties commonProperties; + + /** + * 配置SSO客户端相关参数 + */ + @Autowired + private void configSsoClient(SaSsoClientConfig saSsoClientConfig) { + saSsoClientConfig.setCurrSsoLogin(commonProperties.getBackendUrl() + "/auth/sso/b/doLoginByTicket"); + saSsoClientConfig.setCurrSsoLogoutCall(commonProperties.getBackendUrl() + "/auth/sso/b/logoutCall"); + saSsoClientConfig.setIsHttp(true); + saSsoClientConfig.setIsSlo(true); + saSsoClientConfig.setRegLogoutCall(true); + saSsoClientConfig.setIsCheckSign(true); + } +} diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/controller/AuthSsoController.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/controller/AuthSsoController.java index b5e1e5e7..1aa8a16b 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/controller/AuthSsoController.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/controller/AuthSsoController.java @@ -18,10 +18,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import vip.xiaonuo.auth.core.enums.SaClientTypeEnum; +import vip.xiaonuo.auth.modular.sso.param.AuthGetSsoAuthUrlParam; import vip.xiaonuo.auth.modular.sso.param.AuthSsoTicketLoginParam; import vip.xiaonuo.auth.modular.sso.service.AuthSsoService; import vip.xiaonuo.common.pojo.CommonResult; @@ -44,15 +43,54 @@ public class AuthSsoController { private AuthSsoService authSsoService; /** - * 根据ticket执行单点登录 + * B端获取认证中心地址 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + @ApiOperationSupport(order = 1) + @Operation(summary = "B端获取认证中心地址") + @GetMapping("/auth/sso/b/getSsoAuthUrl") + public CommonResult getSsoAuthUrl(@Valid AuthGetSsoAuthUrlParam authGetSsoAuthUrlParam) { + return CommonResult.data(authSsoService.getSsoAuthUrl(authGetSsoAuthUrlParam, SaClientTypeEnum.B.getValue())); + } + + /** + * B端根据ticket执行单点登录 * * @author xuyuxiang * @date 2021/10/15 13:12 **/ - @ApiOperationSupport(order = 1) - @Operation(summary = "根据ticket执行单点登录") - @PostMapping("/auth/sso/doLogin") - public CommonResult doLogin(@RequestBody @Valid AuthSsoTicketLoginParam authAccountPasswordLoginParam) { - return CommonResult.data(authSsoService.doLogin(authAccountPasswordLoginParam, SaClientTypeEnum.B.getValue())); + @ApiOperationSupport(order = 2) + @Operation(summary = "B端根据ticket执行单点登录") + @PostMapping("/auth/sso/b/doLoginByTicket") + public CommonResult doLoginByTicket(@RequestBody @Valid AuthSsoTicketLoginParam authSsoTicketLoginParam) { + return CommonResult.data(authSsoService.doLoginByTicket(authSsoTicketLoginParam, SaClientTypeEnum.B.getValue())); + } + + /** + * B端单点注销回调 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + @ApiOperationSupport(order = 3) + @Operation(summary = "B端单点注销回调") + @RequestMapping("/auth/sso/b/logoutCall") + public Object logoutCall() { + return authSsoService.logoutCall(SaClientTypeEnum.B.getValue()); + } + + /** + * B端推送客户端地址 + * + * @author xuyuxiang + * @date 2021/10/15 13:12 + **/ + @ApiOperationSupport(order = 4) + @Operation(summary = "推送客户端地址") + @RequestMapping("/auth/sso/b/pushClient") + public Object pushClient() { + return authSsoService.pushClient(SaClientTypeEnum.B.getValue()); } } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/param/AuthGetSsoAuthUrlParam.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/param/AuthGetSsoAuthUrlParam.java new file mode 100644 index 00000000..cb845779 --- /dev/null +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/param/AuthGetSsoAuthUrlParam.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.sso.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * 获取认证中心地址参数 + * + * @author xuyuxiang + * @date 2022/7/7 16:46 + **/ +@Getter +@Setter +public class AuthGetSsoAuthUrlParam { + + /** 跳转地址 */ + @Schema(description = "跳转地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "redirectUrl不能为空") + private String redirectUrl; +} diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/AuthSsoService.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/AuthSsoService.java index 96bbfb58..0538132a 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/AuthSsoService.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/AuthSsoService.java @@ -12,6 +12,7 @@ */ package vip.xiaonuo.auth.modular.sso.service; +import vip.xiaonuo.auth.modular.sso.param.AuthGetSsoAuthUrlParam; import vip.xiaonuo.auth.modular.sso.param.AuthSsoTicketLoginParam; /** @@ -22,11 +23,35 @@ import vip.xiaonuo.auth.modular.sso.param.AuthSsoTicketLoginParam; **/ public interface AuthSsoService { + /** + * 获取认证中心地址 + * + * @author xuyuxiang + * @date 2022/8/30 9:36 + **/ + String getSsoAuthUrl(AuthGetSsoAuthUrlParam authGetSsoAuthUrlParam, String type); + /** * 根据ticket执行单点登录 * * @author xuyuxiang * @date 2022/8/30 9:36 **/ - String doLogin(AuthSsoTicketLoginParam authAccountPasswordLoginParam, String value); + String doLoginByTicket(AuthSsoTicketLoginParam authSsoTicketLoginParam, String type); + + /** + * 单点注销回调 + * + * @author xuyuxiang + * @date 2022/8/30 9:36 + **/ + Object logoutCall(String type); + + /** + * 推送客户端地址 + * + * @author xuyuxiang + * @date 2022/8/30 9:36 + **/ + Object pushClient(String type); } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/impl/AuthSsoServiceImpl.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/impl/AuthSsoServiceImpl.java index bc8bf964..b19a848c 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/impl/AuthSsoServiceImpl.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/sso/service/impl/AuthSsoServiceImpl.java @@ -12,11 +12,22 @@ */ package vip.xiaonuo.auth.modular.sso.service.impl; +import cn.dev33.satoken.sso.model.SaCheckTicketResult; +import cn.dev33.satoken.sso.processor.SaSsoClientProcessor; +import cn.dev33.satoken.sso.template.SaSsoClientUtil; +import cn.dev33.satoken.util.SaResult; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ObjectUtil; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; +import vip.xiaonuo.auth.core.enums.SaClientTypeEnum; +import vip.xiaonuo.auth.modular.login.enums.AuthDeviceTypeEnum; +import vip.xiaonuo.auth.modular.login.enums.AuthStrategyWhenNoUserWithPhoneOrEmailEnum; import vip.xiaonuo.auth.modular.login.service.AuthService; +import vip.xiaonuo.auth.modular.sso.param.AuthGetSsoAuthUrlParam; import vip.xiaonuo.auth.modular.sso.param.AuthSsoTicketLoginParam; import vip.xiaonuo.auth.modular.sso.service.AuthSsoService; +import vip.xiaonuo.common.exception.CommonException; /** * 单点登录Service接口实现类 @@ -31,7 +42,58 @@ public class AuthSsoServiceImpl implements AuthSsoService { private AuthService authService; @Override - public String doLogin(AuthSsoTicketLoginParam authAccountPasswordLoginParam, String device) { - return null; + public String getSsoAuthUrl(AuthGetSsoAuthUrlParam authGetSsoAuthUrlParam, String type) { + if(SaClientTypeEnum.B.getValue().equals(type)) { + return SaSsoClientUtil.buildServerAuthUrl(authGetSsoAuthUrlParam.getRedirectUrl(), ""); + } else { + throw new CommonException("不支持的客户端类型:{}", type); + } + } + + @Override + public String doLoginByTicket(AuthSsoTicketLoginParam authSsoTicketLoginParam, String type) { + if(SaClientTypeEnum.B.getValue().equals(type)) { + SaCheckTicketResult saCheckTicketResult = SaSsoClientProcessor.instance.checkTicket(authSsoTicketLoginParam.getTicket()); + // 获取用户信息 + SaResult result = saCheckTicketResult.result; + Object account = result.get("account"); + Object phone = result.get("phone"); + Object email = result.get("email"); + if(ObjectUtil.isNotEmpty(account)) { + return authService.doLoginByAccount(Convert.toStr(account), + ObjectUtil.isEmpty(authSsoTicketLoginParam.getDevice()) ? AuthDeviceTypeEnum.PC.getValue() : authSsoTicketLoginParam.getDevice(), + SaClientTypeEnum.B.getValue()); + } else if(ObjectUtil.isNotEmpty(phone)) { + return authService.doLoginByPhone(Convert.toStr(phone), + ObjectUtil.isEmpty(authSsoTicketLoginParam.getDevice()) ? AuthDeviceTypeEnum.PC.getValue() : authSsoTicketLoginParam.getDevice(), + SaClientTypeEnum.B.getValue(), AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue()); + } else if(ObjectUtil.isNotEmpty(email)) { + return authService.doLoginByEmail(Convert.toStr(email), + ObjectUtil.isEmpty(authSsoTicketLoginParam.getDevice()) ? AuthDeviceTypeEnum.PC.getValue() : authSsoTicketLoginParam.getDevice(), + SaClientTypeEnum.B.getValue(), AuthStrategyWhenNoUserWithPhoneOrEmailEnum.NOT_ALLOW_LOGIN.getValue()); + } else { + throw new CommonException("登录失败,根据账号、手机号、邮箱未匹配到用户"); + } + } else { + throw new CommonException("不支持的客户端类型:{}", type); + } + } + + @Override + public Object logoutCall(String type) { + if(SaClientTypeEnum.B.getValue().equals(type)) { + return SaSsoClientProcessor.instance.ssoLogoutCall(); + } else { + throw new CommonException("不支持的客户端类型:{}", type); + } + } + + @Override + public Object pushClient(String type) { + if(SaClientTypeEnum.B.getValue().equals(type)) { + return SaSsoClientProcessor.instance.ssoPushC(); + } else { + throw new CommonException("不支持的客户端类型:{}", type); + } } } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/controller/AuthThirdController.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/controller/AuthThirdController.java index 1a20569c..81fc3ada 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/controller/AuthThirdController.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/controller/AuthThirdController.java @@ -21,8 +21,11 @@ import jakarta.annotation.Resource; import me.zhyd.oauth.model.AuthCallback; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import vip.xiaonuo.auth.modular.third.entity.AuthThirdUser; +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; @@ -73,13 +76,26 @@ public class AuthThirdController { return CommonResult.data(authThirdService.callback(authThirdCallbackParam, authCallback)); } + /** + * 第三方登录绑定账号 + * + * @author xuyuxiang + * @date 2022/7/8 16:42 + **/ + @ApiOperationSupport(order = 3) + @Operation(summary = "第三方登录绑定账号") + @PostMapping("/auth/third/bindAccount") + public CommonResult bindAccount(@RequestBody @Valid AuthThirdBindAccountParam authThirdBindAccountParam) { + return CommonResult.data(authThirdService.bindAccount(authThirdBindAccountParam)); + } + /** * 获取三方用户分页 * * @author xuyuxiang * @date 2022/4/24 20:00 */ - @ApiOperationSupport(order = 3) + @ApiOperationSupport(order = 4) @Operation(summary = "获取三方用户分页") @GetMapping("/auth/third/page") public CommonResult> page(AuthThirdUserPageParam authThirdUserPageParam) { diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdBindAccountParam.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdBindAccountParam.java new file mode 100644 index 00000000..c003bdf4 --- /dev/null +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdBindAccountParam.java @@ -0,0 +1,56 @@ +/* + * 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.third.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * 第三方登录绑定账号参数 + * + * @author xuyuxiang + * @date 2022/7/7 16:46 + **/ +@Getter +@Setter +public class AuthThirdBindAccountParam { + + /** 三方主键 */ + @Schema(description = "三方主键", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "thirdId不能为空") + private String thirdId; + + /** 账号 */ + @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "account不能为空") + private String account; + + /** 密码 */ + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "password不能为空") + private String password; + + /** 设备 */ + @Schema(description = "设备") + private String device; + + /** 验证码 */ + @Schema(description = "验证码") + private String validCode; + + /** 验证码请求号 */ + @Schema(description = "验证码请求号") + private String validCodeReqNo; +} diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdRenderParam.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdRenderParam.java index 19bf8daf..56c92fe1 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdRenderParam.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/param/AuthThirdRenderParam.java @@ -31,4 +31,9 @@ public class AuthThirdRenderParam { @Schema(description = "第三方平台标识", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "platform不能为空") private String platform; + + /** 登录端类型 */ + @Schema(description = "登录端类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "clientType不能为空") + private String clientType; } diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/AuthThirdService.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/AuthThirdService.java index c53f6660..3ff7a54e 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/AuthThirdService.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/AuthThirdService.java @@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import me.zhyd.oauth.model.AuthCallback; import vip.xiaonuo.auth.modular.third.entity.AuthThirdUser; +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; @@ -45,6 +46,14 @@ public interface AuthThirdService extends IService { **/ String callback(AuthThirdCallbackParam authThirdCallbackParam, AuthCallback authCallback); + /** + * 第三方登录绑定账号 + * + * @author xuyuxiang + * @date 2022/4/24 20:08 + */ + String bindAccount(AuthThirdBindAccountParam authThirdBindAccountParam); + /** * 获取三方用户分页 * diff --git a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/impl/AuthThirdServiceImpl.java b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/impl/AuthThirdServiceImpl.java index 25dd46f9..259a724a 100644 --- a/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/impl/AuthThirdServiceImpl.java +++ b/snowy-plugin/snowy-plugin-auth/src/main/java/vip/xiaonuo/auth/modular/third/service/impl/AuthThirdServiceImpl.java @@ -12,6 +12,7 @@ */ package vip.xiaonuo.auth.modular.third.service.impl; +import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; @@ -34,17 +35,19 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vip.xiaonuo.auth.api.SaBaseLoginUserApi; import vip.xiaonuo.auth.core.enums.SaClientTypeEnum; -import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser; import vip.xiaonuo.auth.modular.login.enums.AuthDeviceTypeEnum; +import vip.xiaonuo.auth.modular.login.param.AuthAccountPasswordLoginParam; import vip.xiaonuo.auth.modular.login.service.AuthService; import vip.xiaonuo.auth.modular.third.entity.AuthThirdUser; import vip.xiaonuo.auth.modular.third.enums.AuthThirdPlatformEnum; import vip.xiaonuo.auth.modular.third.mapper.AuthThirdMapper; +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.result.AuthThirdRenderResult; import vip.xiaonuo.auth.modular.third.service.AuthThirdService; +import vip.xiaonuo.common.cache.CommonCacheOperator; import vip.xiaonuo.common.enums.CommonSortOrderEnum; import vip.xiaonuo.common.exception.CommonException; import vip.xiaonuo.common.page.CommonPageRequest; @@ -59,6 +62,9 @@ import vip.xiaonuo.dev.api.DevConfigApi; @Service public class AuthThirdServiceImpl extends ServiceImpl implements AuthThirdService { + /** 缓存前缀 */ + private static final String CONFIG_CACHE_KEY = "auth-third-state:"; + 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"; @@ -67,6 +73,9 @@ public class AuthThirdServiceImpl extends ServiceImpl authResponse = authRequest.login(authCallback); if (authResponse.ok()) { - // 授权的用户信息 AuthUser authUser = authResponse.getData(); - // 获取第三方用户id String uuid = authUser.getUuid(); - // 获取第三方用户来源 String source = authUser.getSource(); - // 根据第三方用户id和用户来源获取用户信息 AuthThirdUser authThirdUser = this.getOne(new LambdaQueryWrapper().eq(AuthThirdUser::getThirdId, uuid) .eq(AuthThirdUser::getCategory, source)); - // 定义系统用户id String userId; if(ObjectUtil.isEmpty(authThirdUser)) { - - // 如果用户不存在,则绑定用户并登录 - userId = this.bindUser(authUser); + // 如果用户不存在,则需要绑定用户,先将第三方用户id插入数据库 + String id = this.insertAuthThirdUser(authUser); + // 返回 + return "needBind:" + id; } else { - // 否则直接获取用户id登录 + // 否则直接获取用户id,判断是否存在(有可能没绑定) userId = authThirdUser.getUserId(); + if(ObjectUtil.isEmpty(userId)) { + return "needBind:" + authThirdUser.getId(); + } + } + // 根据客户端类型执行登录,返回token + if(SaClientTypeEnum.B.getValue().equals(clientType)) { + return authService.doLoginById(userId, AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.B.getValue()); + } else { + return authService.doLoginById(userId, AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.C.getValue()); } - // TODO 此处使用PC端执行B端登录,返回token - return authService.doLoginById(userId, AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.B.getValue()); } else { throw new CommonException("第三方登录授权回调失败,原因:{}", authResponse.getMsg()); } } + @Transactional(rollbackFor = Exception.class) + @Override + public String bindAccount(AuthThirdBindAccountParam authThirdBindAccountParam) { + AuthThirdUser authThirdUser = this.getById(authThirdBindAccountParam.getThirdId()); + if(ObjectUtil.isEmpty(authThirdUser)) { + throw new CommonException("三方用户不存在"); + } + if(ObjectUtil.isNotEmpty(authThirdUser.getUserId())) { + throw new CommonException("三方用户已绑定,不可重复绑定"); + } + AuthAccountPasswordLoginParam authAccountPasswordLoginParam = new AuthAccountPasswordLoginParam(); + authAccountPasswordLoginParam.setAccount(authThirdBindAccountParam.getAccount()); + authAccountPasswordLoginParam.setPassword(authThirdBindAccountParam.getPassword()); + authAccountPasswordLoginParam.setValidCode(authThirdBindAccountParam.getValidCode()); + authAccountPasswordLoginParam.setValidCodeReqNo(authThirdBindAccountParam.getValidCodeReqNo()); + String token = authService.doLogin(authAccountPasswordLoginParam, SaClientTypeEnum.B.getValue()); + String userId = StpUtil.getLoginIdAsString(); + authThirdUser.setUserId(userId); + this.updateById(authThirdUser); + return token; + } + @Override public Page page(AuthThirdUserPageParam authThirdUserPageParam) { QueryWrapper queryWrapper = new QueryWrapper().checkSqlInjection(); @@ -165,20 +210,15 @@ public class AuthThirdServiceImpl extends ServiceImpl getUserListByIdListWithoutException(List userIdList) { + return clientUserService.listByIds(userIdList).stream().map(JSONUtil::parseObj).collect(Collectors.toList()); + } + + @Override + public JSONObject getUserByIdWithException(String userId) { + return JSONUtil.parseObj(clientUserService.queryEntity(userId)); + } + + @Override + public List getUserListByIdWithException(List userIdList) { + HashSet userIdSet = CollectionUtil.newHashSet(userIdList); + List clientUserList = clientUserService.listByIds(userIdSet); + if(clientUserList.size() != userIdSet.size()) { + throw new CommonException("某用户不存在,id值集合为:{}", userIdSet); + } + return clientUserList.stream().map(JSONUtil::parseObj).collect(Collectors.toList()); + } + + @Override + public List listUserWithoutCurrent() { + return clientUserService.list(new LambdaQueryWrapper() + .select(ClientUser::getId, ClientUser::getAccount, ClientUser::getName, ClientUser::getAvatar) + .ne(ClientUser::getId, StpUtil.getLoginId())) + .stream().map(JSONUtil::parseObj).collect(Collectors.toList()); + } + + @Override + public JSONObject getOrCreateClientUserExt(String userId) { + return JSONUtil.parseObj(clientUserService.getOrCreateClientUserExt(userId)); + } +} diff --git a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserExtService.java b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserExtService.java index 044f5c34..6dd66517 100644 --- a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserExtService.java +++ b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserExtService.java @@ -37,5 +37,5 @@ public interface ClientUserExtService extends IService { * @author xuyuxiang * @date 2022/4/27 21:38 */ - void createExtInfo(String userId, String sourceFromType); + ClientUserExt createExtInfo(String userId, String sourceFromType); } diff --git a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserService.java b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserService.java index 66022b3d..2cca250a 100644 --- a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserService.java +++ b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/ClientUserService.java @@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import org.springframework.web.multipart.MultipartFile; import vip.xiaonuo.client.modular.user.entity.ClientUser; +import vip.xiaonuo.client.modular.user.entity.ClientUserExt; import vip.xiaonuo.client.modular.user.param.*; import vip.xiaonuo.client.modular.user.result.ClientLoginUser; import vip.xiaonuo.client.modular.user.result.ClientUserPicValidCodeResult; @@ -341,4 +342,12 @@ public interface ClientUserService extends IService { * @date 2022/8/25 15:16 **/ void doRegister(String account, String password); + + /** + * 获取用户扩展信息,没有则创建 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + ClientUserExt getOrCreateClientUserExt(String userId); } diff --git a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/impl/ClientUserExtServiceImpl.java b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/impl/ClientUserExtServiceImpl.java index 16745d30..08a14d20 100644 --- a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/impl/ClientUserExtServiceImpl.java +++ b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/service/impl/ClientUserExtServiceImpl.java @@ -17,10 +17,13 @@ import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; +import vip.xiaonuo.client.core.enums.ClientYesOrNoEnum; import vip.xiaonuo.client.modular.user.entity.ClientUserExt; import vip.xiaonuo.client.modular.user.enums.ClientUserSourceFromTypeEnum; import vip.xiaonuo.client.modular.user.mapper.ClientUserExtMapper; import vip.xiaonuo.client.modular.user.service.ClientUserExtService; +import vip.xiaonuo.common.util.CommonCryptogramUtil; +import vip.xiaonuo.common.util.CommonOtpUtil; /** * C端用户扩展Service接口实现类 @@ -47,11 +50,15 @@ public class ClientUserExtServiceImpl extends ServiceImpl().eq(ClientUserExt::getUserId, userId)); + if(ObjectUtil.isEmpty(clientUserExt)){ + clientUserExt = clientUserExtService.createExtInfo(userId, ClientUserSourceFromTypeEnum.SYSTEM_ADD.getValue()); + } else { + if(ObjectUtil.isEmpty(clientUserExt.getOtpSecretKey())){ + String otpSecretKeyEncrypt = CommonCryptogramUtil.doSm4CbcEncrypt(CommonOtpUtil.generateSecretKey()); + clientUserExt.setOtpSecretKey(otpSecretKeyEncrypt); + clientUserExt.setHasBindOtp(ClientYesOrNoEnum.NO.getValue()); + clientUserExtService.updateById(clientUserExt); + } + } + return clientUserExt; + } + /** * 获取验证码失效时间(单位:秒) * diff --git a/snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/config/service/impl/DevConfigServiceImpl.java b/snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/config/service/impl/DevConfigServiceImpl.java index 29266646..ace868b3 100644 --- a/snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/config/service/impl/DevConfigServiceImpl.java +++ b/snowy-plugin/snowy-plugin-dev/src/main/java/vip/xiaonuo/dev/modular/config/service/impl/DevConfigServiceImpl.java @@ -70,6 +70,12 @@ public class DevConfigServiceImpl extends ServiceImpl getUpdatePasswordValidConfig() { return CommonResult.data(sysUserService.getUpdatePasswordValidConfig()); } + + /** + * 获取绑定动态口令状态 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + @ApiOperationSupport(order = 38) + @Operation(summary = "获取绑定动态口令状态") + @GetMapping("/sys/userCenter/getOtpInfoBindStatus") + public CommonResult getOtpInfoBindStatus() { + return CommonResult.data(sysUserService.getOtpInfoBindStatus()); + } + + /** + * 获取动态口令信息 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + @ApiOperationSupport(order = 39) + @Operation(summary = "获取动态口令信息") + @GetMapping("/sys/userCenter/getOtpInfo") + public CommonResult getOtpInfo() { + return CommonResult.data(sysUserService.getOtpInfo()); + } + + /** + * 绑定动态口令 + * + * @author xuyuxiang + * @date 2021/10/13 14:01 + **/ + @ApiOperationSupport(order = 40) + @Operation(summary = "绑定动态口令") + @CommonLog("绑定动态口令") + @PostMapping("/sys/userCenter/bindOtp") + public CommonResult bindOtp(@RequestBody @Valid SysUserOtpParam sysUserOtpParam) { + sysUserService.bindOtp(sysUserOtpParam); + return CommonResult.ok(); + } + + /** + * 解绑动态口令 + * + * @author xuyuxiang + * @date 2021/10/13 14:01 + **/ + @ApiOperationSupport(order = 41) + @Operation(summary = "解绑动态口令") + @CommonLog("解绑动态口令") + @PostMapping("/sys/userCenter/unBindOtp") + public CommonResult unBindOtp(@RequestBody @Valid SysUserOtpParam sysUserOtpParam) { + sysUserService.unBindOtp(sysUserOtpParam); + return CommonResult.ok(); + } } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserExt.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserExt.java index 667da98e..79e1de2d 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserExt.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/entity/SysUserExt.java @@ -49,4 +49,20 @@ public class SysUserExt extends CommonEntity { @Schema(description = "密码修改日期") private Date passwordUpdateTime; + /** 身份源ID */ + @Schema(description = "身份源ID") + private String idSourceId; + + /** 身份源用户ID */ + @Schema(description = "身份源用户ID") + private String idSourceUserId; + + /** OTP密钥 */ + @Schema(description = "OTP密钥") + private String otpSecretKey; + + /** OTP绑定状态 */ + @Schema(description = "OTP绑定状态") + private String hasBindOtp; + } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/param/SysUserOtpParam.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/param/SysUserOtpParam.java new file mode 100644 index 00000000..7c3fae2b --- /dev/null +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/param/SysUserOtpParam.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.sys.modular.user.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +/** + * 绑定/解绑动态口令参数 + * + * @author xuyuxiang + * @date 2022/7/26 16:04 + **/ +@Getter +@Setter +public class SysUserOtpParam { + + /** 动态口令 */ + @Schema(description = "动态口令") + @NotBlank(message = "otpCode不能为空") + private String otpCode; +} diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysUserApiProvider.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysUserApiProvider.java index 83f5b57f..ce17280d 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysUserApiProvider.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysUserApiProvider.java @@ -253,4 +253,9 @@ public class SysUserApiProvider implements SysUserApi { return obj; }).collect(Collectors.toList()); } + + @Override + public JSONObject getOrCreateSysUserExt(String userId) { + return JSONUtil.parseObj(sysUserService.getOrCreateSysUserExt(userId)); + } } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/result/SysUserOtpInfoResult.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/result/SysUserOtpInfoResult.java new file mode 100644 index 00000000..ebab264a --- /dev/null +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/result/SysUserOtpInfoResult.java @@ -0,0 +1,74 @@ +/* + * 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.sys.modular.user.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +/** + * 动口令信息结果 + * + * @author xuyuxiang + * @date 2022/7/8 9:28 + **/ +@Getter +@Setter +@Builder +public class SysUserOtpInfoResult { + + /** 动态口令信息,Base64 */ + @Schema(description = "动态口令信息,Base64") + private String otpInfoBase64; + + /** 动态口令信息,JSON */ + @Schema(description = "动态口令信息,JSON") + private OtpInfo otpInfo; + + /** + * 动态口令信息类 + * + * @author xuyuxiang + * @date 2022/4/28 23:19 + */ + @Getter + @Setter + @Builder + public static class OtpInfo { + + /** 发行者 */ + @Schema(description = "发行者") + private String issuer; + + /** 账号 */ + @Schema(description = "账号") + private String account; + + /** 密钥 */ + @Schema(description = "密钥") + private String secretKey; + + /** 算法 */ + @Schema(description = "算法") + private String algorithm; + + /** 位数 */ + @Schema(description = "位数") + private String digits; + + /** 周期 */ + @Schema(description = "周期") + private String period; + } +} diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserExtService.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserExtService.java index 27ac58aa..ed0184a1 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserExtService.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserExtService.java @@ -37,5 +37,5 @@ public interface SysUserExtService extends IService { * @author xuyuxiang * @date 2022/4/27 21:38 */ - void createExtInfo(String userId, String sourceFromType); + SysUserExt createExtInfo(String userId, String sourceFromType); } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserService.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserService.java index 9ea8a15c..3b87cd97 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserService.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/SysUserService.java @@ -23,6 +23,7 @@ import vip.xiaonuo.sys.modular.org.entity.SysOrg; import vip.xiaonuo.sys.modular.position.entity.SysPosition; import vip.xiaonuo.sys.modular.role.entity.SysRole; import vip.xiaonuo.sys.modular.user.entity.SysUser; +import vip.xiaonuo.sys.modular.user.entity.SysUserExt; import vip.xiaonuo.sys.modular.user.param.*; import vip.xiaonuo.sys.modular.user.result.*; @@ -646,4 +647,44 @@ public interface SysUserService extends IService { * @date 2022/8/25 15:16 **/ JSONObject getUpdatePasswordValidConfig(); + + /** + * 获取用户扩展信息,没有则创建 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + SysUserExt getOrCreateSysUserExt(String userId); + + /** + * 获取绑定动态口令状态 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + Boolean getOtpInfoBindStatus(); + + /** + * 获取动态口令信息 + * + * @author xuyuxiang + * @date 2022/7/8 9:26 + **/ + SysUserOtpInfoResult getOtpInfo(); + + /** + * 绑定动态口令 + * + * @author xuyuxiang + * @date 2021/10/13 14:01 + **/ + void bindOtp(SysUserOtpParam sysUserOtpParam); + + /** + * 解绑动态口令 + * + * @author xuyuxiang + * @date 2021/10/13 14:01 + **/ + void unBindOtp(SysUserOtpParam sysUserOtpParam); } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserExtServiceImpl.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserExtServiceImpl.java index 4f223d6e..b1e6dd3b 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserExtServiceImpl.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserExtServiceImpl.java @@ -17,6 +17,9 @@ import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; +import vip.xiaonuo.common.util.CommonCryptogramUtil; +import vip.xiaonuo.common.util.CommonOtpUtil; +import vip.xiaonuo.sys.core.enums.SysYesOrNoEnum; import vip.xiaonuo.sys.modular.user.entity.SysUserExt; import vip.xiaonuo.sys.modular.user.enums.SysUserSourceFromTypeEnum; import vip.xiaonuo.sys.modular.user.mapper.SysUserExtMapper; @@ -47,11 +50,15 @@ public class SysUserExtServiceImpl extends ServiceImpl impl /** 工作台默认快捷方式 */ private static final String SNOWY_SYS_DEFAULT_WORKBENCH_DATA_KEY = "SNOWY_SYS_DEFAULT_WORKBENCH_DATA"; + /** 系统名称 */ + private static final String SNOWY_SYS_NAME_KEY = "SNOWY_SYS_NAME"; + /** 验证码缓存前缀 */ private static final String USER_VALID_CODE_CACHE_KEY = "user-validCode:"; @@ -2401,6 +2407,84 @@ public class SysUserServiceImpl extends ServiceImpl impl return SysPasswordUtl.getUpdatePasswordValidConfig(); } + @Override + public SysUserExt getOrCreateSysUserExt(String userId) { + SysUserExt sysUserExt = sysUserExtService.getOne(new LambdaQueryWrapper().eq(SysUserExt::getUserId, userId)); + if(ObjectUtil.isEmpty(sysUserExt)){ + sysUserExt = sysUserExtService.createExtInfo(userId, SysUserSourceFromTypeEnum.SYSTEM_ADD.getValue()); + } else { + if(ObjectUtil.isEmpty(sysUserExt.getOtpSecretKey())){ + String otpSecretKeyEncrypt = CommonCryptogramUtil.doSm4CbcEncrypt(CommonOtpUtil.generateSecretKey()); + sysUserExt.setOtpSecretKey(otpSecretKeyEncrypt); + sysUserExt.setHasBindOtp(SysYesOrNoEnum.NO.getValue()); + sysUserExtService.updateById(sysUserExt); + } + } + return sysUserExt; + } + + @Override + public Boolean getOtpInfoBindStatus() { + String loginIdAsString = StpUtil.getLoginIdAsString(); + SysUserExt sysUserExt = this.getOrCreateSysUserExt(loginIdAsString); + return sysUserExt.getHasBindOtp().equals(SysYesOrNoEnum.YES.getValue()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SysUserOtpInfoResult getOtpInfo() { + String loginIdAsString = StpUtil.getLoginIdAsString(); + SysUser sysUser = this.queryEntity(loginIdAsString); + SysUserExt sysUserExt = this.getOrCreateSysUserExt(loginIdAsString); + String otpSecretKey = CommonCryptogramUtil.doSm4CbcDecrypt(sysUserExt.getOtpSecretKey()); + String account = sysUser.getAccount(); + String issuer = devConfigApi.getValueByKey(SNOWY_SYS_NAME_KEY); + String uri = CommonOtpUtil.getTotUri(otpSecretKey, issuer, account); + String qrCodeBase64 = QrCodeUtil.generateAsBase64(uri, new QrConfig(200, 200), ImgUtil.IMAGE_TYPE_PNG); + return SysUserOtpInfoResult.builder().otpInfoBase64(qrCodeBase64) + .otpInfo(SysUserOtpInfoResult.OtpInfo.builder() + .issuer(issuer) + .account(account) + .secretKey(otpSecretKey) + .algorithm("HmacSHA1") + .digits("6位") + .period("30秒") + .build()).build(); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void bindOtp(SysUserOtpParam sysUserOtpParam) { + doCheckAndUpdate(sysUserOtpParam, SysYesOrNoEnum.YES.getValue()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void unBindOtp(SysUserOtpParam sysUserOtpParam) { + doCheckAndUpdate(sysUserOtpParam, SysYesOrNoEnum.NO.getValue()); + } + + public void doCheckAndUpdate(SysUserOtpParam sysUserOtpParam, String binOtpStatus) { + String otpCode = sysUserOtpParam.getOtpCode(); + String loginIdAsString = StpUtil.getLoginIdAsString(); + SysUserExt sysUserExt = this.getOrCreateSysUserExt(loginIdAsString); + if(binOtpStatus.equals(SysYesOrNoEnum.YES.getValue()) && sysUserExt.getHasBindOtp().equals(SysYesOrNoEnum.YES.getValue())){ + throw new CommonException("该账户已绑定动态口令,不可重复绑定"); + } + if(binOtpStatus.equals(SysYesOrNoEnum.NO.getValue()) && sysUserExt.getHasBindOtp().equals(SysYesOrNoEnum.NO.getValue())){ + throw new CommonException("该账户未绑定动态口令,无需解绑"); + } + // 解密密钥 + String otpSecretKey = CommonCryptogramUtil.doSm4CbcDecrypt(sysUserExt.getOtpSecretKey()); + // 校验动态口令 + boolean isValid = CommonOtpUtil.validateCode(otpSecretKey, otpCode, 1); + if(!isValid){ + throw new CommonException("动态口令错误"); + } + sysUserExt.setHasBindOtp(binOtpStatus); + sysUserExtService.updateById(sysUserExt); + } + /** * 获取验证码失效时间(单位:秒) * diff --git a/snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java b/snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java index d1bd29ea..fe583351 100644 --- a/snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java +++ b/snowy-web-app/src/main/java/vip/xiaonuo/core/config/GlobalConfigure.java @@ -132,6 +132,8 @@ public class GlobalConfigure implements WebMvcConfigurer { "/auth/c/register", "/auth/c/getEmailValidCode", "/auth/c/doLoginByEmail", + "/auth/c/doLoginByOtp", + "/auth/c/isLogin", "/auth/b/getPicCaptcha", "/auth/b/getPhoneValidCode", @@ -140,10 +142,14 @@ public class GlobalConfigure implements WebMvcConfigurer { "/auth/b/register", "/auth/b/getEmailValidCode", "/auth/b/doLoginByEmail", + "/auth/b/doLoginByOtp", + "/auth/b/isLogin", + "/auth/sso/b/**", /* 三方登录相关 */ "/auth/third/render", "/auth/third/callback", + "/auth/third/bindAccount", /* 系统基础配置 */ "/dev/config/sysBaseList", @@ -178,6 +184,13 @@ public class GlobalConfigure implements WebMvcConfigurer { "/wiki/wikidocumentshare/getInfoByCode", "/wiki/wikidocument/getInfoById", "/wiki/wikidocumentfile/pdfProxy", + + /* 统一认证插件放行 */ + "/iam/auth/login/**", + "/iam/auth/protocol/**", + "/iam/auth/source/render", + "/iam/auth/source/callback/**", + "/iam/id/source/eventCallback/**", }; /** diff --git a/snowy-web-app/src/main/resources/_sql/snowy_mysql.sql b/snowy-web-app/src/main/resources/_sql/snowy_mysql.sql index 859f8ff8..45b314a4 100644 --- a/snowy-web-app/src/main/resources/_sql/snowy_mysql.sql +++ b/snowy-web-app/src/main/resources/_sql/snowy_mysql.sql @@ -138,6 +138,8 @@ CREATE TABLE `CLIENT_USER_EXT` ( `USER_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id', `SOURCE_FROM_TYPE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '来源类别', `PASSWORD_UPDATE_TIME` datetime NULL DEFAULT NULL COMMENT '密码修改日期', + `OTP_SECRET_KEY` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OTP密钥', + `HAS_BIND_OTP` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OTP绑定状态', `DELETE_FLAG` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '删除标志', `CREATE_TIME` datetime NULL DEFAULT NULL COMMENT '创建时间', `CREATE_USER` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户', @@ -337,7 +339,8 @@ INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755288', 'SNOWY_SYS_DEFAULT_PASSW INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755289', 'SNOWY_SYS_DEFAULT_PASSWORD_DEFINE_WEAK_DATABASE_FOR_C', 'xiaonuo,xiaonuoark', 'PASSWORD_STRATEGY_FOR_C', 'C端密码自定义额外弱密码库', 172, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL); INSERT INTO `DEV_CONFIG` VALUES ('1908870094824755290', 'SNOWY_SYS_DEFAULT_PASSWORD_EXPIRED_DAYS_FOR_C', '30', 'PASSWORD_STRATEGY_FOR_C', 'C端密码有效期天数', 173, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL); 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); -- ---------------------------- -- Table structure for DEV_DICT -- ---------------------------- @@ -977,6 +980,8 @@ CREATE TABLE `SYS_ORG_EXT` ( `ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `ORG_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组织id', `SOURCE_FROM_TYPE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '来源类别', + `ID_SOURCE_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '身份源ID', + `ID_SOURCE_ORG_ID` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '身份源机构ID', `DELETE_FLAG` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '删除标志', `CREATE_TIME` datetime NULL DEFAULT NULL COMMENT '创建时间', `CREATE_USER` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户', @@ -1413,6 +1418,10 @@ CREATE TABLE `SYS_USER_EXT` ( `USER_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id', `SOURCE_FROM_TYPE` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '来源类别', `PASSWORD_UPDATE_TIME` datetime NULL DEFAULT NULL COMMENT '密码修改日期', + `ID_SOURCE_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '身份源ID', + `ID_SOURCE_USER_ID` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '身份源用户ID', + `OTP_SECRET_KEY` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OTP密钥', + `HAS_BIND_OTP` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OTP绑定状态', `DELETE_FLAG` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '删除标志', `CREATE_TIME` datetime NULL DEFAULT NULL COMMENT '创建时间', `CREATE_USER` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户', diff --git a/snowy-web-app/src/main/resources/application.properties b/snowy-web-app/src/main/resources/application.properties index dd8324f9..28790a8d 100644 --- a/snowy-web-app/src/main/resources/application.properties +++ b/snowy-web-app/src/main/resources/application.properties @@ -179,10 +179,16 @@ springdoc.group-configs[6].packages-to-scan=vip.xiaonuo.sys # snowy configuration ######################################### # common configuration +snowy.config.common.front-url=http://localhost:81 snowy.config.common.backend-url=http://localhost:82 # plugin dev-sms configuration sms-oa.config-type=yaml sms-oa.core-pool-size=20 sms-oa.queue-capacity=20 sms-oa.max-pool-size=20 - +# sso configuration +sa-token.sso-client.client= +sa-token.sso-client.auth-url= +sa-token.sso-client.signout-url= +sa-token.sso-client.push-url= +sa-token.sso-client.secret-key=
{{ tipText }}
1.打开Google Authenticator或者Okta Verify等动态口令应用。
2.点击“扫一扫”或者“手动输入”,将动态口令应用中的二维码扫描到应用中。
3.在下方输入框中输入动态口令,即可完成绑定/解绑。