mirror of
https://gitee.com/xiaonuobase/snowy.git
synced 2025-12-25 23:56:20 +08:00
【更新】底座增加动态口令登录,完善单点登录客户端用于未来无缝接入统一认证平台,优化诸多代码,更新sql
This commit is contained in:
parent
8111719330
commit
f4d875ae3c
28
pom.xml
28
pom.xml
@ -252,6 +252,27 @@
|
||||
<version>0.16.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- zxing -->
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- bouncycastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.70</version>
|
||||
</dependency>
|
||||
|
||||
<!-- bouncycastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
<version>1.70</version>
|
||||
</dependency>
|
||||
|
||||
<!-- sa-token-core -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
@ -287,6 +308,13 @@
|
||||
<version>1.44.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 插件:整合 Forest 请求工具 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-forest</artifactId>
|
||||
<version>1.44.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JustAuth 第三方登录 -->
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -29,5 +29,9 @@ export default {
|
||||
// 第三方登录授权回调
|
||||
thirdCallback(data) {
|
||||
return request('callback', data, 'get')
|
||||
},
|
||||
// 第三方登录绑定账号
|
||||
thirdBindAccount(data) {
|
||||
return request('bindAccount', data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@ -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?'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: '确定要重置吗?'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -21,13 +21,68 @@
|
||||
<div class="login-form">
|
||||
<a-card>
|
||||
<div class="login-header">
|
||||
<h2>三方登录</h2>
|
||||
<h2>{{tipText}}</h2>
|
||||
</div>
|
||||
<a-spin tip="正在登录中...">
|
||||
<a-spin :tip="tipText" v-if="showLoading">
|
||||
<div class="h-[300px]">
|
||||
<a-skeleton />
|
||||
</div>
|
||||
</a-spin>
|
||||
<a-empty :description="tipText" v-if="!showLoading && !showBind"></a-empty>
|
||||
<a-form ref="loginForm" :model="ruleForm" :rules="rules" v-if="showBind">
|
||||
<a-form-item name="account">
|
||||
<a-input
|
||||
v-model:value="ruleForm.account"
|
||||
:placeholder="$t('login.accountPlaceholder')"
|
||||
size="large"
|
||||
@keyup.enter="bindAccount"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="ruleForm.password"
|
||||
:placeholder="$t('login.PWPlaceholder')"
|
||||
size="large"
|
||||
autocomplete="off"
|
||||
@keyup.enter="bindAccount"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item name="validCode" v-if="captchaOpen === 'true'">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="ruleForm.validCode"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
@keyup.enter="bindAccount"
|
||||
>
|
||||
<template #prefix>
|
||||
<verified-outlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="7">
|
||||
<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a href="/login" class="xn-color-0d84ff">{{ $t('login.signIn') }}</a>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" class="w-full" :loading="loading" round size="large" @click="bindAccount"
|
||||
>{{ $t('login.bindAccount') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,20 +91,53 @@
|
||||
|
||||
<script setup name="loginCallback">
|
||||
import { message } from 'ant-design-vue'
|
||||
import tool from '@/utils/tool'
|
||||
import router from '@/router'
|
||||
import thirdApi from '@/api/auth/thirdApi'
|
||||
import loginApi from '@/api/auth/loginApi'
|
||||
import userCenterApi from '@/api/sys/userCenterApi'
|
||||
import dictApi from '@/api/dev/dictApi'
|
||||
import { globalStore } from '@/store'
|
||||
|
||||
import {afterLogin} from "@/views/auth/login/util";
|
||||
import router from '@/router'
|
||||
import {required} from "@/utils/formRules";
|
||||
import tool from "@/utils/tool";
|
||||
import loginApi from "@/api/auth/loginApi";
|
||||
import smCrypto from '@/utils/smCrypto'
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = router.currentRoute.value
|
||||
const showLoading = ref(true)
|
||||
const showBind = ref(false)
|
||||
const tipText = ref(proxy.$t('login.thirdLogin'))
|
||||
const thirdId = ref(null)
|
||||
const store = globalStore()
|
||||
const sysBaseConfig = computed(() => {
|
||||
return store.sysBaseConfig
|
||||
return tool.data.get('SNOWY_SYS_BASE_CONFIG') || store.sysBaseConfig
|
||||
})
|
||||
const captchaOpen = ref(sysBaseConfig.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B)
|
||||
|
||||
const loading = ref(false)
|
||||
const validCodeBase64 = ref('')
|
||||
|
||||
const ruleForm = reactive({
|
||||
validCode: '',
|
||||
validCodeReqNo: '',
|
||||
autologin: false
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
account: [required(proxy.$t('login.accountError'), 'blur')],
|
||||
password: [required(proxy.$t('login.PWError'), 'blur')]
|
||||
})
|
||||
|
||||
const showError = (msg, alert) => {
|
||||
if(alert) {
|
||||
message.error(msg)
|
||||
}
|
||||
tipText.value = msg
|
||||
showLoading.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if(!route.params.platform) {
|
||||
showError(proxy.$t('login.paramError'), true)
|
||||
return
|
||||
}
|
||||
// 获取当前url
|
||||
const url = new URL(window.location.href)
|
||||
let argLength = 0
|
||||
@ -60,37 +148,71 @@
|
||||
})
|
||||
// 当然了,不可能只有一个参数
|
||||
if (argLength < 2) {
|
||||
window.location.href = '/login'
|
||||
showError(proxy.$t('login.paramError'), true)
|
||||
return
|
||||
}
|
||||
|
||||
// 平台
|
||||
params.platform = route.params.platform
|
||||
thirdApi
|
||||
.thirdCallback(params)
|
||||
.then((data) => {
|
||||
tool.data.set('TOKEN', data)
|
||||
// 获取登录的用户信息
|
||||
loginApi.getLoginUser().then((loginUser) => {
|
||||
tool.data.set('USER_INFO', loginUser)
|
||||
})
|
||||
userCenterApi.userLoginMenu().then((menu) => {
|
||||
const indexMenu = menu[0].children[0].path
|
||||
tool.data.set('MENU', menu)
|
||||
// 重置系统默认应用
|
||||
tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
|
||||
router.replace({
|
||||
path: indexMenu
|
||||
})
|
||||
message.success('登录成功')
|
||||
dictApi.dictTree().then((dictData) => {
|
||||
// 设置字典到store中
|
||||
tool.data.set('DICT_TYPE_TREE_DATA', dictData)
|
||||
})
|
||||
})
|
||||
.then(async (data) => {
|
||||
if (data.startsWith('needBind')) {
|
||||
showError(proxy.$t('login.bindAccount'), true)
|
||||
thirdId.value = data.split(":")[1];
|
||||
showBind.value = true
|
||||
refreshSwitch()
|
||||
} else {
|
||||
await afterLogin(data)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = '/login'
|
||||
.catch((err) => {
|
||||
showError(proxy.$t('login.thirdLoginError'), false)
|
||||
console.log(err)
|
||||
})
|
||||
})
|
||||
// 通过开关加载内容
|
||||
const refreshSwitch = () => {
|
||||
// 判断是否开启验证码
|
||||
if (captchaOpen.value === 'true') {
|
||||
// 加载验证码
|
||||
loginCaptcha()
|
||||
// 加入校验
|
||||
rules.validCode = [required(proxy.$t('login.validError'), 'blur')]
|
||||
}
|
||||
}
|
||||
// 获取验证码
|
||||
const loginCaptcha = () => {
|
||||
loginApi.getPicCaptcha().then((data) => {
|
||||
validCodeBase64.value = data.validCodeBase64
|
||||
ruleForm.validCodeReqNo = data.validCodeReqNo
|
||||
})
|
||||
}
|
||||
// 绑定账号
|
||||
const loginForm = ref()
|
||||
const bindAccount = async () => {
|
||||
loginForm.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
loading.value = true
|
||||
const loginData = {
|
||||
thirdId: thirdId.value,
|
||||
account: ruleForm.account,
|
||||
// 密码进行SM2加密,传输过程中看到的只有密文,后端存储使用hash
|
||||
password: smCrypto.doSm2Encrypt(ruleForm.password),
|
||||
validCode: ruleForm.validCode,
|
||||
validCodeReqNo: ruleForm.validCodeReqNo
|
||||
}
|
||||
const loginToken = await thirdApi.thirdBindAccount(loginData)
|
||||
await afterLogin(loginToken)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
loading.value = false
|
||||
if (captchaOpen.value === 'true') {
|
||||
loginCaptcha()
|
||||
}}
|
||||
)
|
||||
}
|
||||
// logo链接
|
||||
const handleLink = (e) => {
|
||||
if (!sysBaseConfig.value.SNOWY_SYS_COPYRIGHT_URL) {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<a-form-item name="emailValidCode">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="16">
|
||||
<a-input v-model:value="emailFormData.emailValidCode" :placeholder="$t('login.validError')" size="large">
|
||||
<a-input v-model:value="emailFormData.emailValidCode" :placeholder="$t('login.emailCodePlaceholder')" size="large">
|
||||
<template #prefix>
|
||||
<mail-outlined class="text-black text-opacity-25" />
|
||||
</template>
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
target="_blank"
|
||||
@click="handleLink"
|
||||
>
|
||||
<img :alt="sysBaseConfig.SNOWY_SYS_NAME" :src="sysBaseConfig.SNOWY_SYS_LOGO" />
|
||||
<label>{{ sysBaseConfig.SNOWY_SYS_NAME }}</label>
|
||||
<img :alt="systemName" :src="sysBaseConfig.SNOWY_SYS_LOGO" />
|
||||
<label>{{ systemName }}</label>
|
||||
</a>
|
||||
</div>
|
||||
<div class="version">
|
||||
@ -74,7 +74,7 @@
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="ruleForm.validCode"
|
||||
:placeholder="$t('login.validLaceholder')"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
@keyup.enter="login"
|
||||
>
|
||||
@ -104,17 +104,26 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="userSms" :tab="$t('login.phoneSms')" force-render v-if="phoneLogin === 'true'">
|
||||
<a-tab-pane key="userSms" :tab="$t('login.phoneLogin')" force-render v-if="loginTypes.phoneLogin === 'true'">
|
||||
<phone-login-form />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="userEmail" :tab="$t('login.emailLogin')" force-render v-if="emailLogin === 'true'">
|
||||
<a-tab-pane key="userEmail" :tab="$t('login.emailLogin')" force-render v-if="loginTypes.emailLogin === 'true'">
|
||||
<email-login-form />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="userOtp" :tab="$t('login.otpLogin')" force-render v-if="loginTypes.otpLogin === 'true'">
|
||||
<otp-login-form :captchaOpen="captchaOpen" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div v-if="configData.FRONT_BACK_LOGIN_URL_SHOW">
|
||||
<a href="/front/client/index" class="xn-color-0d84ff">前台登录</a>
|
||||
<a href="/front/client/index" class="xn-color-0d84ff">{{ $t('login.frontLogin') }}</a>
|
||||
</div>
|
||||
<three-login v-if="configData.THREE_LOGIN_SHOW" />
|
||||
<three-login v-if="configData.THREE_LOGIN_SHOW && !appId" />
|
||||
<three-login-for-app ref="threeLoginForAppRef"
|
||||
v-if="configData.THREE_LOGIN_SHOW && appId"
|
||||
:appId="appId"
|
||||
:loginTypes="loginTypes"
|
||||
@updateLoginTypes="updateLoginTypes"
|
||||
@updateSystemName="updateSystemName"/>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
@ -124,7 +133,9 @@
|
||||
import loginApi from '@/api/auth/loginApi'
|
||||
const PhoneLoginForm = defineAsyncComponent(() => import('./phoneLoginForm.vue'))
|
||||
const EmailLoginForm = defineAsyncComponent(() => import('./emailLoginForm.vue'))
|
||||
const OtpLoginForm = defineAsyncComponent(() => import('./otpLoginForm.vue'))
|
||||
import ThreeLogin from './threeLogin.vue'
|
||||
import ThreeLoginForApp from './threeLoginForApp.vue'
|
||||
import smCrypto from '@/utils/smCrypto'
|
||||
import { required } from '@/utils/formRules'
|
||||
import { afterLogin } from './util'
|
||||
@ -132,13 +143,28 @@
|
||||
import configApi from '@/api/dev/configApi'
|
||||
import tool from '@/utils/tool'
|
||||
import { globalStore, iframeStore, keepAliveStore, viewTagsStore } from '@/store'
|
||||
import router from '@/router'
|
||||
const route = router.currentRoute.value
|
||||
const appId = computed(() => {
|
||||
return route.query.appId
|
||||
})
|
||||
const threeLoginForAppRef = ref(null)
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const activeKey = ref('userAccount')
|
||||
|
||||
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B)
|
||||
const registerOpen = ref('false')
|
||||
const phoneLogin = ref('false')
|
||||
const emailLogin = ref('false')
|
||||
const loginTypes = reactive({
|
||||
phoneLogin: 'false',
|
||||
emailLogin: 'false',
|
||||
otpLogin: 'false'
|
||||
})
|
||||
const updateLoginTypes = (newTypes) => {
|
||||
Object.assign(loginTypes, newTypes)
|
||||
}
|
||||
const updateSystemName = (newSystemName) => {
|
||||
systemName.value = newSystemName
|
||||
}
|
||||
const validCodeBase64 = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
@ -185,7 +211,7 @@
|
||||
const sysBaseConfig = computed(() => {
|
||||
return store.sysBaseConfig
|
||||
})
|
||||
|
||||
const systemName = ref(sysBaseConfig.value.SNOWY_SYS_NAME)
|
||||
onMounted(() => {
|
||||
let formData = ref(configData.SYS_BASE_CONFIG)
|
||||
configApi
|
||||
@ -197,11 +223,15 @@
|
||||
})
|
||||
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B
|
||||
registerOpen.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B
|
||||
phoneLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B
|
||||
emailLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B
|
||||
loginTypes.phoneLogin = formData.value.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B
|
||||
loginTypes.emailLogin = formData.value.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B
|
||||
loginTypes.otpLogin = formData.value.SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B
|
||||
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
|
||||
setSysBaseConfig(formData.value)
|
||||
refreshSwitch()
|
||||
if (threeLoginForAppRef.value) {
|
||||
threeLoginForAppRef.value.init(appId)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
@ -245,7 +275,7 @@
|
||||
ruleForm.validCodeReqNo = data.validCodeReqNo
|
||||
})
|
||||
}
|
||||
//登陆
|
||||
// 登录
|
||||
const loginForm = ref()
|
||||
const login = async () => {
|
||||
loginForm.value
|
||||
@ -259,18 +289,16 @@
|
||||
validCode: ruleForm.validCode,
|
||||
validCodeReqNo: ruleForm.validCodeReqNo
|
||||
}
|
||||
// 获取token
|
||||
try {
|
||||
const loginToken = await loginApi.login(loginData)
|
||||
await afterLogin(loginToken)
|
||||
} catch (err) {
|
||||
loading.value = false
|
||||
if (captchaOpen.value === 'true') {
|
||||
loginCaptcha()
|
||||
}
|
||||
}
|
||||
const loginToken = await loginApi.login(loginData)
|
||||
await afterLogin(loginToken)
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
loading.value = false
|
||||
if (captchaOpen.value === 'true') {
|
||||
loginCaptcha()
|
||||
}}
|
||||
)
|
||||
}
|
||||
const configLang = (key) => {
|
||||
config.value.lang = key
|
||||
|
||||
115
snowy-admin-web/src/views/auth/login/otpLoginForm.vue
Normal file
115
snowy-admin-web/src/views/auth/login/otpLoginForm.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<a-form ref="otpLoginFormRef" :model="ruleForm" :rules="formRules">
|
||||
<a-form-item name="accountForOtp">
|
||||
<a-input v-model:value="ruleForm.accountForOtp" :placeholder="$t('login.accountPlaceholder')" size="large">
|
||||
<template #prefix>
|
||||
<user-outlined class="text-black text-opacity-25" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="otpCode">
|
||||
<a-input v-model:value="ruleForm.otpCode" :placeholder="$t('login.otpCodePlaceholder')" size="large">
|
||||
<template #prefix>
|
||||
<mail-outlined class="text-black text-opacity-25" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="validCodeForOtp" v-if="captchaOpen === 'true'">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="ruleForm.validCodeForOtp"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<verified-outlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="7">
|
||||
<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" class="xn-wd" :loading="loading" round size="large" @click="submitLogin">
|
||||
{{ $t('login.signIn') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup name="otpLoginForm">
|
||||
import loginApi from '@/api/auth/loginApi'
|
||||
import { afterLogin } from './util'
|
||||
import {required} from "@/utils/formRules";
|
||||
const { proxy } = getCurrentInstance()
|
||||
const props = defineProps({
|
||||
captchaOpen: {
|
||||
type: String,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const otpLoginFormRef = ref()
|
||||
const validCodeBase64 = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const ruleForm = reactive({
|
||||
accountForOtp: '',
|
||||
otpCode: '',
|
||||
validCodeForOtp: '',
|
||||
validCodeReqNo: ''
|
||||
})
|
||||
|
||||
const formRules = reactive({
|
||||
accountForOtp: [required(proxy.$t('login.accountError'), 'blur')],
|
||||
otpCode: [required(proxy.$t('login.otpCodePlaceholder'), 'blur')]
|
||||
})
|
||||
|
||||
// 通过开关加载内容
|
||||
const refreshSwitch = () => {
|
||||
// 判断是否开启验证码
|
||||
if (props.captchaOpen === 'true') {
|
||||
// 加载验证码
|
||||
loginCaptcha()
|
||||
// 加入校验
|
||||
formRules.validCodeForOtp = [required(proxy.$t('login.validError'), 'blur')]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const loginCaptcha = () => {
|
||||
loginApi.getPicCaptcha().then((data) => {
|
||||
validCodeBase64.value = data.validCodeBase64
|
||||
ruleForm.validCodeReqNo = data.validCodeReqNo
|
||||
})
|
||||
}
|
||||
|
||||
// 通过开关加载内容
|
||||
refreshSwitch()
|
||||
|
||||
// 点击登录按钮
|
||||
const submitLogin = async () => {
|
||||
const validate = await otpLoginFormRef.value.validate().catch(() => {})
|
||||
if (!validate) return false
|
||||
const loginData = {
|
||||
account: ruleForm.accountForOtp,
|
||||
otpCode: ruleForm.otpCode,
|
||||
validCode: ruleForm.validCodeForOtp,
|
||||
validCodeReqNo: ruleForm.validCodeReqNo
|
||||
}
|
||||
loading.value = true
|
||||
loginApi
|
||||
.loginByOtp(loginData)
|
||||
.then(async (loginToken) => {
|
||||
await afterLogin(loginToken)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
if (props.captchaOpen === 'true') {
|
||||
loginCaptcha()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -51,7 +51,7 @@
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="phoneFormModalData.validCode"
|
||||
:placeholder="$t('login.validLaceholder')"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
|
||||
@ -2,23 +2,32 @@
|
||||
<a-divider>{{ $t('login.signInOther') }}</a-divider>
|
||||
<div class="login-oauth layout-center">
|
||||
<a-space align="start">
|
||||
<a @click="getLoginRenderUrl('gitee')"><GiteeIcon /></a>
|
||||
<a-button type="primary" shape="circle">
|
||||
<wechat-filled />
|
||||
</a-button>
|
||||
<a @click="getLoginRenderUrl('gitee')">
|
||||
<GiteeIcon />
|
||||
</a>
|
||||
<a @click="getLoginRenderUrl('wechat')">
|
||||
<wechat-outlined class="bind-icon" :style="{ color: '#1AAD19' }" />
|
||||
</a>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="threeLogin">
|
||||
import thirdApi from '@/api/auth/thirdApi'
|
||||
import WechatOutlined from "@ant-design/icons-vue/WechatOutlined";
|
||||
|
||||
const getLoginRenderUrl = (platform) => {
|
||||
const param = {
|
||||
platform: platform
|
||||
platform: platform,
|
||||
clientType: 'B'
|
||||
}
|
||||
thirdApi.thirdRender(param).then((data) => {
|
||||
window.location.href = data.authorizeUrl
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.bind-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
66
snowy-admin-web/src/views/auth/login/threeLoginForApp.vue
Normal file
66
snowy-admin-web/src/views/auth/login/threeLoginForApp.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<a-divider>{{ $t('login.signInOther') }}</a-divider>
|
||||
<div class="login-oauth layout-center">
|
||||
<a-space align="start">
|
||||
<a @click="renderAuthSource(record)" v-for="record in appAuthSourceList" :key="record.authSourceId">
|
||||
<img :src="record.authSourceLogo" class="record-img"/>
|
||||
</a>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="threeLoginForApp">
|
||||
/*import authLoginApi from '@/api/iam/auth/authLoginApi'
|
||||
import authSourceApi from '@/api/iam/auth/authSourceApi'*/
|
||||
// 定义emit事件
|
||||
const emit = defineEmits({ updateLoginTypes: null, updateSystemName: null })
|
||||
|
||||
const props = defineProps({
|
||||
appId: {
|
||||
type: String,
|
||||
default: () => {}
|
||||
},
|
||||
loginTypes: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const appAuthSourceList = ref([])
|
||||
const init = () => {
|
||||
const param = {
|
||||
appId: props.appId
|
||||
}
|
||||
/*authLoginApi.getAppAuthSourceList(param).then((data) => {
|
||||
const appName = data.appName
|
||||
const authAppLinkResultList = data.authAppLinkResultList
|
||||
let phoneLogin = authAppLinkResultList.filter((item) => item.authSourceTemplateCode === 'PHONE').length > 0?'true':'false';
|
||||
let emailLogin = authAppLinkResultList.filter((item) => item.authSourceTemplateCode === 'EMAIL').length > 0?'true':'false';
|
||||
let otpLogin = authAppLinkResultList.filter((item) => item.authSourceTemplateCode === 'OTP').length > 0?'true':'false';
|
||||
appAuthSourceList.value = authAppLinkResultList.filter((item) => !item.isBuildIn);
|
||||
phoneLogin = props.loginTypes.phoneLogin === 'true' && phoneLogin === 'true'?'true':'false'
|
||||
emailLogin = props.loginTypes.emailLogin === 'true' && emailLogin === 'true'?'true':'false'
|
||||
otpLogin = props.loginTypes.otpLogin === 'true' && otpLogin === 'true'?'true':'false'
|
||||
emit('updateLoginTypes', { phoneLogin, emailLogin, otpLogin })
|
||||
emit('updateSystemName', appName)
|
||||
})*/
|
||||
}
|
||||
const renderAuthSource = (record) => {
|
||||
const param = {
|
||||
appId: props.appId,
|
||||
authSourceId: record.authSourceId,
|
||||
clientType: 'B'
|
||||
}
|
||||
/*authSourceApi.authSourceRender(param).then((data) => {
|
||||
window.location.href = data
|
||||
})*/
|
||||
}
|
||||
defineExpose({
|
||||
init
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.record-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
204
snowy-admin-web/src/views/auth/sso/index.vue
Normal file
204
snowy-admin-web/src/views/auth/sso/index.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<div class="content-box">
|
||||
<div class="content-form">
|
||||
<div class="content-header">
|
||||
<!-- 加载状态容器 -->
|
||||
<div class="loading-container" v-if="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">{{ tipText }}</p>
|
||||
</div>
|
||||
<!-- 错误状态容器 -->
|
||||
<div class="error-container" v-else>
|
||||
<p class="error-text">{{ tipText }}</p>
|
||||
<button class="retry-btn" @click="tryJump">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ssoLogin">
|
||||
import { useRoute } from "vue-router";
|
||||
import ssoApi from "@/api/auth/ssoApi";
|
||||
import { afterLogin } from "@/views/auth/login/util";
|
||||
import { ref, onMounted } from 'vue';
|
||||
import tool from "@/utils/tool";
|
||||
import loginApi from "@/api/auth/loginApi";
|
||||
|
||||
const route = useRoute();
|
||||
const tipText = ref('加载中...');
|
||||
const loading = ref(true); // 新增加载状态控制
|
||||
|
||||
// 从url中查询到指定名称的参数值
|
||||
const getParam = (name, defaultValue) => {
|
||||
const query = window.location.search.substring(1);
|
||||
const vars = query.split("&");
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split("=");
|
||||
if (pair[0] === name) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return defaultValue === undefined ? null : defaultValue;
|
||||
};
|
||||
|
||||
const ticket = getParam('ticket') || route.query.ticket;
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await tryJump();
|
||||
});
|
||||
|
||||
// 跳转
|
||||
const tryJump = async () => {
|
||||
// 重置加载状态
|
||||
loading.value = true;
|
||||
tipText.value = '加载中...';
|
||||
|
||||
try {
|
||||
let existToken = tool.data.get('TOKEN');
|
||||
if (existToken) {
|
||||
const isLogin = await loginApi.isLogin();
|
||||
if (isLogin) {
|
||||
await goHome(existToken);
|
||||
} else {
|
||||
await redirectSsoAuthUrl(window.location.href);
|
||||
}
|
||||
} else {
|
||||
if (ticket) {
|
||||
await doLoginByTicket(ticket);
|
||||
} else {
|
||||
await redirectSsoAuthUrl(window.location.href);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
loading.value = false;
|
||||
tipText.value = '处理失败,请重试';
|
||||
console.error('SSO登录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转首页
|
||||
const goHome = async (loginToken) => {
|
||||
tipText.value = '验证成功,即将跳转...';
|
||||
setTimeout(async () => {
|
||||
await afterLogin(loginToken);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 处理SSO登录回调
|
||||
const doLoginByTicket = async (ticket) => {
|
||||
const loginToken = await ssoApi.doLoginByTicket({ ticket: ticket });
|
||||
tipText.value = '验证成功,即将跳转...';
|
||||
setTimeout(async () => {
|
||||
await afterLogin(loginToken);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 重定向到SSO登录页
|
||||
const redirectSsoAuthUrl = async (redirectUrl) => {
|
||||
const authUrl = await ssoApi.getSsoAuthUrl({ redirectUrl: redirectUrl });
|
||||
tipText.value = '即将跳转至SSO登录页...';
|
||||
setTimeout(() => {
|
||||
window.location.href = authUrl;
|
||||
}, 500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0f5cb3 0%, #1677ff 50%, #4096ff 100%);
|
||||
box-shadow: inset 0 0 200px rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-form {
|
||||
width: 450px;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto 20px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@ -105,7 +105,7 @@
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a @click="formRef.onOpen(record)" v-if="hasPerm('bizUserEdit')">{{ $t('common.editButton') }}</a>
|
||||
<a-divider type="vertical" v-if="hasPerm(['bizUserEdit', 'bizUserDelete'], 'and')" />
|
||||
<a-popconfirm :title="$t('user.popconfirmDeleteUser')" @confirm="removeUser(record)">
|
||||
<a-popconfirm :title="$t('user.popConfirmDeleteUser')" @confirm="removeUser(record)">
|
||||
<a-button type="link" danger size="small" v-if="hasPerm('bizUserDelete')">{{
|
||||
$t('common.removeButton')
|
||||
}}</a-button>
|
||||
@ -123,7 +123,7 @@
|
||||
<a-menu>
|
||||
<a-menu-item v-if="hasPerm('bizUserPwdReset')">
|
||||
<a-popconfirm
|
||||
:title="$t('user.popconfirmResatUserPwd')"
|
||||
:title="$t('user.popConfirmResatUserPwd')"
|
||||
placement="topRight"
|
||||
@confirm="resetPassword(record)"
|
||||
>
|
||||
|
||||
@ -85,7 +85,14 @@
|
||||
placeholder="请选择邮箱无对应用户时策略"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="是否允许动态口令登录:" name="SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B">
|
||||
<a-switch
|
||||
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
placeholder="请选择是否允许动态口令登录"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
|
||||
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
|
||||
@ -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 = () => {
|
||||
|
||||
@ -85,6 +85,14 @@
|
||||
placeholder="请选择邮箱无对应用户时策略"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否允许动态口令登录:" name="SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C">
|
||||
<a-switch
|
||||
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
placeholder="请选择是否允许动态口令登录"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
|
||||
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
|
||||
@ -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 = () => {
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a @click="formRef.onOpen(record)">{{ $t('common.editButton') }}</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm :title="$t('user.popconfirmDeleteUser')" placement="topRight" @confirm="removeUser(record)">
|
||||
<a-popconfirm :title="$t('user.popConfirmDeleteUser')" placement="topRight" @confirm="removeUser(record)">
|
||||
<a-button type="link" danger size="small">
|
||||
{{ $t('common.removeButton') }}
|
||||
</a-button>
|
||||
@ -113,7 +113,7 @@
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a-popconfirm
|
||||
:title="$t('user.popconfirmResatUserPwd')"
|
||||
:title="$t('user.popConfirmResatUserPwd')"
|
||||
placement="topRight"
|
||||
@confirm="resetPassword(record)"
|
||||
>
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<CropUpload ref="cropUploadRef" :img-src="userInfo ? userInfo.avatar : undefined" @successful="cropUploadSuccess" />
|
||||
<CropUpload ref="cropUploadRef" :img-src="userInfo ? userInfo.avatar : undefined" @successful="cropUploadSuccess" :z-index="2000" />
|
||||
</template>
|
||||
|
||||
<script setup name="userCenter">
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<a-form-item label="昵称:" name="nickname">
|
||||
<a-input v-model:value="formData.nickname" placeholder="请输入昵称" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="性别:" name="sex">
|
||||
<a-form-item label="性别:" name="gender">
|
||||
<a-radio-group v-model:value="formData.gender" :options="genderOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="生日:" name="birthday">
|
||||
@ -43,8 +43,8 @@
|
||||
const submitLoading = ref(false)
|
||||
// 默认要校验的
|
||||
const formRules = {
|
||||
name: [required('请输入姓名')],
|
||||
gender: [required('请选择性别')]
|
||||
account: [required('请输入账号')],
|
||||
name: [required('请输入姓名')]
|
||||
}
|
||||
const genderOptions = tool.dictList('GENDER')
|
||||
// 验证并提交数据
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-list item-layout="horizontal" :data-source="data">
|
||||
<a-list item-layout="horizontal" :data-source="bindInfoList">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta class="list-item-meta">
|
||||
@ -10,17 +10,16 @@
|
||||
<span class="security-list-value">{{ item.value }}</span>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<qq-outlined v-if="item.type === 'qq'" class="bind-icon" :style="{ color: '#1677FF' }" />
|
||||
<wechat-outlined v-if="item.type === 'weChat'" class="bind-icon" :style="{ color: '#1AAD19' }" />
|
||||
<alipay-circle-outlined v-if="item.type === 'AliPay'" class="bind-icon" :style="{ color: '#178bf5' }" />
|
||||
<mail-outlined v-if="item.type === 'email'" class="bind-icon" :style="{ color: '#fcab43' }" />
|
||||
<mobile-outlined v-if="item.type === 'phone'" class="bind-icon" :style="{ color: '#43a0fc' }" />
|
||||
<verified-outlined v-if="item.type === 'password'" class="bind-icon" :style="{ color: '#a059e8' }" />
|
||||
<usb-outlined v-if="item.type === 'otp'" class="bind-icon" :style="{ color: '#1AAD19' }" />
|
||||
<GiteeIcon v-if="item.type === 'Gitee'" class="bind-icon xn-wd40" />
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a @click="bindCommon(item.type)">{{ item.value ? '修改' : '去绑定' }}</a>
|
||||
<a @click="bindCommon(item)">{{ item.value ? (item.type === 'otp'?'解绑' : '修改') : '去绑定' }}</a>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
@ -28,6 +27,7 @@
|
||||
<updatePassword ref="updatePasswordRef" />
|
||||
<bind-phone ref="bindPhoneRef" />
|
||||
<bind-email ref="bindEmailRef" />
|
||||
<bind-otp ref="bindOtpRef" @successful="getOtpInfoBindStatus()"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -35,18 +35,20 @@
|
||||
import UpdatePassword from './bindForm/updatePassword.vue'
|
||||
import BindPhone from '@/views/sys/user/userTab/bindForm/bindPhone.vue'
|
||||
import BindEmail from '@/views/sys/user/userTab/bindForm/bindEmail.vue'
|
||||
import BindOtp from '@/views/sys/user/userTab/bindForm/bindOtp.vue'
|
||||
// 按需导入图标组件
|
||||
import QqOutlined from '@ant-design/icons-vue/QqOutlined'
|
||||
import WechatOutlined from '@ant-design/icons-vue/WechatOutlined'
|
||||
import AlipayCircleOutlined from '@ant-design/icons-vue/AlipayCircleOutlined'
|
||||
import MailOutlined from '@ant-design/icons-vue/MailOutlined'
|
||||
import MobileOutlined from '@ant-design/icons-vue/MobileOutlined'
|
||||
import VerifiedOutlined from '@ant-design/icons-vue/VerifiedOutlined'
|
||||
import UsbOutlined from '@ant-design/icons-vue/UsbOutlined'
|
||||
import { globalStore } from '@/store'
|
||||
import userCenterApi from "@/api/sys/userCenterApi";
|
||||
|
||||
const updatePasswordRef = ref()
|
||||
const bindPhoneRef = ref()
|
||||
const bindEmailRef = ref()
|
||||
const bindOtpRef = ref()
|
||||
const store = globalStore()
|
||||
const userInfo = computed(() => {
|
||||
if (store.userInfo) {
|
||||
@ -60,40 +62,58 @@
|
||||
}
|
||||
})
|
||||
// 获取绑定的情况
|
||||
const data = [
|
||||
const bindInfoList = ref([
|
||||
{ title: '密码强度', description: '当前密码强度', value: '弱', type: 'password', bindStatus: 0 },
|
||||
{
|
||||
title: '邮箱',
|
||||
description: userInfo && userInfo.value.email ? '已绑定邮箱' : '未绑定邮箱',
|
||||
value: userInfo && userInfo.value.email ? userInfo.value.email : '',
|
||||
type: 'email',
|
||||
bindStatus: 0
|
||||
bindStatus: userInfo && userInfo.value.email
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
description: userInfo && userInfo.value.phone ? '已绑定手机' : '未绑定手机',
|
||||
value: userInfo && userInfo.value.phone ? userInfo.value.phone : '',
|
||||
type: 'phone',
|
||||
bindStatus: 1
|
||||
bindStatus: userInfo && userInfo.value.phone
|
||||
},
|
||||
{ title: '绑定QQ', description: '未绑定', value: '', type: 'qq', bindStatus: 0 },
|
||||
{ title: '绑定微信', description: '未绑定', value: '', type: 'weChat', bindStatus: 0 },
|
||||
{ title: '绑定支付宝', description: '未绑定', value: '', type: 'AliPay', bindStatus: 0 },
|
||||
{ title: '绑定Gitee', description: '未绑定', value: '', type: 'Gitee', bindStatus: 0 }
|
||||
]
|
||||
const bindCommon = (key) => {
|
||||
])
|
||||
const bindCommon = (item) => {
|
||||
let key = item.type
|
||||
if (key === 'password') {
|
||||
updatePasswordRef.value.onOpen()
|
||||
} else if (key === 'phone') {
|
||||
bindPhoneRef.value.open(userInfo.value.phone)
|
||||
bindPhoneRef.value.open()
|
||||
} else if (key === 'email') {
|
||||
bindEmailRef.value.open(userInfo.value.email)
|
||||
bindEmailRef.value.open()
|
||||
} else if (key === 'otp') {
|
||||
if(item.value) {
|
||||
bindOtpRef.value.onOpen('unbind')
|
||||
} else {
|
||||
bindOtpRef.value.onOpen('bind')
|
||||
}
|
||||
} else {
|
||||
message.info('开发中')
|
||||
message.info('请在登录页使用三方登录后输入账号信息' + item.title)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
// 获取绑定情况
|
||||
// 获取动态口令绑定状态
|
||||
const getOtpInfoBindStatus = async () => {
|
||||
userCenterApi.userCenterGetOtpInfoBindStatus().then((data) => {
|
||||
bindInfoList.value[3] = {
|
||||
title: '动态口令',
|
||||
description: data ? '已绑定动态口令' : '未绑定动态口令',
|
||||
value: data ? '******' : '',
|
||||
type: 'otp',
|
||||
bindStatus: data
|
||||
}
|
||||
})
|
||||
}
|
||||
onMounted(async () => {
|
||||
// 获取动态口令绑定状态
|
||||
await getOtpInfoBindStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
120
snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue
Normal file
120
snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-modal title="绑定动态口令" :width="800" :open="visible" :destroy-on-close="true" @cancel="onClose">
|
||||
<a-skeleton active v-if="!otpInfo" />
|
||||
<div v-else>
|
||||
<a-alert type="info" banner class="mb-3">
|
||||
<template #description>
|
||||
<p>1.打开Google Authenticator或者Okta Verify等动态口令应用。</p>
|
||||
<p>2.点击“扫一扫”或者“手动输入”,将动态口令应用中的二维码扫描到应用中。</p>
|
||||
<p>3.在下方输入框中输入动态口令,即可完成绑定/解绑。</p>
|
||||
</template>
|
||||
</a-alert>
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="8">
|
||||
<img style="width: 100%;vertical-align: middle" :src="otpInfo.otpInfoBase64" />
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-descriptions :column="1" size="middle" bordered class="mb-2">
|
||||
<a-descriptions-item label="发行者">{{otpInfo.otpInfo.issuer}}</a-descriptions-item>
|
||||
<a-descriptions-item label="账号">{{otpInfo.otpInfo.account}}</a-descriptions-item>
|
||||
<a-descriptions-item label="密钥">{{otpInfo.otpInfo.secretKey}}</a-descriptions-item>
|
||||
<a-descriptions-item label="算法">{{otpInfo.otpInfo.algorithm}}</a-descriptions-item>
|
||||
<a-descriptions-item label="位数">{{otpInfo.otpInfo.digits}}</a-descriptions-item>
|
||||
<a-descriptions-item label="周期">{{otpInfo.otpInfo.period}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
|
||||
<a-form-item
|
||||
label="动态口令"
|
||||
name="otpCode"
|
||||
has-feedback
|
||||
>
|
||||
<a-input v-model:value="formState.otpCode" placeholder="请输入动态口令" allow-clear autocomplete="off"/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
|
||||
<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="bindOtp">
|
||||
import { required } from '@/utils/formRules'
|
||||
import userCenterApi from '@/api/sys/userCenterApi'
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits({ successful: null })
|
||||
// 默认是关闭状态
|
||||
const visible = ref(false)
|
||||
const formRef = ref()
|
||||
// 表单数据
|
||||
const formState = ref({})
|
||||
const submitLoading = ref(false)
|
||||
const otpInfo = ref()
|
||||
const bindType = ref()
|
||||
// 打开抽屉
|
||||
const onOpen = (type) => {
|
||||
visible.value = true
|
||||
bindType.value = type
|
||||
// 获得动态口令信息
|
||||
userCenterApi.userCenterGetOtpInfo().then((data) => {
|
||||
otpInfo.value = data
|
||||
})
|
||||
}
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
visible.value = false
|
||||
otpInfo.value = {}
|
||||
formState.value = {}
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
// 默认要校验的
|
||||
const formRules = {
|
||||
otpCode: [required('请输入动态口令')]
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
const onSubmit = async () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
submitLoading.value = true
|
||||
if(bindType.value === 'bind') {
|
||||
userCenterApi
|
||||
.userCenterBindOtp(formState.value)
|
||||
.then(() => {
|
||||
message.success('绑定成功')
|
||||
visible.value = false
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
} else {
|
||||
userCenterApi
|
||||
.userCenterUnBindOtp(formState.value)
|
||||
.then(() => {
|
||||
message.success('解绑成功')
|
||||
visible.value = false
|
||||
formRef.value.resetFields()
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 调用这个函数将子组件的一些数据和方法暴露出去
|
||||
defineExpose({
|
||||
onOpen
|
||||
})
|
||||
</script>
|
||||
@ -135,5 +135,23 @@
|
||||
<groupId>com.github.wnameless.json</groupId>
|
||||
<artifactId>json-flattener</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- zxing -->
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- bouncycastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- bouncycastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -29,6 +29,9 @@ import org.springframework.stereotype.Component;
|
||||
@ConfigurationProperties(prefix = "snowy.config.common")
|
||||
public class CommonProperties {
|
||||
|
||||
/** 前端地址 */
|
||||
private String frontUrl;
|
||||
|
||||
/** 后端地址 */
|
||||
private String backendUrl;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<JSONObject> getUserListByIdListWithoutException(List<String> userIdList);
|
||||
|
||||
/**
|
||||
* 根据用户id获取用户对象,没有则抛出异常
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/6/20 18:19
|
||||
**/
|
||||
JSONObject getUserByIdWithException(String userId);
|
||||
|
||||
/**
|
||||
* 根据用户id获取用户对象列表,只要有一个没有则抛出异常
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/6/20 18:19
|
||||
**/
|
||||
List<JSONObject> getUserListByIdWithException(List<String> userIdList);
|
||||
|
||||
/**
|
||||
* 获取用户列表(排除当前用户)
|
||||
*
|
||||
* @author chengchuanyao
|
||||
* @date 2024/7/19 9:54
|
||||
*/
|
||||
List<JSONObject> listUserWithoutCurrent();
|
||||
|
||||
/**
|
||||
* 获取用户扩展信息,没有则创建
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/7/8 9:26
|
||||
**/
|
||||
JSONObject getOrCreateClientUserExt(String userId);
|
||||
}
|
||||
@ -128,4 +128,12 @@ public interface SysUserApi {
|
||||
* @date 2022/6/20 18:19
|
||||
**/
|
||||
List<JSONObject> getPositionListByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 获取用户扩展信息,没有则创建
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/7/8 9:26
|
||||
**/
|
||||
JSONObject getOrCreateSysUserExt(String userId);
|
||||
}
|
||||
|
||||
@ -22,6 +22,18 @@
|
||||
<artifactId>snowy-plugin-auth-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 引入系统接口 -->
|
||||
<dependency>
|
||||
<groupId>vip.xiaonuo</groupId>
|
||||
<artifactId>snowy-plugin-sys-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 引入C端接口 -->
|
||||
<dependency>
|
||||
<groupId>vip.xiaonuo</groupId>
|
||||
<artifactId>snowy-plugin-client-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 引入开发工具接口,用于获取配置 -->
|
||||
<dependency>
|
||||
<groupId>vip.xiaonuo</groupId>
|
||||
@ -52,6 +64,12 @@
|
||||
<artifactId>sa-token-sso</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 插件:整合 Forest 请求工具 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-forest</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JustAuth 第三方登录 -->
|
||||
<dependency>
|
||||
<groupId>me.zhyd.oauth</groupId>
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> 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<Boolean> isLogin() {
|
||||
return CommonResult.data(StpClientUtil.isLogin());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> 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<Boolean> isLogin() {
|
||||
return CommonResult.data(StpUtil.isLogin());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
|
||||
@ -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("管理员未开启注册");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<String> 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<String> 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<String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> 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<AuthThirdUser>> page(AuthThirdUserPageParam authThirdUserPageParam) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<AuthThirdUser> {
|
||||
**/
|
||||
String callback(AuthThirdCallbackParam authThirdCallbackParam, AuthCallback authCallback);
|
||||
|
||||
/**
|
||||
* 第三方登录绑定账号
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/4/24 20:08
|
||||
*/
|
||||
String bindAccount(AuthThirdBindAccountParam authThirdBindAccountParam);
|
||||
|
||||
/**
|
||||
* 获取三方用户分页
|
||||
*
|
||||
|
||||
@ -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<AuthThirdMapper, AuthThirdUser> 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<AuthThirdMapper, AuthThird
|
||||
private static final String SNOWY_THIRD_WECHAT_CLIENT_SECRET_KEY = "SNOWY_THIRD_WECHAT_CLIENT_SECRET";
|
||||
private static final String SNOWY_THIRD_WECHAT_REDIRECT_URL_KEY = "SNOWY_THIRD_WECHAT_REDIRECT_URL";
|
||||
|
||||
@Resource
|
||||
private CommonCacheOperator commonCacheOperator;
|
||||
|
||||
@Resource
|
||||
private DevConfigApi devConfigApi;
|
||||
|
||||
@ -84,19 +93,20 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
|
||||
|
||||
// 获取请求
|
||||
AuthRequest authRequest = this.getAuthRequest(authThirdRenderParam.getPlatform());
|
||||
|
||||
// 获取状态
|
||||
// 获取登录端类型
|
||||
String clientType = authThirdRenderParam.getClientType();
|
||||
// 校验登录端类型
|
||||
SaClientTypeEnum.validate(clientType);
|
||||
// 创建state
|
||||
String state = AuthStateUtils.createState();
|
||||
|
||||
// 放入缓存
|
||||
commonCacheOperator.put(CONFIG_CACHE_KEY + state, JSONUtil.createObj().set("clientType", clientType), 300);
|
||||
// 构造授权地址
|
||||
String authorizeUrl = authRequest.authorize(state);
|
||||
|
||||
// 构造结果
|
||||
AuthThirdRenderResult authThirdRenderResult = new AuthThirdRenderResult();
|
||||
|
||||
// 返回授权地址
|
||||
authThirdRenderResult.setAuthorizeUrl(authorizeUrl);
|
||||
|
||||
// 返回状态码
|
||||
authThirdRenderResult.setState(state);
|
||||
return authThirdRenderResult;
|
||||
@ -106,44 +116,79 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public String callback(AuthThirdCallbackParam authThirdCallbackParam, AuthCallback authCallback) {
|
||||
|
||||
// 获取请求
|
||||
AuthRequest authRequest = this.getAuthRequest(authThirdCallbackParam.getPlatform());
|
||||
|
||||
// 获取state
|
||||
String state = authThirdCallbackParam.getState();
|
||||
// 获取缓存值
|
||||
Object stateCacheValueObj = commonCacheOperator.get(CONFIG_CACHE_KEY + state);
|
||||
// 判断是否为空
|
||||
if(ObjectUtil.isEmpty(stateCacheValueObj)){
|
||||
throw new CommonException("state已失效");
|
||||
}
|
||||
// 获取登录端类型
|
||||
String clientType = JSONUtil.parseObj(stateCacheValueObj).getStr("clientType");
|
||||
// 移除缓存
|
||||
commonCacheOperator.remove(CONFIG_CACHE_KEY + state);
|
||||
// 执行请求
|
||||
AuthResponse<AuthUser> 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<AuthThirdUser>().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<AuthThirdUser> page(AuthThirdUserPageParam authThirdUserPageParam) {
|
||||
QueryWrapper<AuthThirdUser> queryWrapper = new QueryWrapper<AuthThirdUser>().checkSqlInjection();
|
||||
@ -165,20 +210,15 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定用户并返回用户id
|
||||
* 保存三方用户并返回主键
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/7/9 14:58
|
||||
*/
|
||||
private String bindUser(AuthUser authUser) {
|
||||
// TODO 此处固定绑定超管
|
||||
SaBaseLoginUser saBaseLoginUser = loginUserApi.getUserByAccount("admin");
|
||||
if(ObjectUtil.isEmpty(saBaseLoginUser)) {
|
||||
throw new CommonException("第三方登录失败,无法绑定账号admin,原因:账户admin不存在");
|
||||
}
|
||||
private String insertAuthThirdUser(AuthUser authUser) {
|
||||
AuthThirdUser authThirdUser = new AuthThirdUser();
|
||||
authThirdUser.setThirdId(authUser.getUuid());
|
||||
authThirdUser.setUserId(saBaseLoginUser.getId());
|
||||
authThirdUser.setUserId(null);
|
||||
authThirdUser.setAvatar(authUser.getAvatar());
|
||||
authThirdUser.setName(authUser.getUsername());
|
||||
authThirdUser.setNickname(authUser.getNickname());
|
||||
@ -186,7 +226,7 @@ public class AuthThirdServiceImpl extends ServiceImpl<AuthThirdMapper, AuthThird
|
||||
authThirdUser.setCategory(authUser.getSource());
|
||||
authThirdUser.setExtJson(JSONUtil.toJsonStr(authUser.getRawUserInfo()));
|
||||
this.save(authThirdUser);
|
||||
return authThirdUser.getUserId();
|
||||
return authThirdUser.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.core.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 是否枚举
|
||||
*
|
||||
* @author yubaoshan
|
||||
* @date 2024/9/17 00:14
|
||||
**/
|
||||
@Getter
|
||||
public enum ClientYesOrNoEnum {
|
||||
|
||||
/** 是 */
|
||||
YES("YES"),
|
||||
|
||||
/** 否 */
|
||||
NO("NO");
|
||||
|
||||
private final String value;
|
||||
|
||||
ClientYesOrNoEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@ -49,4 +49,11 @@ public class ClientUserExt extends CommonEntity {
|
||||
@Schema(description = "密码修改日期")
|
||||
private Date passwordUpdateTime;
|
||||
|
||||
/** OTP密钥 */
|
||||
@Schema(description = "OTP密钥")
|
||||
private String otpSecretKey;
|
||||
|
||||
/** OTP绑定状态 */
|
||||
@Schema(description = "OTP绑定状态")
|
||||
private String hasBindOtp;
|
||||
}
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.modular.user.provider;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.xiaonuo.client.ClientUserApi;
|
||||
import vip.xiaonuo.client.modular.user.entity.ClientUser;
|
||||
import vip.xiaonuo.client.modular.user.service.ClientUserService;
|
||||
import vip.xiaonuo.common.exception.CommonException;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户API接口提供者
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/6/20 18:24
|
||||
**/
|
||||
@Service
|
||||
public class ClientUserApiProvider implements ClientUserApi {
|
||||
|
||||
@Resource
|
||||
private ClientUserService clientUserService;
|
||||
|
||||
@Override
|
||||
public JSONObject getUserByIdWithoutException(String userId) {
|
||||
ClientUser clientUser = clientUserService.getById(userId);
|
||||
if(ObjectUtil.isNotEmpty(clientUser)) {
|
||||
return JSONUtil.parseObj(clientUser);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JSONObject> getUserListByIdListWithoutException(List<String> 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<JSONObject> getUserListByIdWithException(List<String> userIdList) {
|
||||
HashSet<String> userIdSet = CollectionUtil.newHashSet(userIdList);
|
||||
List<ClientUser> 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<JSONObject> listUserWithoutCurrent() {
|
||||
return clientUserService.list(new LambdaQueryWrapper<ClientUser>()
|
||||
.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));
|
||||
}
|
||||
}
|
||||
@ -37,5 +37,5 @@ public interface ClientUserExtService extends IService<ClientUserExt> {
|
||||
* @author xuyuxiang
|
||||
* @date 2022/4/27 21:38
|
||||
*/
|
||||
void createExtInfo(String userId, String sourceFromType);
|
||||
ClientUserExt createExtInfo(String userId, String sourceFromType);
|
||||
}
|
||||
|
||||
@ -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<ClientUser> {
|
||||
* @date 2022/8/25 15:16
|
||||
**/
|
||||
void doRegister(String account, String password);
|
||||
|
||||
/**
|
||||
* 获取用户扩展信息,没有则创建
|
||||
*
|
||||
* @author xuyuxiang
|
||||
* @date 2022/7/8 9:26
|
||||
**/
|
||||
ClientUserExt getOrCreateClientUserExt(String userId);
|
||||
}
|
||||
|
||||
@ -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<ClientUserExtMapper, C
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createExtInfo(String userId, String sourceFromType) {
|
||||
public ClientUserExt createExtInfo(String userId, String sourceFromType) {
|
||||
ClientUserExt clientUserExt = new ClientUserExt();
|
||||
clientUserExt.setUserId(userId);
|
||||
clientUserExt.setSourceFromType(sourceFromType);
|
||||
clientUserExt.setPasswordUpdateTime(DateTime.now());
|
||||
clientUserExt.setHasBindOtp(ClientYesOrNoEnum.NO.getValue());
|
||||
String otpSecretKeyEncrypt = CommonCryptogramUtil.doSm4CbcEncrypt(CommonOtpUtil.generateSecretKey());
|
||||
clientUserExt.setOtpSecretKey(otpSecretKeyEncrypt);
|
||||
this.save(clientUserExt);
|
||||
return clientUserExt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import vip.xiaonuo.auth.core.util.StpClientUtil;
|
||||
import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
|
||||
import vip.xiaonuo.client.core.enums.ClientYesOrNoEnum;
|
||||
import vip.xiaonuo.client.core.util.ClientEmailFormatUtl;
|
||||
import vip.xiaonuo.client.core.util.ClientPasswordUtl;
|
||||
import vip.xiaonuo.client.modular.user.entity.ClientUser;
|
||||
@ -1113,6 +1114,22 @@ public class ClientUserServiceImpl extends ServiceImpl<ClientUserMapper, ClientU
|
||||
this.createUserWithAccount(account, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientUserExt getOrCreateClientUserExt(String userId) {
|
||||
ClientUserExt clientUserExt = clientUserExtService.getOne(new LambdaQueryWrapper<ClientUserExt>().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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码失效时间(单位:秒)
|
||||
*
|
||||
|
||||
@ -70,6 +70,12 @@ public class DevConfigServiceImpl extends ServiceImpl<DevConfigMapper, DevConfig
|
||||
/** 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";
|
||||
|
||||
@Resource
|
||||
private CommonCacheOperator commonCacheOperator;
|
||||
|
||||
@ -123,7 +129,9 @@ public class DevConfigServiceImpl extends ServiceImpl<DevConfigMapper, DevConfig
|
||||
SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B_KEY,
|
||||
SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C_KEY,
|
||||
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B_KEY,
|
||||
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C_KEY));
|
||||
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C_KEY,
|
||||
SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B_KEY,
|
||||
SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C_KEY));
|
||||
return this.list(lambdaQueryWrapper);
|
||||
}
|
||||
|
||||
|
||||
@ -10,4 +10,28 @@
|
||||
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
|
||||
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
|
||||
*/
|
||||
package vip.xiaonuo.client;
|
||||
package vip.xiaonuo.sys.core.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 是否枚举
|
||||
*
|
||||
* @author yubaoshan
|
||||
* @date 2024/9/17 00:14
|
||||
**/
|
||||
@Getter
|
||||
public enum SysYesOrNoEnum {
|
||||
|
||||
/** 是 */
|
||||
YES("YES"),
|
||||
|
||||
/** 否 */
|
||||
NO("NO");
|
||||
|
||||
private final String value;
|
||||
|
||||
SysYesOrNoEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@ -43,4 +43,12 @@ public class SysOrgExt extends CommonEntity {
|
||||
@Schema(description = "来源类别")
|
||||
private String sourceFromType;
|
||||
|
||||
/** 身份源ID */
|
||||
@Schema(description = "身份源ID")
|
||||
private String idSourceId;
|
||||
|
||||
/** 身份源组织ID */
|
||||
@Schema(description = "身份源机构ID")
|
||||
private String idSourceOrgId;
|
||||
|
||||
}
|
||||
|
||||
@ -32,10 +32,7 @@ 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.param.*;
|
||||
import vip.xiaonuo.sys.modular.user.result.SysUserMessageDetailResult;
|
||||
import vip.xiaonuo.sys.modular.user.result.SysUserMessageResult;
|
||||
import vip.xiaonuo.sys.modular.user.result.SysUserPicValidCodeResult;
|
||||
import vip.xiaonuo.sys.modular.user.result.SysUserPositionResult;
|
||||
import vip.xiaonuo.sys.modular.user.result.*;
|
||||
import vip.xiaonuo.sys.modular.user.service.SysUserService;
|
||||
|
||||
import javax.validation.Valid;
|
||||
@ -567,4 +564,60 @@ public class SysUserCenterController {
|
||||
public CommonResult<JSONObject> 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<Boolean> 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<SysUserOtpInfoResult> 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<String> 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<String> unBindOtp(@RequestBody @Valid SysUserOtpParam sysUserOtpParam) {
|
||||
sysUserService.unBindOtp(sysUserOtpParam);
|
||||
return CommonResult.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -37,5 +37,5 @@ public interface SysUserExtService extends IService<SysUserExt> {
|
||||
* @author xuyuxiang
|
||||
* @date 2022/4/27 21:38
|
||||
*/
|
||||
void createExtInfo(String userId, String sourceFromType);
|
||||
SysUserExt createExtInfo(String userId, String sourceFromType);
|
||||
}
|
||||
|
||||
@ -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<SysUser> {
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -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<SysUserExtMapper, SysUser
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createExtInfo(String userId, String sourceFromType) {
|
||||
public SysUserExt createExtInfo(String userId, String sourceFromType) {
|
||||
SysUserExt sysUserExt = new SysUserExt();
|
||||
sysUserExt.setUserId(userId);
|
||||
sysUserExt.setSourceFromType(sourceFromType);
|
||||
sysUserExt.setPasswordUpdateTime(DateTime.now());
|
||||
sysUserExt.setHasBindOtp(SysYesOrNoEnum.NO.getValue());
|
||||
String otpSecretKeyEncrypt = CommonCryptogramUtil.doSm4CbcEncrypt(CommonOtpUtil.generateSecretKey());
|
||||
sysUserExt.setOtpSecretKey(otpSecretKeyEncrypt);
|
||||
this.save(sysUserExt);
|
||||
return sysUserExt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,8 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.PhoneUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||
import cn.hutool.extra.qrcode.QrConfig;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
@ -80,6 +82,7 @@ import vip.xiaonuo.mobile.api.MobileButtonApi;
|
||||
import vip.xiaonuo.mobile.api.MobileMenuApi;
|
||||
import vip.xiaonuo.sys.core.enums.SysBuildInEnum;
|
||||
import vip.xiaonuo.sys.core.enums.SysDataTypeEnum;
|
||||
import vip.xiaonuo.sys.core.enums.SysYesOrNoEnum;
|
||||
import vip.xiaonuo.sys.core.util.SysEmailFormatUtl;
|
||||
import vip.xiaonuo.sys.core.util.SysPasswordUtl;
|
||||
import vip.xiaonuo.sys.modular.group.entity.SysGroup;
|
||||
@ -196,6 +199,9 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> 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<SysUserMapper, SysUser> impl
|
||||
return SysPasswordUtl.getUpdatePasswordValidConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysUserExt getOrCreateSysUserExt(String userId) {
|
||||
SysUserExt sysUserExt = sysUserExtService.getOne(new LambdaQueryWrapper<SysUserExt>().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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码失效时间(单位:秒)
|
||||
*
|
||||
|
||||
@ -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/**",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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 '创建用户',
|
||||
|
||||
@ -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=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user