This commit is contained in:
itcsx committed 2026-09-23 13:24:49 +08:00
1 parent eb7a9e9b88
commit fd51982804
7 files changed
+242 -11

No files matched your search

@@ -19,6 +19,14 @@
<PackageReference Include="Fido2" Version="4.0.1" />
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<!--
必须显式对齐:JwtBearer 8.0.0 传递引入 System.IdentityModel.Tokens.Jwt 7.0.3,
而 Fido2 把 Microsoft.IdentityModel.Tokens / JsonWebTokens 提升到了 8.2.0。
两个版本混用时旧版 JwtSecurityTokenHandler 读令牌会错乱(issuer 解析为空),
抛 IDX10211,导致 AuthService.RefreshToken 永远失败 → 访问令牌 2 小时过期时
用户被提示「无效的令牌,请重新登录.」。这里把 legacy 包也对齐到 8.2.0。
-->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
@@ -417,20 +417,28 @@ namespace CloudBuilder.Security.Service
ValidateLifetime = false,//不验证过期时间
};
// 验证 Token
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken vTocken;
// 验证 Token。
// 必须用 JsonWebTokenHandler(.NET 8 的 JwtBearer 鉴权、ClaimPolicy.GetClaimValue 用的都是它):
// legacy 的 JwtSecurityTokenHandler 与 Microsoft.IdentityModel.Tokens 8.x 版本混用时会错乱解析令牌
// (issuer 读成空 → IDX10211),于是刷新恒失败、访问令牌两小时一到期用户就被踢下线。
var tokenHandler = new JsonWebTokenHandler();
string token = jwtInfo.AccessToken.Replace("Bearer ", "");
try
{
var tokenValidationResult = tokenHandler.ValidateToken(token, parameters, out vTocken);
var tokenValidationResult = tokenHandler.ValidateTokenAsync(token, parameters).GetAwaiter().GetResult();
if (tokenValidationResult == null || !tokenValidationResult.Identities.Any() || !tokenValidationResult.Identities.First().IsAuthenticated)
throw new ServiceErrorException("无效的令牌,请重新登录.");
if (tokenValidationResult == null || !tokenValidationResult.IsValid
|| tokenValidationResult.ClaimsIdentity == null || !tokenValidationResult.ClaimsIdentity.IsAuthenticated)
throw new ServiceErrorException($"无效的令牌,请重新登录.({tokenValidationResult?.Exception?.Message})");
}
catch (Exception)
catch (ServiceErrorException)
{
throw new ServiceErrorException("无效的令牌,请重新登录.");
throw;
}
catch (Exception ex)
{
// 带上原始异常:以前直接吞掉,出问题时只看到"无效的令牌",无法定位
throw new ServiceErrorException($"无效的令牌,请重新登录.({ex.Message})");
}
string clainmName = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_NAME);
@@ -13,6 +13,10 @@
<PackageReference Include="Fido2" Version="4.0.1" />
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<!-- 与 Fido2 提升上来的 Microsoft.IdentityModel.Tokens 8.2.0 对齐;
否则输出目录里会同时存在 Jwt 7.0.3 + Tokens 8.2.0,legacy 校验器会错乱解析令牌。
本项目对同级工程用的是 <Reference>+HintPath(不会传递依赖),所以这里必须单独声明。 -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
<History>True|2026-09-22T15:03:13.6509491Z||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
<History>True|2026-09-23T05:19:13.0148919Z||;True|2026-09-23T13:16:07.8584304+08:00||;True|2026-09-22T23:03:13.6509491+08:00||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>C:\inetpub\wwwroot\windows</_PublishTargetUrl>
<History>True|2026-09-22T15:02:08.0902556Z||;True|2026-09-22T16:47:49.2530282+08:00||;True|2026-09-22T15:18:53.4935889+08:00||;True|2026-09-22T11:24:43.8593406+08:00||;True|2026-09-19T09:55:09.0653660+08:00||;True|2026-09-19T09:53:13.8933944+08:00||;</History>
<History>True|2026-09-23T05:17:58.5542095Z||;True|2026-09-22T23:02:08.0902556+08:00||;True|2026-09-22T16:47:49.2530282+08:00||;True|2026-09-22T15:18:53.4935889+08:00||;True|2026-09-22T11:24:43.8593406+08:00||;True|2026-09-19T09:55:09.0653660+08:00||;True|2026-09-19T09:53:13.8933944+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
@@ -38,7 +38,7 @@
"Issuer": "Owen Chen",
"Audience": "Cloud Builder",
"Secret": "{8619F7C3-B53C-4B85-99F0-983D351ECD82}",
"AccessExpireMins": 120, //2小时过期
"AccessExpireMins": 2, //2小时过期
"RefreshExpireMins": 86400, // 60天重刷新过期时间
"IsRefreshAbsoluteExpired": false
},
+211
View File
@@ -0,0 +1,211 @@
<template>
<section class="hero">
<div class="hero-inner">
<div class="hero-top">
<div class="brand" :class="{ 'is-clickable': clickable }" @click="onBrandClick">
<a-avatar
shape="square"
:size="36"
src="/youjia.svg"
class="brand-mark"
/>
<span>{{ title }}</span>
</div>
<a-dropdown placement="bottomRight" :trigger="['click']">
<div class="avatar" :title="userName">{{ avatarText }}</div>
<template #overlay>
<a-menu @click="onUserMenuClick">
<a-menu-item key="amentUserLocalName">
<UserOutlined />
{{ t("layout.header.amentUserLocalName") }}
</a-menu-item>
<a-menu-item key="amentPassword">
<LockOutlined />
{{ t("layout.header.amentPassword") }}
</a-menu-item>
<a-menu-item key="biometricButton">
<LockOutlined />
{{ biometricButtonLabel }}
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">
<LogoutOutlined />
{{ t("layout.header.logout") }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</section>
<changePassword v-model:showModel="showChangePassword" />
<changeLocalName v-model:showModel="showChangeUserLocalName" />
<changeBiometricEnrollCard v-model:showModel="showBiometricEnroll" />
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { notification } from "ant-design-vue";
import type { MenuProps } from "ant-design-vue";
import { LockOutlined, LogoutOutlined, UserOutlined } from "@ant-design/icons-vue";
import authService from "@/service/security/authService";
import { useLoginProfileStore } from "@/store/modules/loginProfile";
import i18n from "@/locales";
import changePassword from "@/views/login/changePassword.vue";
import changeLocalName from "@/views/login/changeLocalName.vue";
import changeBiometricEnrollCard from "@/views/login/changeBiometricEnrollCard.vue";
/**
* 欢迎页顶部那一条「品牌 + 头像」的渐变头图。
*
* 欢迎页(DashboardHero)与后台布局(layout/index.vue、layout/master.vue)共用它,
* 两处顶栏才会长得一模一样。
*/
const props = withDefaults(
defineProps<{
/** 品牌名 */
title?: string;
/** 品牌名是否可点(后台布局点了回工作台) */
clickable?: boolean;
}>(),
{ title: "有家相册", clickable: false },
);
const emit = defineEmits<{ (e: "brand-click"): void }>();
const router = useRouter();
const profile = useLoginProfileStore();
const { t } = i18n.global;
const userName = computed(
() => profile.getUserLocalName() || profile.getUsername() || "新朋友",
);
const avatarText = computed(() => {
const u = (userName.value || "U").trim();
return u ? u.slice(-1).toUpperCase() : "U";
});
const onBrandClick = () => {
if (!props.clickable) return;
emit("brand-click");
};
/* ==================== 头像菜单 ==================== */
const showChangePassword = ref<boolean>(false);
const showChangeUserLocalName = ref<boolean>(false);
const showBiometricEnroll = ref<boolean>(false);
// Coarse platform detection so the button label is friendly on each device.
const biometricButtonLabel = computed(() => {
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
if (/iPhone|iPad/.test(ua)) return "Sign in with Face ID / Touch ID";
if (/Android/.test(ua)) return "Sign in with fingerprint";
if (/Windows/.test(ua)) return "Sign in with Windows Hello";
if (/Mac OS X/.test(ua)) return "Sign in with Touch ID";
return "Sign in with fingerprint / face";
});
const onUserMenuClick: MenuProps["onClick"] = (menuInfo) => {
if (menuInfo.key === "amentUserLocalName") {
showChangeUserLocalName.value = true;
} else if (menuInfo.key === "amentPassword") {
showChangePassword.value = true;
} else if (menuInfo.key === "biometricButton") {
showBiometricEnroll.value = true;
} else if (menuInfo.key === "logout") {
logout();
}
};
const logout = async () => {
await authService.Logout();
profile.clearStorage();
notification.info({
message: t("layout.notification.message"),
description: t("layout.notification.description"),
placement: "topRight",
});
router.push("/");
};
</script>
<style lang="less" scoped>
/* 渐变头图:与欢迎页 hero 完全一致 */
.hero {
/* 通栏:不管父容器是 block 还是 flex,渐变条都要铺满整宽 */
width: 100%;
box-sizing: border-box;
padding: 18px 16px 36px;
border-radius: 0 0 22px 22px;
background: linear-gradient(135deg, #1677ff 0%, #36cfc9 100%);
color: #fff;
box-shadow: 0 6px 18px rgba(22, 119, 255, 0.22);
}
.hero-inner {
margin: 0 auto;
}
.hero-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0;
}
.brand {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
&.is-clickable {
cursor: pointer;
}
}
.brand-mark {
display: flex;
align-items: center;
justify-content: center;
border-radius: 9px;
background: rgba(255, 255, 255, 0.22);
overflow: hidden;
}
.avatar {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 50%;
background: #fff;
color: #1677ff;
font-size: 14px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
cursor: pointer;
user-select: none;
}
@media (min-width: 576px) {
.hero {
padding: 22px 20px 40px;
}
}
@media (min-width: 1200px) {
.hero-inner {
max-width: 1080px;
margin-right: auto;
margin-left: auto;
}
.hero {
padding: 24px 24px 44px;
}
}
</style>