iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
佛心分享-SideProject30

30天的旅程!從學習C#到開發小專案系列 第 21

DAY 21 - 取得ID Token後,User資料發送到後端驗證儲存

  • 分享至 

  • xImage
  •  

哈囉大家好!
昨天處理完User Table和Neon資料庫後,要把前端收到的Token和使用者資訊(例如:姓名、郵件...等)儲存到User Table裡面。
需要完成的部分有以下步驟:

  1. 成功登入後:從response.credential取得ID Token(JWT)。
  2. 發送到後端:使用Angular的HttpClient將這個ID Token發送到後端API。
  3. 後端驗證:後端程式碼必須使用Google官方的library(Google Auth Library)來驗證這個ID Token的簽名、發行者(iss)和受眾(aud, 即我的Client ID)。
  4. 發放Session: 驗證成功後,後端會發放一個新的、專屬於專案的Session Token或JWT給前端。
  5. 前端儲存:前端將專案專屬的Session Token儲存在HttpOnlyCookie或Local Storage中,並用於後續的API呼叫。

取得ID Token(JWT)

登入後,callback function handleCredentialResponse() 會接收到Google傳送的response,
將response.credential用之前定義好的decodeJWT()decode後就可以拿到JWT:

handleCredentialResponse(response: any): void {
   const responsePayload = this.decodeJWT(response.credential);
}

ID Token發送到後端

先定義一個ApiService:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../environments/environment.development';
import { TokenRequest } from '../interfaces/auth';

@Injectable({
  providedIn: 'root',
})
export class ApiServiceService {
  constructor(private http: HttpClient) {}

  saveIdToken(tokenRequest: TokenRequest) {
    const url = `${environment.apiBaseUrl}/auth/save-token`;
    return this.http.post(url, { tokenRequest });
  }
}

之後在LoginComponent的constructor注入這個服務,並呼叫儲存Id Token的function:

handleCredentialResponse(response: any): void {
    // 1. JWT處理
    const responsePayload = this.decodeJWT(response.credential);

    // 2. 將id token 存到local Storage或儲存到後端
    const tokenRequest: TokenRequest = {
      googleSubId: responsePayload.sub,
      email: responsePayload.email,
      name: responsePayload.name,
    };
    this.apiService.saveIdToken(tokenRequest).subscribe({
      next: (res) => {
        console.log('ID token saved successfully:', res);
        // 3. 登入狀態更新
        this.loggedIn = true;
        this.router.navigate(['/records']);
      },
      error: (err) => {
        console.error('Error saving ID token:', err);
        this.loggedIn = false;
      },
    });
  }

後端驗證

接著需要用Google官方的library來安全地驗證JWT ID Token,

  1. 執行下列指令來安裝:
dotnet add package Google.Apis.Auth
  1. 為了讓AuthController保持乾淨,要把所有的驗證邏輯放在可注入的服務AuthService中!
    先來定義從前端接收傳過來的資料DTO (Data Transfer Object):
using System.ComponentModel.DataAnnotations;

namespace GoDutchBackend.Models
{
    public class TokenRequest
    {
        [Required]
        public string GoogleSubId { get; set; } = string.Empty;
        [Required]
        public string Email { get; set; } = string.Empty;
        [Required]
        public string Name { get; set; } = string.Empty;
    }
}

接著再定義Interface IAuthService:

namespace GoDutchBackend.Services
{
    public interface IAuthService
    {
        Task<string?> AuthenticateGoogleUserAsync(string googleIdToken);
    }
}
  1. 完成AuthService.cs檔案中的Google驗證邏輯:
  • 在constructor中,從appsettings.json裡面讀取Client ID。
  • AuthenticateGoogleUserAsync() function中,會執行以下步驟:
    <1> 驗證前端傳送過來的JWT, 會自動檢查簽名、發行者(iss)、過期時間(exp)和受眾(aud)
    <2> 處理使用者的資料,若資料庫中沒有使用者的資料(即第一次登入),會在User table中新增紀錄。
    <3> 產生專案專屬的Session Token!會利用JWT library產生一個新的、效期較短的JWT作為新的Session。
    註:因為「產生專案專屬的session Token」這個部分內容比較多,所以會留到明天分享~
using GoDutch.Data;
using GoDutch.Models;
using Google.Apis.Auth;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace GoDutchBackend.Services
{
    public class AuthService : IAuthService
    {
        private readonly string _googleClientId;
        private readonly AppDbContext _dbContext;
        private readonly ILogger<AuthService> _logger;

        public AuthService(IConfiguration configuration, AppDbContext dbContext, ILogger<AuthService> logger)
        {
            // 從appsettings.json讀取 client ID
            _googleClientId = configuration["GoogleAuth:ClientId"] ?? throw new ArgumentNullException("Google Client ID is not configured.");
            _dbContext = dbContext;
            _logger = logger;
        }

        public async Task<string?> AuthenticateGoogleUserAsync(string googleIdToken)
        {
            try
            {
                // 1. 驗證google Id Token
                var payload = await GoogleJsonWebSignature.ValidateAsync(googleIdToken, new GoogleJsonWebSignature.ValidationSettings
                {
                    Audience = new[] { _googleClientId }
                });

                // 2. 處理使用者資料
                var subId = payload.Subject; // google User唯一不變id(GoogleSubId)
                var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.GoogleSubId == subId);
                if (user == null) // 第一次登入
                {
                    user = new User
                    {
                        GoogleSubId = subId,
                        Email = payload.Email,
                        Name = payload.Name,
                        CreatedAt = DateTime.Now
                    };
                    _dbContext.Users.Add(user);
                    await _dbContext.SaveChangesAsync();
                    _logger.LogInformation("New users created: {Email}", user.Email);
                }
                // 3. 產生專案專屬的session Token
                // 產生session token的部分會保留到明天!
                return $"temporary_fake_session_token";
            }
            catch (InvalidJwtException ex)
            {
                _logger.LogError(ex, "Invalid GoogleId Token.");
                return null;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "AuthService.AuthenticateGoogleUserAsync failed.");
                return null;
            }
        }
    }

上一篇
DAY 20 - 建立User Table + 連接Neon 資料庫
下一篇
DAY 22 - 生成Session Token並註冊驗證服務
系列文
30天的旅程!從學習C#到開發小專案24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言