login.component.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. /*
  2. * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit, OnDestroy, AfterViewInit, Optional } from '@angular/core';
  17. import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
  18. import { Router, ActivatedRoute } from '@angular/router';
  19. import { throwIfAlreadyLoaded } from '@core';
  20. import { ReuseTabService } from '@delon/abc/reuse-tab';
  21. import { SettingsService, _HttpClient } from '@delon/theme';
  22. import { environment } from '@env/environment';
  23. import { NzSafeAny } from 'ng-zorro-antd/core/types';
  24. import { NzMessageService } from 'ng-zorro-antd/message';
  25. import { NzTabChangeEvent } from 'ng-zorro-antd/tabs';
  26. import { finalize } from 'rxjs/operators';
  27. import { AuthnService } from '../../../service/authn.service';
  28. import { ImageCaptchaService } from '../../../service/image-captcha.service';
  29. import { QrCodeService } from '../../../service/qr-code.service';
  30. import { SocialsProviderService } from '../../../service/socials-provider.service';
  31. import { CONSTS } from '../../../shared/consts';
  32. import { stringify } from 'querystring';
  33. @Component({
  34. selector: 'passport-login',
  35. templateUrl: './login.component.html',
  36. styleUrls: ['./login.component.less'],
  37. changeDetection: ChangeDetectionStrategy.OnPush
  38. })
  39. export class UserLoginComponent implements OnInit, OnDestroy {
  40. socials: {
  41. providers: NzSafeAny[];
  42. qrScan: string;
  43. } = {
  44. providers: [],
  45. qrScan: ''
  46. };
  47. form: FormGroup;
  48. error = '';
  49. switchTab = true;
  50. loginType = 'normal';
  51. loading = false;
  52. passwordVisible = false;
  53. qrexpire = false;
  54. imageCaptcha = '';
  55. captchaType = '';
  56. state = '';
  57. count = 0;
  58. interval$: any;
  59. //二维码内容
  60. ticket = '';
  61. constructor(
  62. fb: FormBuilder,
  63. private router: Router,
  64. private settingsService: SettingsService,
  65. private authnService: AuthnService,
  66. private socialsProviderService: SocialsProviderService,
  67. private imageCaptchaService: ImageCaptchaService,
  68. private qrCodeService: QrCodeService,
  69. @Optional()
  70. @Inject(ReuseTabService)
  71. private reuseTabService: ReuseTabService,
  72. private route: ActivatedRoute,
  73. private msg: NzMessageService,
  74. private cdr: ChangeDetectorRef,
  75. private http: _HttpClient
  76. ) {
  77. this.form = fb.group({
  78. userName: [null, [Validators.required]],
  79. password: [null, [Validators.required]],
  80. captcha: [null, [Validators.required]],
  81. mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
  82. otpCaptcha: [null, [Validators.required]],
  83. remember: [false]
  84. });
  85. }
  86. ngOnInit(): void {
  87. //set redirect_uri , is BASE64URL
  88. if (this.route.snapshot.queryParams[CONSTS.REDIRECT_URI]) {
  89. this.authnService.setRedirectUri(this.route.snapshot.queryParams[CONSTS.REDIRECT_URI]);
  90. }
  91. //congress login
  92. if (this.route.snapshot.queryParams[CONSTS.CONGRESS]) {
  93. this.congressLogin(this.route.snapshot.queryParams[CONSTS.CONGRESS]);
  94. }
  95. //init socials,state
  96. this.authnService.clear();
  97. this.get();
  98. this.cdr.detectChanges();
  99. }
  100. get() {
  101. this.authnService
  102. .get({ remember_me: localStorage.getItem(CONSTS.REMEMBER) })
  103. .pipe(
  104. finalize(() => {
  105. this.loading = false;
  106. this.cdr.detectChanges();
  107. })
  108. )
  109. .subscribe(res => {
  110. this.loading = true;
  111. if (res.code !== 0) {
  112. this.error = res.msg;
  113. } else {
  114. // 清空路由复用信息
  115. //console.log(res.data);
  116. //REMEMBER ME
  117. if (res.data.token) {
  118. // 清空路由复用信息
  119. this.reuseTabService.clear();
  120. // 设置用户Token信息
  121. this.authnService.auth(res.data);
  122. this.authnService.navigate({});
  123. } else {
  124. this.socials = res.data.socials;
  125. this.state = res.data.state;
  126. this.captchaType = res.data.captcha;
  127. if (this.captchaType === 'NONE') {
  128. //清除校验规则
  129. this.form.get('captcha')?.clearValidators();
  130. } else {
  131. //init image captcha
  132. this.getImageCaptcha();
  133. }
  134. }
  135. }
  136. });
  137. }
  138. congressLogin(congress: string) {
  139. this.authnService
  140. .congress({
  141. congress: congress
  142. })
  143. .pipe(
  144. finalize(() => {
  145. this.loading = false;
  146. this.cdr.detectChanges();
  147. })
  148. )
  149. .subscribe(res => {
  150. this.loading = true;
  151. if (res.code !== 0) {
  152. this.error = res.msg;
  153. } else {
  154. // 清空路由复用信息
  155. this.reuseTabService.clear();
  156. // 设置用户Token信息
  157. this.authnService.auth(res.data);
  158. this.authnService.navigate({});
  159. }
  160. });
  161. }
  162. // #region fields
  163. get userName(): AbstractControl {
  164. return this.form.get('userName')!;
  165. }
  166. get password(): AbstractControl {
  167. return this.form.get('password')!;
  168. }
  169. get mobile(): AbstractControl {
  170. return this.form.get('mobile')!;
  171. }
  172. get captcha(): AbstractControl {
  173. return this.form.get('captcha')!;
  174. }
  175. get otpCaptcha(): AbstractControl {
  176. return this.form.get('otpCaptcha')!;
  177. }
  178. get remember(): AbstractControl {
  179. return this.form.get('remember')!;
  180. }
  181. // #endregion
  182. // #region get captcha
  183. getImageCaptcha(): void {
  184. this.imageCaptchaService.captcha({ state: this.state, captcha: this.captchaType }).subscribe(res => {
  185. if (res.code == 0) {
  186. this.imageCaptcha = res.data.image;
  187. this.cdr.detectChanges();
  188. } else {
  189. //令牌失效时,重新刷新页面
  190. window.location.reload();
  191. }
  192. });
  193. }
  194. //send sms
  195. sendOtpCode(): void {
  196. if (this.mobile.invalid) {
  197. this.mobile.markAsDirty({ onlySelf: true });
  198. this.mobile.updateValueAndValidity({ onlySelf: true });
  199. return;
  200. }
  201. this.authnService.produceOtp({ mobile: this.mobile.value }).subscribe(res => {
  202. if (res.code !== 0) {
  203. this.msg.success(`发送失败`);
  204. }
  205. });
  206. this.count = 59;
  207. this.interval$ = setInterval(() => {
  208. this.count -= 1;
  209. if (this.count <= 0) {
  210. clearInterval(this.interval$);
  211. }
  212. this.cdr.detectChanges();
  213. }, 1000);
  214. }
  215. // #endregion
  216. submit(): void {
  217. this.error = '';
  218. if (this.loginType === 'normal') {
  219. this.userName.markAsDirty();
  220. this.userName.updateValueAndValidity();
  221. this.password.markAsDirty();
  222. this.password.updateValueAndValidity();
  223. this.captcha.markAsDirty();
  224. this.captcha.updateValueAndValidity();
  225. if (this.userName.invalid || this.password.invalid || this.captcha.invalid) {
  226. return;
  227. }
  228. } else {
  229. this.mobile.markAsDirty();
  230. this.mobile.updateValueAndValidity();
  231. this.otpCaptcha.markAsDirty();
  232. this.otpCaptcha.updateValueAndValidity();
  233. if (this.mobile.invalid || this.otpCaptcha.invalid) {
  234. return;
  235. }
  236. }
  237. localStorage.setItem(CONSTS.REMEMBER, this.form.get(CONSTS.REMEMBER)?.value);
  238. this.loading = true;
  239. this.cdr.detectChanges();
  240. this.authnService
  241. .login({
  242. authType: this.loginType,
  243. state: this.state,
  244. username: this.userName.value,
  245. password: this.password.value,
  246. captcha: this.captcha.value,
  247. mobile: this.mobile.value,
  248. otpCaptcha: this.otpCaptcha.value,
  249. remeberMe: this.remember.value
  250. })
  251. .pipe(
  252. finalize(() => {
  253. this.loading = false;
  254. this.cdr.detectChanges();
  255. })
  256. )
  257. .subscribe(res => {
  258. this.loading = true;
  259. if (res.code !== 0) {
  260. this.error = res.message;
  261. //this.msg.error(this.error);
  262. if (this.loginType === 'normal') {
  263. this.getImageCaptcha();
  264. }
  265. } else {
  266. // 清空路由复用信息
  267. this.reuseTabService.clear();
  268. if (res.data.twoFactor === '0') {
  269. // 设置用户Token信息
  270. this.authnService.auth(res.data);
  271. this.authnService.navigate({});
  272. } else {
  273. localStorage.setItem(CONSTS.TWO_FACTOR_DATA, JSON.stringify(res.data));
  274. this.router.navigateByUrl('/passport/tfa');
  275. }
  276. }
  277. this.cdr.detectChanges();
  278. });
  279. }
  280. // #region social
  281. socialauth(provider: string): void {
  282. this.authnService.clearUser();
  283. this.socialsProviderService.authorize(provider).subscribe(res => {
  284. //console.log(res.data);
  285. window.location.href = res.data;
  286. });
  287. }
  288. /**
  289. * 获取二维码
  290. */
  291. getScanQrCode() {
  292. this.authnService.clearUser();
  293. console.log(`qrScan : ${this.socials.qrScan}`);
  294. if (this.socials.qrScan === 'workweixin' || this.socials.qrScan === 'dingtalk' || this.socials.qrScan === 'feishu') {
  295. this.socialsProviderService.scanqrcode(this.socials.qrScan).subscribe(res => {
  296. if (res.code === 0) {
  297. if (this.socials.qrScan === 'workweixin') {
  298. this.qrScanWorkweixin(res.data);
  299. } else if (this.socials.qrScan === 'dingtalk') {
  300. this.qrScanDingtalk(res.data);
  301. } else if (this.socials.qrScan === 'feishu') {
  302. this.qrScanFeishu(res.data);
  303. }
  304. }
  305. });
  306. } else {
  307. this.qrexpire = false;
  308. if (this.interval$) {
  309. clearInterval(this.interval$);
  310. }
  311. this.qrCodeService.getLoginQrCode().subscribe(res => {
  312. if (res.code === 0 && res.data.rqCode) {
  313. // 使用返回的 rqCode
  314. const qrImageElement = document.getElementById('div_qrcodelogin');
  315. this.ticket = res.data.ticket;
  316. if (qrImageElement) {
  317. qrImageElement.innerHTML = `<img src="${res.data.rqCode}" alt="QR Code" style="width: 200px; height: 200px;">`;
  318. }
  319. /* // 设置5分钟后 qrexpire 为 false
  320. setTimeout(() => {
  321. this.qrexpire = true;
  322. this.cdr.detectChanges(); // 更新视图
  323. }, 5 * 60 * 1000); // 5 分钟*/
  324. this.scanQrCodeLogin();
  325. }
  326. });
  327. }
  328. }
  329. /**
  330. * 二维码轮询登录
  331. */
  332. scanQrCodeLogin() {
  333. const interval = setInterval(() => {
  334. this.qrCodeService
  335. .loginByQrCode({
  336. authType: 'scancode',
  337. code: this.ticket,
  338. state: this.state
  339. })
  340. .subscribe(res => {
  341. if (res.code === 0) {
  342. this.qrexpire = true;
  343. // 清空路由复用信息
  344. this.reuseTabService.clear();
  345. // 设置用户Token信息
  346. this.authnService.auth(res.data);
  347. this.authnService.navigate({});
  348. } else if (res.code === 20004) {
  349. this.qrexpire = true;
  350. } else if (res.code === 20005) {
  351. this.get();
  352. }
  353. // Handle response here
  354. // If you need to stop the interval after a certain condition is met,
  355. // you can clear the interval like this:
  356. if (this.qrexpire) {
  357. clearInterval(interval);
  358. }
  359. this.cdr.detectChanges(); // 更新视图
  360. });
  361. }, 5 * 1000); // 5 seconds
  362. }
  363. // #endregion
  364. ngOnDestroy(): void {
  365. if (this.interval$) {
  366. clearInterval(this.interval$);
  367. }
  368. }
  369. // #region QR Scan for workweixin, dingtalk ,feishu
  370. qrScanWorkweixin(data: any) {
  371. //see doc https://developer.work.weixin.qq.com/document/path/91025
  372. // @ts-ignore
  373. let wwLogin = new WwLogin({
  374. id: 'div_qrcodelogin',
  375. appid: data.clientId,
  376. agentid: data.agentId,
  377. redirect_uri: encodeURIComponent(data.redirectUri),
  378. state: data.state,
  379. href: 'data:text/css;base64,LmltcG93ZXJCb3ggLnFyY29kZSB7d2lkdGg6IDI1MHB4O30NCi5pbXBvd2VyQm94IC50aXRsZSB7ZGlzcGxheTogbm9uZTt9DQouaW1wb3dlckJveCAuaW5mbyB7d2lkdGg6IDI1MHB4O30NCi5zdGF0dXNfaWNvbiB7ZGlzcGxheTpub25lfQ0KLmltcG93ZXJCb3ggLnN0YXR1cyB7dGV4dC1hbGlnbjogY2VudGVyO30='
  380. });
  381. }
  382. qrScanFeishu(data: any) {
  383. //see doc https://open.feishu.cn/document/common-capabilities/sso/web-application-sso/qr-sdk-documentation
  384. //remove old div
  385. var qrcodeDiv = document.querySelector('#div_qrcodelogin');
  386. qrcodeDiv?.childNodes.forEach(function (value, index, array) {
  387. qrcodeDiv?.removeChild(value);
  388. });
  389. // @ts-ignore
  390. fsredirectUri = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${data.clientId}&redirect_uri=${encodeURIComponent(
  391. data.redirectUri
  392. )}&response_type=code&state=${data.state}`;
  393. // @ts-ignore
  394. var redirectUri = fsredirectUri;
  395. // @ts-ignore
  396. QRLoginObj = QRLogin({
  397. id: 'div_qrcodelogin',
  398. goto: redirectUri,
  399. width: '300',
  400. height: '300',
  401. style: 'border: 0;'
  402. });
  403. }
  404. qrScanDingtalk(data: any) {
  405. //see doc https://open.dingtalk.com/document/isvapp-server/scan-qr-code-to-log-on-to-third-party-websites
  406. var url = encodeURIComponent(data.redirectUri);
  407. var gotodingtalk = encodeURIComponent(
  408. `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${data.clientId}&response_type=code&scope=snsapi_login&state=${data.state}&redirect_uri=${url}`
  409. );
  410. // @ts-ignore
  411. ddredirect_uri = `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${data.clientId}&response_type=code&scope=snsapi_login&state=${data.state}&redirect_uri=${data.redirectUri}`;
  412. // @ts-ignore
  413. var obj = DDLogin({
  414. id: 'div_qrcodelogin', //这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
  415. goto: gotodingtalk, //请参考注释里的方式
  416. style: 'border:none;background-color:#FFFFFF;',
  417. width: '360',
  418. height: '400'
  419. });
  420. }
  421. // #region QR Scan end
  422. qrScanMaxkey(data: any) {
  423. // @ts-ignore
  424. document.getElementById('div_qrcodelogin').innerHTML = '';
  425. // @ts-ignore
  426. var qrcode = new QRCode('div_qrcodelogin', {
  427. width: 200,
  428. height: 200,
  429. colorDark: '#000000',
  430. colorLight: '#ffffff'
  431. }).makeCode(data.state);
  432. //3分钟监听二维码
  433. this.count = 90;
  434. this.interval$ = setInterval(() => {
  435. this.count -= 1;
  436. if (this.loginType != 'qrscan') {
  437. clearInterval(this.interval$);
  438. }
  439. if (this.count <= 0) {
  440. clearInterval(this.interval$);
  441. }
  442. //轮询发送监听请求
  443. this.socialsProviderService.qrcallback(this.socials.qrScan, data.state).subscribe(res => {
  444. if (res.code === 0) {
  445. // 清空路由复用信息
  446. this.reuseTabService.clear();
  447. // 设置用户Token信息
  448. this.authnService.auth(res.data);
  449. this.authnService.navigate({});
  450. clearInterval(this.interval$);
  451. } else if (res.code === 102) {
  452. // 二维码过期
  453. clearInterval(this.interval$);
  454. this.qrexpire = true;
  455. this.cdr.detectChanges();
  456. } else if (res.code === 103) {
  457. // 暂无用户扫码
  458. }
  459. });
  460. this.cdr.detectChanges();
  461. }, 2000);
  462. }
  463. /**
  464. * Passkey 无用户名登录
  465. */
  466. async passkeyLogin(): Promise<void> {
  467. console.log('=== PASSKEY LOGIN DEBUG START ===');
  468. console.log('Passkey usernameless login clicked at:', new Date().toISOString());
  469. try {
  470. // 检查浏览器是否支持 WebAuthn
  471. if (!window.PublicKeyCredential) {
  472. console.error('WebAuthn not supported');
  473. this.msg.error('您的浏览器不支持 WebAuthn/Passkey 功能');
  474. return;
  475. }
  476. console.log('WebAuthn support confirmed');
  477. this.loading = true;
  478. this.cdr.detectChanges();
  479. // 1. 调用后端 API 获取认证选项(不传递任何用户信息)
  480. console.log('Step 1: Requesting authentication options from backend...');
  481. let authOptionsResponse;
  482. try {
  483. authOptionsResponse = await this.http.post<any>('/passkey/authentication/begin?_allow_anonymous=true', {}).toPromise();
  484. } catch (httpError: any) {
  485. console.error('HTTP error occurred:', httpError);
  486. // 处理HTTP错误,提取错误信息
  487. let errorMessage = '获取认证选项失败';
  488. if (httpError.error && httpError.error.message) {
  489. errorMessage = httpError.error.message;
  490. } else if (httpError.message) {
  491. errorMessage = httpError.message;
  492. }
  493. // 检查是否是没有注册 Passkey 的错误
  494. if (errorMessage.includes('没有注册任何 Passkey') ||
  495. errorMessage.includes('No Passkeys registered') ||
  496. errorMessage.includes('还没有注册任何 Passkey') ||
  497. errorMessage.includes('系统中还没有注册任何 Passkey')) {
  498. // 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获
  499. this.msg.warning('还未注册 Passkey,请注册 Passkey');
  500. console.log('=== PASSKEY LOGIN DEBUG END ===');
  501. return;
  502. }
  503. throw new Error(errorMessage);
  504. }
  505. console.log('Backend auth options response:', authOptionsResponse);
  506. if (!authOptionsResponse || authOptionsResponse.code !== 0) {
  507. console.error('Failed to get auth options:', authOptionsResponse);
  508. // 检查是否是没有注册 Passkey 的错误
  509. const errorMessage = authOptionsResponse?.message || '获取认证选项失败';
  510. if (errorMessage.includes('没有注册任何 Passkey') ||
  511. errorMessage.includes('No Passkeys registered') ||
  512. errorMessage.includes('还没有注册任何 Passkey') ||
  513. errorMessage.includes('系统中还没有注册任何 Passkey')) {
  514. // 直接显示友好提示并返回,不抛出错误避免被全局拦截器捕获
  515. this.msg.warning('还未注册 Passkey,请注册 Passkey');
  516. console.log('=== PASSKEY LOGIN DEBUG END ===');
  517. return;
  518. }
  519. throw new Error(errorMessage);
  520. }
  521. const authOptions = authOptionsResponse.data;
  522. console.log('Auth options received:', authOptions);
  523. // 检查返回的数据是否有效
  524. if (!authOptions || !authOptions.challenge) {
  525. console.error('Invalid auth options:', authOptions);
  526. throw new Error('服务器返回的认证选项无效');
  527. }
  528. // 2. 转换认证选项格式
  529. console.log('Step 2: Converting authentication options...');
  530. const convertedOptions: PublicKeyCredentialRequestOptions = {
  531. challenge: this.base64ToArrayBuffer(authOptions.challenge),
  532. timeout: authOptions.timeout || 60000,
  533. rpId: authOptions.rpId,
  534. userVerification: authOptions.userVerification || 'preferred'
  535. // 注意:不设置 allowCredentials,让认证器自动选择可用的凭据
  536. };
  537. console.log('Converted options:', {
  538. challengeLength: convertedOptions.challenge.byteLength,
  539. timeout: convertedOptions.timeout,
  540. rpId: convertedOptions.rpId,
  541. userVerification: convertedOptions.userVerification,
  542. allowCredentials: convertedOptions.allowCredentials || 'undefined (auto-select)'
  543. });
  544. // 3. 调用 WebAuthn API 进行认证
  545. console.log('Step 3: Calling WebAuthn API navigator.credentials.get()...');
  546. console.log('Available authenticators will be queried automatically');
  547. const credential = await navigator.credentials.get({
  548. publicKey: convertedOptions
  549. }) as PublicKeyCredential;
  550. if (!credential) {
  551. console.error('No credential returned from WebAuthn API');
  552. throw new Error('认证失败');
  553. }
  554. console.log('=== CREDENTIAL DEBUG INFO ===');
  555. console.log('Credential ID:', credential.id);
  556. console.log('Credential ID length:', credential.id.length);
  557. console.log('Credential type:', credential.type);
  558. console.log('Credential rawId length:', credential.rawId.byteLength);
  559. console.log('Credential rawId as base64:', this.arrayBufferToBase64(credential.rawId));
  560. // 验证 credential.id 和 rawId 的一致性
  561. const rawIdBase64 = this.arrayBufferToBase64(credential.rawId);
  562. console.log('ID consistency check:');
  563. console.log(' credential.id:', credential.id);
  564. console.log(' rawId as base64:', rawIdBase64);
  565. console.log(' IDs match:', credential.id === rawIdBase64);
  566. const credentialResponse = credential.response as AuthenticatorAssertionResponse;
  567. console.log('Authenticator response type:', credentialResponse.constructor.name);
  568. console.log('User handle:', credentialResponse.userHandle ? this.arrayBufferToBase64(credentialResponse.userHandle) : 'null');
  569. console.log('=== END CREDENTIAL DEBUG INFO ===');
  570. // 4. 将认证结果发送到后端验证
  571. console.log('Step 4: Sending credential to backend for verification...');
  572. const requestPayload = {
  573. challengeId: authOptions.challengeId,
  574. credentialId: credential.id,
  575. authenticatorData: this.arrayBufferToBase64(credentialResponse.authenticatorData),
  576. clientDataJSON: this.arrayBufferToBase64(credentialResponse.clientDataJSON),
  577. signature: this.arrayBufferToBase64(credentialResponse.signature),
  578. userHandle: credentialResponse.userHandle ? this.arrayBufferToBase64(credentialResponse.userHandle) : null
  579. };
  580. console.log('Request payload to backend:', {
  581. challengeId: requestPayload.challengeId,
  582. credentialId: requestPayload.credentialId,
  583. credentialIdLength: requestPayload.credentialId.length,
  584. authenticatorDataLength: requestPayload.authenticatorData.length,
  585. clientDataJSONLength: requestPayload.clientDataJSON.length,
  586. signatureLength: requestPayload.signature.length,
  587. userHandle: requestPayload.userHandle
  588. });
  589. const finishResponse = await this.http.post<any>('/passkey/authentication/finish?_allow_anonymous=true', requestPayload).toPromise();
  590. console.log('Backend finish response:', finishResponse);
  591. if (!finishResponse || finishResponse.code !== 0) {
  592. console.error('Backend verification failed:', finishResponse);
  593. throw new Error(finishResponse?.message || 'Passkey认证失败');
  594. }
  595. // 5. 认证成功,设置用户信息并跳转
  596. console.log('Step 5: Authentication successful, setting user info...');
  597. const authResult = finishResponse.data;
  598. console.log('Auth result received:', authResult);
  599. this.msg.success(`Passkey 登录成功!欢迎 ${authResult.username || '用户'}`);
  600. // 清空路由复用信息
  601. console.log('Clearing reuse tab service...');
  602. this.reuseTabService.clear();
  603. // 设置用户Token信息
  604. if (authResult && authResult.userId) {
  605. console.log('Valid auth result with userId:', authResult.userId);
  606. // 构建完整的认证信息对象,包含 SimpleGuard 所需的 token 和 ticket
  607. const userInfo = {
  608. id: authResult.userId,
  609. userId: authResult.userId,
  610. username: authResult.username,
  611. displayName: authResult.displayName || authResult.username,
  612. email: authResult.email || '',
  613. authTime: authResult.authTime,
  614. authType: 'passkey',
  615. // 关键:包含认证所需的 token 和 ticket
  616. token: authResult.token || authResult.congress || '',
  617. ticket: authResult.ticket || authResult.onlineTicket || '',
  618. // 其他可能需要的字段
  619. remeberMe: false,
  620. passwordSetType: authResult.passwordSetType || 'normal',
  621. authorities: authResult.authorities || []
  622. };
  623. console.log('Setting auth info:', userInfo);
  624. // 设置认证信息
  625. this.authnService.auth(userInfo);
  626. // 使用 navigate 方法进行跳转,它会处理 StartupService 的重新加载
  627. console.log('Navigating with auth result...');
  628. this.authnService.navigate(authResult);
  629. console.log('=== PASSKEY LOGIN SUCCESS ===');
  630. } else {
  631. console.error('Invalid auth result - missing userId:', authResult);
  632. throw new Error('认证成功但用户数据无效');
  633. }
  634. } catch (error: any) {
  635. console.error('=== PASSKEY LOGIN ERROR ===');
  636. console.error('Error type:', error.constructor.name);
  637. console.error('Error message:', error.message);
  638. console.error('Error stack:', error.stack);
  639. console.error('Full error object:', error);
  640. // 检查是否是没有注册 Passkey 的错误
  641. if (error.message && (error.message.includes('PASSKEY_NOT_REGISTERED') ||
  642. error.message.includes('没有找到可用的凭据') ||
  643. error.message.includes('No credentials available') ||
  644. error.message.includes('用户未注册') ||
  645. error.message.includes('credential not found') ||
  646. error.message.includes('没有注册任何 Passkey') ||
  647. error.message.includes('No Passkeys registered') ||
  648. error.message.includes('还没有注册任何 Passkey'))) {
  649. this.msg.warning('还未注册 Passkey,请注册 Passkey');
  650. console.log('=== PASSKEY LOGIN DEBUG END ===');
  651. return;
  652. }
  653. // 如果是 WebAuthn 相关错误,提供更详细的信息
  654. if (error.name) {
  655. console.error('WebAuthn error name:', error.name);
  656. switch (error.name) {
  657. case 'NotAllowedError':
  658. console.error('User cancelled the operation or timeout occurred');
  659. // 检查是否是因为没有可用凭据导致的取消
  660. this.msg.warning('Passkey 登录已取消。如果您还没有注册 Passkey,请先注册后再使用');
  661. break;
  662. case 'SecurityError':
  663. console.error('Security error - invalid domain or HTTPS required');
  664. this.msg.error('安全错误:请确保在 HTTPS 环境下使用 Passkey 功能');
  665. break;
  666. case 'NotSupportedError':
  667. console.error('Operation not supported by authenticator');
  668. this.msg.error('您的设备不支持 Passkey 功能');
  669. break;
  670. case 'InvalidStateError':
  671. console.error('Authenticator is in invalid state');
  672. this.msg.error('认证器状态异常,请重试');
  673. break;
  674. case 'ConstraintError':
  675. console.error('Constraint error in authenticator');
  676. this.msg.error('认证器约束错误,请重试');
  677. break;
  678. default:
  679. console.error('Unknown WebAuthn error');
  680. this.msg.error('Passkey 登录失败:' + (error.message || '请重试或使用其他登录方式'));
  681. }
  682. } else {
  683. this.msg.error('Passkey 登录失败:' + (error.message || '请重试或使用其他登录方式'));
  684. }
  685. console.log('=== PASSKEY LOGIN DEBUG END ===');
  686. } finally {
  687. this.loading = false;
  688. this.cdr.detectChanges();
  689. console.log('Login loading state reset');
  690. }
  691. }
  692. // 添加辅助方法
  693. private base64ToArrayBuffer(base64: string): ArrayBuffer {
  694. const binaryString = atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
  695. const bytes = new Uint8Array(binaryString.length);
  696. for (let i = 0; i < binaryString.length; i++) {
  697. bytes[i] = binaryString.charCodeAt(i);
  698. }
  699. return bytes.buffer;
  700. }
  701. private arrayBufferToBase64(buffer: ArrayBuffer): string {
  702. const bytes = new Uint8Array(buffer);
  703. let binary = '';
  704. for (let i = 0; i < bytes.byteLength; i++) {
  705. binary += String.fromCharCode(bytes[i]);
  706. }
  707. return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  708. }
  709. }