流程

- 導向至 Google 登入頁面。
- 接收來自 Google 的回應。
- 以
code
取得 Token。 - 以
access_token
取得使用者資訊。 - 以使用者資訊註冊或登入
一、設定 Google OAuth 2.0
- 登入 Google API Console。
- 「選取專案」或「新增專案」。
- 打開控制台左側選單選擇「API 和服務」。
- 設定「OAuth 同意畫面」,建立測試使用者。
- 選擇「憑證」,點擊「建立憑證」,然後選擇「OAuth 用戶端 ID」。
- 完成「建立 OAuth 用戶端 ID」,取得「用戶端 ID」與「用戶端密碼」。
二、導向 Google 登入頁面

- 使用者按下按鈕,取得 Google 登入頁面路徑,導向至 Google 登入頁面。
redirectToSignIn() {
this._http.get(this._env.APIEndpoint.OAuthGoogle, { responseType: 'text' })
.subscribe(res => window.location.href = res);
}
- 設定參數,取得含有參數的 Google 登入頁面路徑。
- 使用者登入 Google,並選擇是否同意授權應用程式(我們的系統)取得
scope
範圍內的個人資訊。 - Google 將使用者是否授權登入的回應,回傳至
redirect_uri
。 - 隨機產生
state
儲存至 HttpOnly Cookies 中,在 Google 回傳回應時驗證是否一致,以防止 CSRF 攻擊 。
[HttpGet]
public string GetSignInUrl()
{
var oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth";
var redirectUri = "http://localhost:4200/login";
var scope = "https://www.googleapis.com/auth/userinfo.profile";
var state = GetRandomString();
SetCookies(Response, "google_state", state);
var queryParams = new Dictionary<string, string>
{
{"client_id", _clientId},
{"redirect_uri", redirectUri},
{"scope", scope},
{"state", state},
{"include_granted_scopes", "true"},
{"response_type", "code"}
};
var queryString = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={kvp.Value}"));
return $"{oauth2Endpoint}?{queryString}";
}
private string GetRandomString()
{
byte[] randomValues = new byte[8];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomValues);
}
string utf8String = Encoding.UTF8.GetString(randomValues);
string base64Encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(utf8String))
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
return base64Encoded;
}
// inject IHostEnvironment in constructor
private readonly IHostEnvironment _env;
private void SetCookies(HttpResponse res, string key, string value)
{
if (_env.IsProduction())
{
res.Cookies.Append(key, value, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict
});
}
else
{
res.Cookies.Append(key, value, new CookieOptions
{
HttpOnly = true
});
}
}
三、Google登入回應處理
redirect_uri
Google登入回應端點,接收參數:- 使用者同意授權並登入成功,則取得
code
、state
參數。 - 使用者拒絕授權或登入失敗,則取得
error
參數。
- 驗證回傳的
state
是否與發送請求時一致,以防CSRF攻擊。 - Google登入成功後,以
code
取得 access_token
,以 access_token
取得使用者資訊;再登入或註冊使用者。
handleSignInCallback() {
const params = new URLSearchParams(location.search);
if(params.size) {
const code = params.get("code");
const state = params.get("state");
const error = params.get("error");
if(error != "access_denied" && code && state) {
const data = new FormData();
data.append('code', code);
data.append('state', state);
this._http.post(this._env.APIEndpoint.OAuthGoogle, data).subscribe(res => {
localStorage.setItem('user', JSON.stringify(res));
this._router.navigateByUrl('/');
});
} else console.error("Google登入失敗。");
}
}
[HttpPost]
public async Task<IActionResult> GetAuthenticateUser([FromForm] string code, [FromForm] string state)
{
try
{
// 驗證state是否一致
var storageState = Request.Cookies["state"];
if (code.IsNullOrEmpty() || state.IsNullOrEmpty() || state != storageState)
throw new Exception();
Response.Cookies.Delete("state");
// 取得Token
var res = await ExchangeAuthorizationCodeAsync(code!);
var accessToken = res?.AccessToken ?? throw new Exception();
// 取得使用者資訊
var profile = await GetUserProfile(accessToken);
var email = profile?.Email;
if (profile!= null && email != null)
{
// 以信箱從資料庫取得使用者
var user = await _userService.GetByEmailAsync(email);
if (user == null)
{
// 若使用者不存在則建立新使用者(註冊)
AppUser newUser = new();
await _userService.InsetAsync(newUser);
return Ok(newUser);
}
// 使用者已存在(登入)
return Ok(user);
}
throw new Exception("無法取得使用者資訊。");
}
catch (Exception ex)
{
Log.Error(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
取得Token
- 以
code
請求取得 Token。 - 取得 Token 的
redirect_uri
需與導向至Google登入頁面的 redirect_uri
一致,否則會出現錯誤 redirect_uri_mismatch
。
public async Task<TokenResponse?> ExchangeAuthorizationCodeAsync(string code)
{
var tokenEndpoint = "https://oauth2.googleapis.com/token";
var redirectUri = "http://localhost:7300/api/OAuth/Callback";
var requestBody = new Dictionary<string, string>
{
{ "client_id", _clientId },
{ "client_secret", _clientSecret },
{ "code", code },
{ "redirect_uri", redirectUri },
{ "grant_type", "authorization_code" }
};
var requestContent = new FormUrlEncodedContent(requestBody);
using HttpClient client = _httpClient.CreateClient();
var response = await client.PostAsync(tokenEndpoint, requestContent);
var resContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
var err = JsonSerializer.Deserialize<ErrorResponse>(resContent);
throw new Exception($"{err?.Error}:${err?.ErrorDescription}");
}
return JsonSerializer.Deserialize<TokenResponse>(resContent);
}
- Token
public class TokenResponse
{
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
[JsonPropertyName("token_type")]
public string? TokenType { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("refresh_token")]
public string? RefreshToken { get; set; }
[JsonPropertyName("scope")]
public string? Scope { get; set; }
}
取得使用者資訊
- 以
access_token
請求取得使用者資訊。
public async Task<UserProfile?> GetUserProfile(string accessToken)
{
var userInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo";
using HttpClient client = _httpClient.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync(userInfoEndpoint);
var resContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
var err = JsonSerializer.Deserialize<ErrorResponse>(resContent);
throw new Exception($"{err?.Error}:${err?.ErrorDescription}");
}
return JsonSerializer.Deserialize<UserProfile>(resContent);
}
- 使用者資訊
public class UserProfile
{
[JsonPropertyName("sub")]
public string? Sub { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("given_name")]
public string? GivenName { get; set; }
[JsonPropertyName("picture")]
public string? Picture { get; set; }
[JsonPropertyName("email")]
public string? Email { get; set; }
[JsonPropertyName("email_verified")]
public bool EmailVerified { get; set; } = false;
[JsonPropertyName("locale")]
public string? Locale { get; set; }
}
參考資料