diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d34be7cb4..ac4fe231d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: - "**.csproj" env: - DOTNET_VERSION: "9.0.101" + DOTNET_VERSION: "9.0.301" jobs: build: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 60b82dcdc..a5cf75644 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [ main ] env: - DOTNET_VERSION: "9.0.101" + DOTNET_VERSION: "9.0.301" jobs: publish: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fdaa2c12..3581e1884 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,4 +14,4 @@ jobs: with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false - automatic_release_tag: "9.1.3" + automatic_release_tag: "9.2.0" diff --git a/Directory.Packages.props b/Directory.Packages.props index fcb44d78d..d38fbca96 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,18 +1,18 @@ - 8.3.2 + 8.3.5 2.15.1 3.3.5 - 9.1.3 - 9.1.3 - 9.0.0 - 9.0.0 - 9.0.0 + 9.2.0 + 9.2.0 + 9.0.4 + 9.0.4 + 9.0.4 true - + @@ -33,6 +33,7 @@ + @@ -72,6 +73,7 @@ + @@ -89,13 +91,14 @@ + + - @@ -121,6 +124,7 @@ + @@ -130,6 +134,7 @@ + @@ -142,6 +147,7 @@ + @@ -150,7 +156,8 @@ - + + @@ -220,35 +227,35 @@ - - - + + + - - - - - - + + + + + + - + - - - - - - - - + + + + + + + + - + @@ -256,25 +263,34 @@ - + + + + + + + + + + - - - - + + + + - + - + @@ -282,8 +298,8 @@ - - + + @@ -292,19 +308,19 @@ - + - + - + - - - - + + + + diff --git a/apps/vben5/apps/app-antd/.env.development b/apps/vben5/apps/app-antd/.env.development index d7204a13d..2e7b0f3d5 100644 --- a/apps/vben5/apps/app-antd/.env.development +++ b/apps/vben5/apps/app-antd/.env.development @@ -15,6 +15,13 @@ VITE_DEVTOOLS=false # 是否注入全局loading VITE_INJECT_APP_LOADING=true +# 是否仅允许OIDC登录 +VITE_GLOB_ONLY_OIDC=false +# 认证服务器 +VITE_GLOB_AUTHORITY="http://127.0.0.1:30001" +# 授权范围 VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" +# 客户端Id VITE_GLOB_CLIENT_ID=vue-admin-client +# 客户端密钥【生产环境请勿设置此值,建议启用仅允许OIDC登录,将使用授权码类型登录】 VITE_GLOB_CLIENT_SECRET=1q2w3e* diff --git a/apps/vben5/apps/app-antd/.env.production b/apps/vben5/apps/app-antd/.env.production index 5375847a6..40c957808 100644 --- a/apps/vben5/apps/app-antd/.env.production +++ b/apps/vben5/apps/app-antd/.env.production @@ -10,10 +10,23 @@ VITE_COMPRESS=none VITE_PWA=false # vue-router 的模式 -VITE_ROUTER_HISTORY=hash +# oauth2.0协议要求回调必须是完整的url +# VITE_ROUTER_HISTORY=hash # 是否注入全局loading VITE_INJECT_APP_LOADING=true # 打包后是否生成dist.zip VITE_ARCHIVER=true + +# 是否仅允许OIDC登录 +VITE_GLOB_ONLY_OIDC=false + +# 认证服务器 +VITE_GLOB_AUTHORITY="http://127.0.0.1:30001" + +# 授权范围 +VITE_GLOB_AUDIENCE="openid email address phone profile offline_access lingyun-abp-application" + +# 客户端Id +VITE_GLOB_CLIENT_ID=vue-oauth-client diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index 392c09fdc..0f267f1d4 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -64,6 +64,7 @@ "@vueuse/core": "catalog:", "ant-design-vue": "catalog:", "dayjs": "catalog:", + "oidc-client-ts": "catalog:", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/apps/vben5/apps/app-antd/src/adapter/request/index.ts b/apps/vben5/apps/app-antd/src/adapter/request/index.ts index ddafbf93c..e0c13d6a5 100644 --- a/apps/vben5/apps/app-antd/src/adapter/request/index.ts +++ b/apps/vben5/apps/app-antd/src/adapter/request/index.ts @@ -5,7 +5,7 @@ import { } from '@vben/request'; import { useAccessStore } from '@vben/stores'; -import { useOAuthError, useTokenApi } from '@abp/account'; +import { useOAuthError } from '@abp/account'; import { useAbpStore } from '@abp/core'; import { requestClient, useWrapperResult } from '@abp/request'; import { message } from 'ant-design-vue'; @@ -13,7 +13,6 @@ import { message } from 'ant-design-vue'; import { useAuthStore } from '#/store'; export function initRequestClient() { - const { refreshTokenApi } = useTokenApi(); /** * 重新认证逻辑 */ @@ -36,19 +35,11 @@ export function initRequestClient() { * 刷新token逻辑 */ async function doRefreshToken() { - const accessStore = useAccessStore(); - if (accessStore.refreshToken) { - try { - const { accessToken, tokenType, refreshToken } = await refreshTokenApi({ - refreshToken: accessStore.refreshToken, - }); - const newToken = `${tokenType} ${accessToken}`; - accessStore.setAccessToken(newToken); - accessStore.setRefreshToken(refreshToken); - return newToken; - } catch { - console.warn('The refresh token has expired or is unavailable.'); - } + const authStore = useAuthStore(); + try { + return await authStore.refreshSession(); + } catch { + console.warn('The refresh token has expired or is unavailable.'); } return ''; } diff --git a/apps/vben5/apps/app-antd/src/auth/authService.ts b/apps/vben5/apps/app-antd/src/auth/authService.ts new file mode 100644 index 000000000..aaeaa626b --- /dev/null +++ b/apps/vben5/apps/app-antd/src/auth/authService.ts @@ -0,0 +1,53 @@ +import { useAppConfig } from '@vben/hooks'; + +import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; + +const { authority, audience, clientId, clientSecret, disablePKCE } = + useAppConfig(import.meta.env, import.meta.env.PROD); + +const userManager = new UserManager({ + authority, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: `${window.location.origin}/signin-callback`, + response_type: 'code', + scope: audience, + post_logout_redirect_uri: `${window.location.origin}/`, + silent_redirect_uri: `${window.location.origin}/silent-renew.html`, + automaticSilentRenew: true, + loadUserInfo: true, + userStore: new WebStorageStateStore({ store: window.localStorage }), + disablePKCE, +}); + +export default { + async login() { + return userManager.signinRedirect(); + }, + + async logout() { + return userManager.signoutRedirect(); + }, + + async refreshToken() { + return userManager.signinSilent(); + }, + + async getAccessToken() { + const user = await userManager.getUser(); + return user?.access_token; + }, + + async isAuthenticated() { + const user = await userManager.getUser(); + return !!user && !user.expired; + }, + + async handleCallback() { + return userManager.signinRedirectCallback(); + }, + + async getUser() { + return userManager.getUser(); + }, +}; diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index d6145c3b6..277f4585d 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -11,6 +11,14 @@ }, "qrcodeLogin": { "scaned": "Please confirm login on your phone." + }, + "errors": { + "accountLockedByInvalidLoginAttempts": "The user account has been locked out due to invalid login attempts. Please wait a while and try again.", + "accountInactive": "You are not allowed to login! Your account is inactive.", + "invalidUserNameOrPassword": "Invalid username or password!", + "tokenHasExpired": "The token is no longer valid!", + "requiresTwoFactor": "Identity verification is required. Please select a verification method!", + "shouldChangePassword": "Your password has expired. Please change it and login!" } }, "manage": { diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json index 618a258c0..03b44db1f 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/page.json @@ -4,7 +4,10 @@ "register": "Register", "codeLogin": "Code Login", "qrcodeLogin": "Qr Code Login", - "forgetPassword": "Forget Password" + "forgetPassword": "Forget Password", + "oidcLogin": "Open Connect", + "oidcLoginMessage": "Please click \"OK\" to jump to the Certification center for login.", + "processingLogin": "Processing Login..." }, "dashboard": { "title": "Dashboard", diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index 8edd4ab47..0665783bc 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -11,6 +11,14 @@ }, "qrcodeLogin": { "scaned": "请在手机上确认登录." + }, + "errors": { + "accountLockedByInvalidLoginAttempts": "由于尝试登录无效,用户帐户被锁定.请稍候再试!", + "accountInactive": "您不能登录,您的帐户是无效的!", + "invalidUserNameOrPassword": "用户名或密码错误!", + "tokenHasExpired": "您的请求会话已过期,请重新登录!", + "requiresTwoFactor": "需要验证身份,请选择一种验证方式!", + "shouldChangePassword": "您的密码已过期,请修改密码后登录!" } }, "manage": { diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json index 4cb67081c..2b3f24782 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/page.json @@ -4,7 +4,10 @@ "register": "注册", "codeLogin": "验证码登录", "qrcodeLogin": "二维码登录", - "forgetPassword": "忘记密码" + "forgetPassword": "忘记密码", + "oidcLogin": "认证中心登录", + "oidcLoginMessage": "请点击确定跳转认证中心登录", + "processingLogin": "登录成功,正在跳转中..." }, "dashboard": { "title": "概览", diff --git a/apps/vben5/apps/app-antd/src/router/routes/core.ts b/apps/vben5/apps/app-antd/src/router/routes/core.ts index 7218da228..5952b5926 100644 --- a/apps/vben5/apps/app-antd/src/router/routes/core.ts +++ b/apps/vben5/apps/app-antd/src/router/routes/core.ts @@ -21,6 +21,17 @@ const fallbackNotFoundRoute: RouteRecordRaw = { /** 基本路由,这些路由是必须存在的 */ const coreRoutes: RouteRecordRaw[] = [ + { + component: () => import('#/views/_core/fallback/login-callback.vue'), + meta: { + hideInBreadcrumb: true, + hideInMenu: true, + hideInTab: true, + title: 'Processing login', + }, + name: 'OidcFallback', + path: '/signin-callback', + }, /** * 根路由 * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。 diff --git a/apps/vben5/apps/app-antd/src/store/auth.ts b/apps/vben5/apps/app-antd/src/store/auth.ts index 94227a382..6773557c0 100644 --- a/apps/vben5/apps/app-antd/src/store/auth.ts +++ b/apps/vben5/apps/app-antd/src/store/auth.ts @@ -9,6 +9,7 @@ import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { + usePhoneLoginApi, useProfileApi, useQrCodeLoginApi, useTokenApi, @@ -19,12 +20,14 @@ import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; import { useAbpConfigApi } from '#/api/core/useAbpConfigApi'; +import authService from '#/auth/authService'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { const { publish } = useEventBus(); - const { loginApi } = useTokenApi(); + const { loginApi, refreshTokenApi } = useTokenApi(); const { loginApi: qrcodeLoginApi } = useQrCodeLoginApi(); + const { loginApi: phoneLoginApi } = usePhoneLoginApi(); const { getUserInfoApi } = useUserInfoApi(); const { getConfigApi } = useAbpConfigApi(); const { getPictureApi } = useProfileApi(); @@ -35,6 +38,46 @@ export const useAuthStore = defineStore('auth', () => { const loginLoading = ref(false); + async function refreshSession() { + if (await authService.getAccessToken()) { + const user = await authService.refreshToken(); + const newToken = `${user?.token_type} ${user?.access_token}`; + accessStore.setAccessToken(newToken); + if (user?.refresh_token) { + accessStore.setRefreshToken(user.refresh_token); + } + return newToken; + } else { + const { accessToken, tokenType, refreshToken } = await refreshTokenApi({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + refreshToken: accessStore.refreshToken!, + }); + const newToken = `${tokenType} ${accessToken}`; + accessStore.setAccessToken(newToken); + accessStore.setRefreshToken(refreshToken); + return newToken; + } + } + + async function oidcLogin() { + await authService.login(); + } + + async function oidcCallback() { + try { + const user = await authService.handleCallback(); + return await _loginSuccess({ + accessToken: user.access_token, + tokenType: user.token_type, + refreshToken: user.refresh_token ?? '', + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expiresIn: user.expires_in!, + }); + } finally { + loginLoading.value = false; + } + } + async function qrcodeLogin( key: string, tenantId?: string, @@ -49,6 +92,20 @@ export const useAuthStore = defineStore('auth', () => { } } + async function phoneLogin( + phoneNumber: string, + code: string, + onSuccess?: () => Promise | void, + ) { + try { + loginLoading.value = true; + const result = await phoneLoginApi({ phoneNumber, code }); + return await _loginSuccess(result, onSuccess); + } finally { + loginLoading.value = false; + } + } + /** * 异步处理登录操作 * Asynchronously handle the login process @@ -69,7 +126,10 @@ export const useAuthStore = defineStore('auth', () => { async function logout(redirect: boolean = true) { try { - // await logoutApi(); + if (await authService.getAccessToken()) { + accessStore.setAccessToken(null); + await authService.logout(); + } } catch { // 不做任何处理 } @@ -126,7 +186,6 @@ export const useAuthStore = defineStore('auth', () => { ) { // 异步处理用户登录操作并获取 accessToken let userInfo: null | UserInfo = null; - loginLoading.value = true; const { accessToken, tokenType, refreshToken } = loginResult; // 如果成功获取到 accessToken if (accessToken) { @@ -172,9 +231,13 @@ export const useAuthStore = defineStore('auth', () => { return { $reset, authLogin, + phoneLogin, qrcodeLogin, + oidcLogin, + oidcCallback, fetchUserInfo, loginLoading, logout, + refreshSession, }; }); diff --git a/apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue b/apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue index 556b273af..8238ed31d 100644 --- a/apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue +++ b/apps/vben5/apps/app-antd/src/views/_core/authentication/code-login.vue @@ -1,29 +1,42 @@