.net core 2.x - 微信、QQ 授權登錄
上一篇是關於模擬請求配置,包括域名問題的解決,本篇就說下授權登錄。嗯,比較閑。以前的fx 開發web的時候好像使用的 微信提供的js插件生成二維碼,然後掃碼登錄,,,記不清了,好久不開發微信了。
1.準備工作。
1.1.單獨解決ajax的跨域問題
首先考慮到web端(ajax)跨域的問題,所以我們首先要解決的就是在core中配置跨域的設置(案例比較多所以不多說只貼代碼):
//ConfigureServices中 services.AddCors(options => { options.AddPolicy("AllowCORS", builder => { builder.WithOrigins("http://s86zxm.natappfree.cc", "http://127.0.0.1:65502").AllowAnyHeader().AllowAnyMethod().AllowCredentials(); }); }); //Configure中(一定是在 app.UserMvc())之前配置) app.UseCors("AllowCORS");
a)這裏的臨時域名就是我們上篇說的基於natapp生成的動態的。
b)這裏的 UseCors一定是要在app.UseMvc()之前;另外我這裏是全局配置,如果您需要針對 controller或者action單獨配置,可以去掉這裏的app.usecors,在每個controller上或者action上加上EnableCors("跨域策略名稱"),我們這裏的策略名稱是AllowCORS。
ajax中的請求方式,要註意以下幾個點:
async: true,//Failed to execute ‘send‘ on ‘XMLHttpRequest‘
dataType: ‘jsonp‘,
crossDomain: true,
需要指定ajax的這三個屬性,其中第一個 如果使用 false,也就似乎非異步方式,會出現後面紅色的錯誤提示。大致的參考腳本如下:
$.ajax({ type: ‘get‘, async: true,//Failed to execute ‘send‘ on ‘XMLHttpRequest‘ dataType: ‘jsonp‘, crossDomain: true, url: ‘/api/identity/OAuth2?provider=Weixin&returnUrl=/‘, success: function (res) { //do something }, error: function (xhr, err) { console.log(xhr.statusCode); //do something } });
1.2.解決配置問題
a) 這裏的配置指的是,比如微信開發域名的問題,這個問題在上一篇中,有說到,如果不知道的可以點這裏 域名配置
b) 另一個就是 配置微信或者QQ的 appId和AppSecret,這個獲取方式上一篇有說(微信),QQ類似;在我們的 core項目中配置,參考如下:
//configureService中配置 services.AddAuthentication().AddWeixinAuthentication(options => { options.ClientId = Configuration["Authentication:WeChat:AppId"]; options.ClientSecret = Configuration["Authentication:WeChat:AppKey"]; }); //configures中使用 app.UseAuthentication(); //配置文件中: { "ESoftor":{ "Authentication": { "WeChat": { "AppId": "你的微信AppId", "AppKey": "你的微信secret" } } } }
以上這些完成之後,我們就一切就緒了,重點來了,代碼:
2.授權實現
以下五個相關文件直接復制到項目(不需要做任何改動),便可直接使用,本人已全部測試通過,,謝謝配合。
WeixinAuthenticationDefaults.cs
/// <summary> /// Default values for Weixin authentication. /// </summary> public static class WeixinAuthenticationDefaults { /// <summary> /// Default value for <see cref="AuthenticationOptions.DefaultAuthenticateScheme"/>. /// </summary> public const string AuthenticationScheme = "Weixin"; public const string DisplayName = "Weixin"; /// <summary> /// Default value for <see cref="RemoteAuthenticationOptions.CallbackPath"/>. /// </summary> public const string CallbackPath = "/signin-weixin"; /// <summary> /// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>. /// </summary> public const string Issuer = "Weixin"; /// <summary> /// Default value for <see cref="OAuth.OAuthOptions.AuthorizationEndpoint"/>. /// </summary> public const string AuthorizationEndpoint = "https://open.weixin.qq.com/connect/qrconnect"; /// <summary> /// Default value for <see cref="OAuth.OAuthOptions.TokenEndpoint"/>. /// </summary> public const string TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token"; /// <summary> /// Default value for <see cref="OAuth.OAuthOptions.UserInformationEndpoint"/>. /// </summary> public const string UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo"; }View Code
WeiXinAuthenticationExtensions.cs
public static class WeiXinAuthenticationExtensions { /// <summary> /// </summary> public static AuthenticationBuilder AddWeixinAuthentication(this AuthenticationBuilder builder) { return builder.AddWeixinAuthentication(WeixinAuthenticationDefaults.AuthenticationScheme, WeixinAuthenticationDefaults.DisplayName, options => { }); } /// <summary> /// </summary> public static AuthenticationBuilder AddWeixinAuthentication(this AuthenticationBuilder builder, Action<WeixinAuthenticationOptions> configureOptions) { return builder.AddWeixinAuthentication(WeixinAuthenticationDefaults.AuthenticationScheme, WeixinAuthenticationDefaults.DisplayName, configureOptions); } /// <summary> /// </summary> public static AuthenticationBuilder AddWeixinAuthentication(this AuthenticationBuilder builder, string authenticationScheme, Action<WeixinAuthenticationOptions> configureOptions) { return builder.AddWeixinAuthentication(authenticationScheme, WeixinAuthenticationDefaults.DisplayName, configureOptions); } /// <summary> /// </summary> public static AuthenticationBuilder AddWeixinAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<WeixinAuthenticationOptions> configureOptions) { return builder.AddOAuth<WeixinAuthenticationOptions, WeixinAuthenticationHandler>(authenticationScheme, displayName, configureOptions); } }View Code
WeixinAuthenticationHandler.cs
public class WeixinAuthenticationHandler : OAuthHandler<WeixinAuthenticationOptions> { public WeixinAuthenticationHandler(IOptionsMonitor<WeixinAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } /// <summary> /// Last step: /// create ticket from remote server /// </summary> /// <param name="identity"></param> /// <param name="properties"></param> /// <param name="tokens"></param> /// <returns></returns> protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string> { ["access_token"] = tokens.AccessToken, ["openid"] = tokens.Response.Value<string>("openid") }); var response = await Backchannel.GetAsync(address); if (!response.IsSuccessStatusCode) { Logger.LogError("An error occurred while retrieving the user profile: the remote server " + "returned a {Status} response with the following payload: {Headers} {Body}.", /* Status: */ response.StatusCode, /* Headers: */ response.Headers.ToString(), /* Body: */ await response.Content.ReadAsStringAsync()); throw new HttpRequestException("An error occurred while retrieving user information."); } var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); if (!string.IsNullOrEmpty(payload.Value<string>("errcode"))) { Logger.LogError("An error occurred while retrieving the user profile: the remote server " + "returned a {Status} response with the following payload: {Headers} {Body}.", /* Status: */ response.StatusCode, /* Headers: */ response.Headers.ToString(), /* Body: */ await response.Content.ReadAsStringAsync()); throw new HttpRequestException("An error occurred while retrieving user information."); } identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, WeixinAuthenticationHelper.GetUnionid(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim(ClaimTypes.Name, WeixinAuthenticationHelper.GetNickname(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim(ClaimTypes.Gender, WeixinAuthenticationHelper.GetSex(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim(ClaimTypes.Country, WeixinAuthenticationHelper.GetCountry(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim("urn:weixin:openid", WeixinAuthenticationHelper.GetOpenId(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim("urn:weixin:province", WeixinAuthenticationHelper.GetProvince(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim("urn:weixin:city", WeixinAuthenticationHelper.GetCity(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim("urn:weixin:headimgurl", WeixinAuthenticationHelper.GetHeadimgUrl(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim("urn:weixin:privilege", WeixinAuthenticationHelper.GetPrivilege(payload), Options.ClaimsIssuer)); identity.AddClaim(new Claim("urn:weixin:user_info", payload.ToString(), Options.ClaimsIssuer)); var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload); context.RunClaimActions(); await Events.CreatingTicket(context); return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); } /// <summary> /// Step 2:通過code獲取access_token /// </summary> protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri) { var address = QueryHelpers.AddQueryString(Options.TokenEndpoint, new Dictionary<string, string>() { ["appid"] = Options.ClientId, ["secret"] = Options.ClientSecret, ["code"] = code, ["grant_type"] = "authorization_code" }); var response = await Backchannel.GetAsync(address); if (!response.IsSuccessStatusCode) { Logger.LogError("An error occurred while retrieving an access token: the remote server " + "returned a {Status} response with the following payload: {Headers} {Body}.", /* Status: */ response.StatusCode, /* Headers: */ response.Headers.ToString(), /* Body: */ await response.Content.ReadAsStringAsync()); return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); } var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); if (!string.IsNullOrEmpty(payload.Value<string>("errcode"))) { Logger.LogError("An error occurred while retrieving an access token: the remote server " + "returned a {Status} response with the following payload: {Headers} {Body}.", /* Status: */ response.StatusCode, /* Headers: */ response.Headers.ToString(), /* Body: */ await response.Content.ReadAsStringAsync()); return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); } return OAuthTokenResponse.Success(payload); } /// <summary> /// Step 1:請求CODE /// 構建用戶授權地址 /// </summary> protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) { return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, new Dictionary<string, string> { ["appid"] = Options.ClientId, ["scope"] = FormatScope(), ["response_type"] = "code", ["redirect_uri"] = redirectUri, ["state"] = Options.StateDataFormat.Protect(properties) }); } protected override string FormatScope() { return string.Join(",", Options.Scope); } }View Code
WeixinAuthenticationHelper.cs
/// <summary> /// Contains static methods that allow to extract user‘s information from a <see cref="JObject"/> /// instance retrieved from Weixin after a successful authentication process. /// </summary> static class WeixinAuthenticationHelper { /// <summary> /// Gets the user identifier. /// </summary> public static string GetOpenId(JObject user) => user.Value<string>("openid"); /// <summary> /// Gets the nickname associated with the user profile. /// </summary> public static string GetNickname(JObject user) => user.Value<string>("nickname"); /// <summary> /// Gets the gender associated with the user profile. /// </summary> public static string GetSex(JObject user) => user.Value<string>("sex"); /// <summary> /// Gets the province associated with the user profile. /// </summary> public static string GetProvince(JObject user) => user.Value<string>("province"); /// <summary> /// Gets the city associated with the user profile. /// </summary> public static string GetCity(JObject user) => user.Value<string>("city"); /// <summary> /// Gets the country associated with the user profile. /// </summary> public static string GetCountry(JObject user) => user.Value<string>("country"); /// <summary> /// Gets the avatar image url associated with the user profile. /// </summary> public static string GetHeadimgUrl(JObject user) => user.Value<string>("headimgurl"); /// <summary> /// Gets the union id associated with the application. /// </summary> public static string GetUnionid(JObject user) => user.Value<string>("unionid"); /// <summary> /// Gets the privilege associated with the user profile. /// </summary> public static string GetPrivilege(JObject user) { var value = user.Value<JArray>("privilege"); if (value == null) { return null; } return string.Join(",", value.ToObject<string[]>()); } }View Code
WeixinAuthenticationOptions.cs
public WeixinAuthenticationOptions() { ClaimsIssuer = WeixinAuthenticationDefaults.Issuer; CallbackPath = new PathString(WeixinAuthenticationDefaults.CallbackPath); AuthorizationEndpoint = WeixinAuthenticationDefaults.AuthorizationEndpoint; TokenEndpoint = WeixinAuthenticationDefaults.TokenEndpoint; UserInformationEndpoint = WeixinAuthenticationDefaults.UserInformationEndpoint; Scope.Add("snsapi_login"); Scope.Add("snsapi_userinfo"); //ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "openid"); //ClaimActions.MapJsonKey(ClaimTypes.Name, "nickname"); //ClaimActions.MapJsonKey("urn:qq:figure", "figureurl_qq_1"); }View Code
3.怎麽用?
首先定義我們的接口,接口中當然依舊是用到了 SignInManager,,如果不清楚的,依舊建議去看上一篇。
/// <summary> /// OAuth2登錄 /// </summary> /// <param name="provider">第三方登錄提供器</param> /// <param name="returnUrl">回調地址</param> /// <returns></returns> [HttpGet] [Description("OAuth2登錄")] [AllowAnonymous] //[ValidateAntiForgeryToken] public IActionResult OAuth2() { string provider = HttpContext.Request.Params("provider"); string returnUrl = HttpContext.Request.Params("returnUrl"); string redirectUrl = Url.Action(nameof(OAuth2Callback), "Identity", new { returnUrl = returnUrl ?? "/" }); AuthenticationProperties properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); return Challenge(properties, provider); }
這裏參數沒啥好說的,一個就是 provider:這個東西其實是我們配置的(下面會說),returnUrl,就是你登陸前訪問的頁面,登陸後還要回過去。 這裏還用到i一個回調接口哦,就是 OAuth2Callback,所以至少是需要兩個。
/// <summary> /// OAuth2登錄回調 /// </summary> /// <param name="returnUrl">回調地址</param> /// <param name="remoteError">第三方登錄錯誤提示</param> /// <returns></returns> [HttpGet] [Description("OAuth2登錄回調")] [AllowAnonymous] //[ValidateAntiForgeryToken] public IActionResult OAuth2Callback(string returnUrl = null, string remoteError = null) { if (remoteError != null) { _logger.LogError($"第三方登錄錯誤:{remoteError}"); return Unauthorized(); } ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) return Unauthorized(); var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false, true); _logger.LogWarning($"SignInResult:{result.ToJsonString()}"); if (result.Succeeded) { _logger.LogInformation($"用戶“{info.Principal.Identity.Name}”通過 {info.ProviderDisplayName} OAuth2登錄成功"); return Ok(); } return Unauthorized(); }
代碼這就完了哦,剩下的就是調試了,使用1中說到的js(ajax),以及結合上一篇的配置,模擬請求去吧,如果你發現返回提示如下錯誤,那麽就等同事成功了,因為可以在header中看到請求的地址,該地址就是微信的二維碼的界面,復制出來在瀏覽器打開就可:
但是,這裏來了個但是,你覺得這樣就完了是吧?其實沒有,這裏有個細節要註意,也就是上面說的 接口的參數:provider,這個東西不是隨便寫的,可以在請求之前獲取一次看看,有哪些provider,如果我們配置了微信那麽就是 Weixin,配置了QQ就是QQ,
查看方式就是一行代碼:
var loginProviders = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
這裏要註意,否則你的道德返回結果永遠都是 未授權,當然這也是可配置的,也就是我們的 WeixinAuthenticationDefaults.cs類中的 的 Scheme,配置啥,傳遞參數就寫啥。
4.總結(註意點)
1.微信、QQ配置,及開發測試的模擬配置(域名)
2.跨域問題
3.參數:provider要一致,不確定的可以通過 _signInManager.GetExternalAuthenticationSchemesAsync() 獲取看一下,或者單獨講這個接口提供給前端調用查看。
.net core 2.x - 微信、QQ 授權登錄