使用 Google OAuth 2.0 作为 ASP.NET C# 中的用户登录 - 基本概述
将 Google OAuth 2.0 集成到 ASP.NET C# 中,实现无缝的用户身份验证
引言
使用 Google OAuth 登录可以实现很多功能。
然而,在本文中,我想重点介绍一个基本功能:使用 Google OAuth 来处理您网站的用户登录。
我们开始吧。
有关更详细的信息,请参阅 Google 官方文档
首先,在 Google API Console 中为您的网站注册并启用 Google API 访问。
Google API Console:https://console.developers.google.com/apis/library
第 1 步:创建一个新项目。
第 2 步:设置 OAuth 同意屏幕。
按照屏幕上的说明填写您的应用程序的详细信息。
第 3 步:作用域 (Scopes)
点击添加或移除作用域 (ADD OR REMOVE SCOPES)。在这里,我们将只选择一个作用域
../auth/userinfo.email
第 4 步:测试用户
添加一些用户用于测试。
第 5 步:创建凭据
导航到API 和服务 (APIs & Services) > 凭据 (Credentials) > 创建凭据 (CREATE CREDENTIALS) > OAuth 客户端 ID (OAuth client ID)
将应用程序类型选择为Web 应用程序 (Web application)。
填写已授权的 JavaScript 来源 (Authorized JavaScript origins) 和已授权的重定向 URI (Authorized redirect URIs)。
这两个 URL 至关重要。请确保稍后在代码中使用这些确切的 URL。
已授权的重定向 URI (Authorized redirect URIs) 应该是 Google API 在成功登录后将值返回到您网站的确切目标 URL。
第 6 步:获取客户端 ID 和客户端密钥
创建 OAuth 客户端后,复制客户端 ID (Client ID) 和客户端密钥 (Client Secret),您稍后将在代码中使用它们。
构建网站
要开始 Google OAuth 登录,请首先准备以下参数
access_type=”online”
(此选项可以更改为“offline”,稍后将在文章中讨论)client_id= <您的 Google 客户端 ID>
– 在 Google API 注册期间获得redirect_uri=
Google 登录后重定向的目标页面 – 与您在 Google API 上注册的完全相同response_type=”code”
scope=”email”
– 在 Google API 注册期间定义的允许的作用域prompt=”consent”
– 告知用户您的应用程序正在请求的权限login_hint
– (可选) 如果您的应用程序知道哪个用户正在尝试进行身份验证,您可以使用此参数向 Google 身份验证服务器提供提示。服务器使用此提示简化登录流程,可以通过预填充登录表单中的电子邮件字段,或通过选择适当的多登录会话。
上述参数将需要传递给 Google OAuth 登录页面或 API 端点,该端点为
https://#/o/oauth2/v2/auth
有多种方式可以发起 Google OAuth 登录。最简单的方法之一是在您的登录页面上放置一个链接,将用户重定向到 Google OAuth 登录页面。将所有参数作为查询字符串附加。
使用 HTML <a>
标签 (为便于文档显示换行)
<a href="https://#/o/oauth2/v2/auth?
access_type=online
&client_id=xxxxx
&redirect_uri=https%3A//mywebsite.com/oauth
&response_type=code
&scope=email
&prompt=consent">Sign In With Google</a>
或者,使用 JavaScript
<button type="button" onclick="signInWithGoogle();">Sign In With Google</button>
<script>
function signInWithGoogle() {
const clientId = 'xxxxxxxxxxxxxxxxxxxxxxxx';
const redirectUri = encodeURIComponent('https://mywebsite.com/oauth');
const url = `https://#/o/oauth2/v2/auth?
access_type=online
&client_id=${clientId}
&redirect_uri=${redirectUri}
&response_type=code
&scope=email
&prompt=consent`;
window.location.href = url;
}
</script>
或者,使用 C# 从后端进行重定向 (为便于文档显示换行)
public static void SignInWithGoogle()
{
string clientId = "xxxxxxxxxxxxxxxxxxxxxxxx";
string redirectUri = HttpUtility.UrlEncode("https://mywebsite.com/oauth");
string url = $@"https://#/o/oauth2/v2/auth?
access_type=online
&client_id={clientId}
&redirect_uri={redirectUri}
&response_type=code
&scope=email
&prompt=consent";
HttpContext.Current.Response.Redirect(url, true);
}
获取授权码
用户成功登录 Google 登录页面后,Google 会将其重定向回您的网站,并附带一些参数 (查询字符串)。
目标重定向 URL 的示例如下
https://mywebsite.com/oauth
or
https://mywebsite.com/login
在 ASP.NET WebForms 中,物理页面以 .aspx 扩展名结尾,物理路径可能如下所示
https://mywebsite.com/oauth.aspx
or
https://mywebsite.com/pages/user/login/google-login.aspx
可以进行路由。创建或打开 Global.asax 文件并添加 URL 路由
void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapPageRoute("oauth", "oauth", "~/oauth.aspx");
// or
RouteTable.Routes.MapPageRoute("google-login",
"pages/user/login/google-login", "~/oauth.aspx");
}
我写了一篇关于一次性路由所有页面的文章,链接如下
https://adriancs.com/aspnet-webforms/419/automatic-route-all-pages-in-asp-net-webforms/
**注意:使用路由对于 Google API 工作不是必需的;如果愿意,您仍然可以使用绝对文件路径。例如
https://mywebsite.com/login.aspx
https://mywebsite.com/oauth.aspx
https://mywebsite.com/google-login.aspx
以下参数将作为查询字符串一起返回到您的网站
code
= <授权码>scope
= <您允许从 Google 用户访问的数据>authuser
= <当前浏览器中登录用户的索引号>prompt = “consent”
<告知用户您的应用正在请求的权限>
完整 URL 的示例如下 (为便于文档显示换行)
https://mywebsite.com/oauth?
code=xxxxxxx
&scope=email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid
&authuser=0
&prompt=consent
在后端获取授权码
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
if (Request.QueryString["code"] != null)
{
string authorizationCode = Request.QueryString["code"] + "";
}
}
}
获取 OAuth 2.0 访问令牌
准备以下参数
client_id
= 您的 Google 客户端 IDclient_secret
= 您的 Google 客户端密钥code
= 上一步获得的授权码grant_type = “authorization_code”
(固定字符串)redirect_uri
= 登录 Google 时使用的重定向 URLaccess_type = “online”
(固定字符串)
**注意:access_type 还有一个选项值,即“offline”。这将在本文的最后部分讨论。
这些参数将发送到 Google 的 OAuth 2.0 端点以获取访问令牌 (Access Token)
https://oauth2.googleapis.com/token
可以使用 POST
或 GET
请求发送这些参数。
发送 POST
请求的示例
using System.Net.Http;
public async Task GetAccessTokenAsync()
{
string url = "https://oauth2.googleapis.com/token";
var dicData = new Dictionary<string, string>();
dicData["client_id"] = google_api_client_id;
dicData["client_secret"] = google_api_client_secret;
dicData["code"] = authorization_code;
dicData["grant_type"] = "authorization_code";
dicData["redirect_uri"] = google_api_redirect_url;
dicData["access_type"] = "online";
try
{
using (var client = new HttpClient())
using (var content = new FormUrlEncodedContent(dicData))
{
HttpResponseMessage response = await client.PostAsync(url, content);
string json = await response.Content.ReadAsStringAsync();
}
}
catch (Exception ex)
{
// error
}
}
发送 GET
请求的示例 (使用 string
插值构建的 URL)
public async Task GetAccessTokenAsync()
{
string baseUrl = "https://oauth2.googleapis.com/token";
string encodedRedirectUri = HttpUtility.UrlEncode(google_api_redirect_url);
string urlWithParameters = $"{baseUrl}?client_id={google_api_client_id}&
client_secret={google_api_client_secret}&code={authorizationCode}&
grant_type=authorization_code&redirect_uri={encodedRedirectUri}&access_type=online";
string json = "";
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(urlWithParameters);
json = await response.Content.ReadAsStringAsync();
}
}
发送 GET
请求的示例 (使用 Dictionary
/NameValueCollection
构建的 URL)
var dicData = new Dictionary<string, string>()
{
{ "client_id", google_api_client_id },
{ "client_secret", google_api_client_secret },
{ "code", authorizationCode },
{ "grant_type", "authorization_code" },
{ "redirect_uri", google_api_redirect_url },
{ "access_type", "online" }
};
string baseUrl = "https://oauth2.googleapis.com/token";
var query = HttpUtility.ParseQueryString(string.Empty);
foreach (var pair in dicData)
{
query[pair.Key] = pair.Value;
}
string urlWithParameters = $"{baseUrl}?{query.ToString()}";
string json = "";
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(urlWithParameters);
json = await response.Content.ReadAsStringAsync();
}
Google 将以 JSON 格式返回结果。
成功请求的示例
{
"access_token": "xxxxxxxxxxx",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/userinfo.email openid",
"token_type": "Bearer",
"id_token": "xxxxxxxxxxxxxxxx"
}
错误请求的示例
// example 1:
{
"error": "invalid_grant",
"error_description": "Bad Request"
}
// example 2:
{
"error": "redirect_uri_mismatch",
"error_description": "Bad Request"
}
构建 C# 类对象以存储响应信息
public class OAuthTokenResponse
{
public string access_token { get; set; }
public int expires_in { get; set; }
public string refresh_token { get; set; }
public string scope { get; set; }
public string token_type { get; set; }
public string id_token { get; set; }
public string error { get; set; }
public string error_description { get; set; }
public bool IsSuccess => string.IsNullOrEmpty(error);
}
然后,使用 System.Text.Json
将 JSON 转换为类对象
using System.Text.Json;
OAuthTokenResponse tokenResponse = JsonSerializer.Deserialize<OAuthTokenResponse>(json);
string AccessToken = "";
if (tokenResponse.IsSuccess)
{
// success
AccessToken = tokenResponse.access_token;
}
else
{
// error
}
访问令牌已获得。您可以使用此令牌访问或保存用户 Google 帐户中的数据,例如读取或发送电子邮件、访问或保存日历事件、获取 Google Drive 文件列表等。
在本文中,我们只关心获取用户的电子邮件地址。访问令牌的有效期为一小时,这足以从 Google API 检索用户的电子邮件地址。在我们的例子中,访问令牌将只使用一次。
下一步:使用 AccessToken 访问另一个 Google API 端点以获取用户的电子邮件。
此操作使用 GET
请求完成。
使用 Authorization 请求头的 GET
请求示例 (推荐,更安全)
using System.Net.Http;
using System.Net.Http.Headers;
public async Task GetEmail()
{
string json = "";
string url = $"https://www.googleapis.com/oauth2/v2/userinfo?fields=email";
using (var client = new HttpClient())
{
// set the access token at the request header
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", AccessToken);
HttpResponseMessage response = await client.GetAsync(url);
json = await response.Content.ReadAsStringAsync();
}
}
使用查询字符串中发送的访问令牌的 GET
请求示例 (不安全)
public async Task GetEmail()
{
string json = "";
string url = $"https://www.googleapis.com/oauth2/v2/userinfo?fields=email&oauth_token={AccessToken}";
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(url);
json = await response.Content.ReadAsStringAsync();
}
}
JSON 的返回内容
成功请求的示例
{
"email": "somebody@gmail.com"
}
失败请求的示例
{
"error": {
"code": 401,
"message": "Request is missing required authentication credential.
Expected OAuth 2 access token, login cookie or other valid authentication credential.
See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
结合两个 JSON 结果,我们可以创建一个单一的 C# 类对象来处理这两种情况
public class ApiEmailResponse
{
public string email { get; set; }
public ApiError error { get; set; }
public bool IsSuccess => error == null;
public class ApiError
{
public int code { get; set; }
public string message { get; set; }
public string status { get; set; }
}
}
将 JSON 转换为类对象
ApiResponse emailResponse = JsonSerializer.Deserialize<ApiEmailResponse>(json);
string UserEmail = "";
if (emailResponse.IsSuccess)
{
// success
UserEmail = emailResponse.email;
}
else
{
// failed
}
完成!
此时,用户电子邮件已成功获取。
在结束本文之前,让我们更深入地了解一下“访问令牌”。
访问令牌是一个数字密钥,它允许应用程序在不需要用户密码的情况下访问用户在 Google 服务上的帐户或数据。如前所述,访问令牌的有效期为一小时。一旦过期,您的应用程序必须重复 Google 登录过程。如果我们的唯一关注点是获取用户的电子邮件,那么这个时长就足够了。但是,对于开发第三方应用程序,例如访问用户 Google 日历或 Google Drive 的应用程序,每小时都提示用户进行 Google 登录是不切实际的。
还记得在发起 Google 登录时,有一个参数 access_type=online
吗?
如果将 access_type
设置为 offline
,您将获得另一个附加参数 refresh_token
{
"access_token": "xxxxxxxxxxx",
"expires_in": 3599,
"refresh_token": "xxxxxxxxxx",
"scope": "https://www.googleapis.com/auth/userinfo.email openid",
"token_type": "Bearer",
"id_token": "xxxxxxxxxxxxxxxx"
}
expires_in
的值表示 access_token
的剩余有效时间。缓存此值,一旦它即将过期,或当 access_token
无法访问 Google 数据时,请使用 refresh_token
获取新的 access_token
。
string AccessToken = "";
async void RenewAccessToken()
{
string url = "https://oauth2.googleapis.com/token";
var dicData = new Dictionary<string, string>()
{
{ "client_id", google_api_client_id },
{ "client_secret", google_api_client_secret },
{ "refresh_token", RefreshToken },
{ "grant_type", "refresh_token" }
};
string url = "https://oauth2.googleapis.com/token";
var content = new FormUrlEncodedContent(dicData);
string json = "";
using (var client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(url, content);
json = await response.Content.ReadAsStringAsync();
var tokenResponse =
JsonSerializer.Deserialize<OAuthTokenResponse>(json, jsonOptions);
AccessToken = tokenResponse.access_token;
}
}
这是返回结果 (JSON) 的示例
{
"access_token": "xxxxxxx",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/userinfo.email openid",
"token_type": "Bearer",
"id_token": "xxxxxxxxxxxxxx"
}
好了,新的 access_token
就在这里。
现在,在您的网站上,通过使用获得的电子邮件地址,您可以继续创建新用户帐户或登录用户 (如果用户帐户已创建)。
在您的 Web 应用程序中,可能有一些方法可以创建会话令牌并使用现有 cookie 实现自动登录。
当用户首次登录您的网站时,会创建一个新的登录会话令牌。此令牌被保存,然后发送到用户的浏览器以存储为 cookie。
每次用户重新打开他们的浏览器时,cookie 都会被发送回服务器。然后服务器从数据库检索用户信息。如果 cookie 中的会话令牌与数据库中的匹配,则执行自动登录,并扩展 cookie 的生命周期 (到期日期)。
会话 cookie 通常有一个到期日期。如果会话令牌 cookie 过期,用户将被重定向到 Google 登录页面以重新开始身份验证过程。
有各种方法可以管理用户登录会话,这可能是另一篇文章的主题,也许是第二部分。
Google OAuth 2.0 登录提供了广泛的自定义选项。如果您有其他想法,请随时在评论部分分享。
感谢您的阅读,祝您编码愉快。
历史
- 2024 年 1 月 19 日:初始版本