1. 程式人生 > >.net core 2.x - 微信、QQ 授權登入

.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() 獲取看一下,或者單獨講這個介面提供給前端呼叫檢視。