【更新】底座增加动态口令登录,完善单点登录客户端用于未来无缝接入统一认证平台,优化诸多代码,更新sql

This commit is contained in:
xuyuxiang 2025-09-14 00:20:56 +08:00
parent 8111719330
commit f4d875ae3c
71 changed files with 2613 additions and 295 deletions

28
pom.xml
View File

@ -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>

View File

@ -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')
}
}

View File

@ -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)
}
}

View File

@ -29,5 +29,9 @@ export default {
// 第三方登录授权回调
thirdCallback(data) {
return request('callback', data, 'get')
},
// 第三方登录绑定账号
thirdBindAccount(data) {
return request('bindAccount', data)
}
}

View File

@ -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)
},
}

View File

@ -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'
}
}

View File

@ -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: '确定要重置吗?'
}
}

View File

@ -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

View File

@ -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',

View File

@ -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) {

View File

@ -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>

View File

@ -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

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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);
}
}

View 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>

View File

@ -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)"
>

View File

@ -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 = () => {

View File

@ -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 = () => {

View File

@ -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)"
>

View File

@ -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">

View File

@ -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')
//

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -29,6 +29,9 @@ import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "snowy.config.common")
public class CommonProperties {
/** 前端地址 */
private String frontUrl;
/** 后端地址 */
private String backendUrl;
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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);
/**
* 校验验证码
*

View File

@ -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("管理员未开启注册");
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
/**
* 获取三方用户分页
*

View File

@ -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();
}
/**

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
/**
* 获取验证码失效时间单位
*

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
/**
* 获取验证码失效时间单位
*

View File

@ -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/**",
};
/**

View File

@ -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 '创建用户',

View File

@ -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=