.net core 第三方微信登录
这里是基于 AddOAuth 实现了第三方微信扫码登陆,如果不清楚 AddOAuth , 请查看 , 下面来看看使用步骤
1. 微信公众平台添加测试号:http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
2. 平台设置回调地址
这里回调地址不用设置 http://
3. 添加基础OAuth认证代码
public static class WeChatOAuth { public const string authenticationScheme = "Wechat"; public static AuthenticationBuilder AddWeChat(this AuthenticationBuilder authenticationBuilder,ActionconfigureOptions) { return authenticationBuilder.AddOAuth (authenticationScheme, configureOptions); } }
public class WeChatOptions : OAuthOptions { ////// Initializes a new public WeChatOptions() { SignInScheme = "Cookies"; CallbackPath = new PathString("/signin-wechat"); StateAddition = "#wechat_redirect"; SaveTokens = true; //PC端扫码登录授权地址 //AuthorizationEndpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"; AuthorizationEndpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"; TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token"; UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo"; //snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid), //snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息) //snsapi_login pc端扫码登录 //WeChatScope = "snsapi_login"; WeChatScope = "snsapi_userinfo"; IsUserInfoClaim = true; } public string AppId { get { return ClientId; } set { ClientId = value; } } public string AppSecret { get { return ClientSecret; } set { ClientSecret = value; } } public string StateAddition { get; set; } public string WeChatScope { get; set; } ///. /// /// 是否将用户信息写入Claims /// public bool IsUserInfoClaim { get; set; } }
public class WeChatHandler : OAuthHandler{ public WeChatHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } /// /// OAuth每次请求的Handle /// ////// public override async Task HandleRequestAsync() { /* ShouldHandleRequestAsync() 源码就是比较回调地址和请求地址是否一致 ShouldHandleRequestAsync() => Task.FromResult(Options.CallbackPath == Request.Path); */ if (!await ShouldHandleRequestAsync()) { return false; } //拿到code回调之后会进行后续操作 AuthenticationTicket? ticket = null; Exception? exception = null; AuthenticationProperties? properties = null; try { var authResult = await HandleRemoteAuthenticateAsync(); if (authResult == null) { exception = new InvalidOperationException("Invalid return state, unable to redirect."); } else if (authResult.Handled) { return true; } else if (authResult.Skipped || authResult.None) { return false; } else if (!authResult.Succeeded) { exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect."); properties = authResult.Properties; } ticket = authResult?.Ticket; } catch (Exception ex) { exception = ex; } if (exception != null) { var errorContext = new RemoteFailureContext(Context, Scheme, Options, exception) { Properties = properties }; await Events.RemoteFailure(errorContext); if (errorContext.Result != null) { if (errorContext.Result.Handled) { return true; } else if (errorContext.Result.Skipped) { return false; } else if (errorContext.Result.Failure != null) { throw new Exception("An error was returned from the RemoteFailure event.", errorContext.Result.Failure); } } if (errorContext.Failure != null) { throw new Exception("An error was encountered while handling the remote login.", errorContext.Failure); } } // We have a ticket if we get here var ticketContext = new TicketReceivedContext(Context, Scheme, Options, ticket) { ReturnUri = ticket.Properties.RedirectUri }; ticket.Properties.RedirectUri = null; // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync ticketContext.Properties!.Items[".AuthScheme"] = Scheme.Name; await Events.TicketReceived(ticketContext); if (ticketContext.Result != null) { if (ticketContext.Result.Handled) { return true; } else if (ticketContext.Result.Skipped) { return false; } } //这里的scheme一定要和注入服务的scheme一样 var identity = new ClaimsIdentity(new ClaimsIdentity("Wechat")); //自定义的claim信息 identity.AddClaim(new Claim("abc", "123")); AuthenticationProperties properties1 = new AuthenticationProperties() { //设置cookie票证的过期时间 ExpiresUtc = DateTime.Now.AddDays(1), RedirectUri = "/Home/Index" }; try { await Context.SignInAsync(SignInScheme, ticketContext.Principal!, ticketContext.Properties); } catch (Exception ex) { var aa = ex.Message; } //await Context.SignInAsync("Wechat", new ClaimsPrincipal(identity), properties); // await Context.SignInAsync(SignInScheme, ticketContext.Principal!, ticketContext.Properties); // Default redirect path is the base path if (string.IsNullOrEmpty(ticketContext.ReturnUri)) { ticketContext.ReturnUri = "/"; } Response.Redirect(ticketContext.ReturnUri); return true; } /// /// 第一步 获取Code之前,需要重定向的地址,可以再这里重写跳转地址的参数 /// /// The. /// The url to redirect to once the challenge is completed. /// The challenge url. protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) { var scopeParameter = properties.GetParameter>(OAuthChallengeProperties.ScopeKey); var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope(); redirectUri = $"{redirectUri}?property={Options.StateDataFormat.Protect(properties)}"; var parameters = new Dictionary { { "appid", Options.ClientId }, { "scope", Options.WeChatScope }, { "response_type", "code" }, { "redirect_uri", redirectUri }, { "state", Options.StateAddition } }; //properties.RedirectUri = redirectUri; if (Options.UsePkce) { var bytes = new byte[32]; RandomNumberGenerator.Fill(bytes); var codeVerifier = Base64UrlTextEncoder.Encode(bytes); // Store this for use during the code redemption. properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier); var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier)); var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes); parameters[OAuthConstants.CodeChallengeKey] = codeChallenge; parameters[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256; } //parameters["state"] = Options.StateDataFormat.Protect(properties); return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters!); } /// /// 第二步 接收到code后,获取token,再创建票据 这里可以重写需要的claims信息等等 /// ///protected override async Task HandleRemoteAuthenticateAsync() { var query = Request.Query; var property = query["property"]; var properties = Options.StateDataFormat.Unprotect(property); if (properties == null) { return HandleRequestResult.Fail("The oauth state was missing or invalid."); } // OAuth2 10.12 CSRF if (!ValidateCorrelationId(properties)) { return HandleRequestResult.Fail("Correlation failed.", properties); } var error = query["error"]; if (!StringValues.IsNullOrEmpty(error)) { // Note: access_denied errors are special protocol errors indicating the user didn't // approve the authorization demand requested by the remote authorization server. // Since it's a frequent scenario (that is not caused by incorrect configuration), // denied errors are handled differently using HandleAccessDeniedErrorAsync(). // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information. var errorDescription = query["error_description"]; var errorUri = query["error_uri"]; if (StringValues.Equals(error, "access_denied")) { var result = await HandleAccessDeniedErrorAsync(properties); if (!result.None) { return result; } var deniedEx = new Exception("Access was denied by the resource owner or by the remote server."); deniedEx.Data["error"] = error.ToString(); deniedEx.Data["error_description"] = errorDescription.ToString(); deniedEx.Data["error_uri"] = errorUri.ToString(); return HandleRequestResult.Fail(deniedEx, properties); } var failureMessage = new StringBuilder(); failureMessage.Append(error); if (!StringValues.IsNullOrEmpty(errorDescription)) { failureMessage.Append(";Description=").Append(errorDescription); } if (!StringValues.IsNullOrEmpty(errorUri)) { failureMessage.Append(";Uri=").Append(errorUri); } var ex = new Exception(failureMessage.ToString()); ex.Data["error"] = error.ToString(); ex.Data["error_description"] = errorDescription.ToString(); ex.Data["error_uri"] = errorUri.ToString(); return HandleRequestResult.Fail(ex, properties); } var code = query["code"]; if (StringValues.IsNullOrEmpty(code)) { return HandleRequestResult.Fail("Code was not found.", properties); } var codeExchangeContext = new OAuthCodeExchangeContext(properties, code.ToString(), BuildRedirectUri(Options.CallbackPath)); using var tokens = await ExchangeCodeAsync(codeExchangeContext); if (tokens.Error != null) { return HandleRequestResult.Fail(tokens.Error, properties); } if (string.IsNullOrEmpty(tokens.AccessToken)) { return HandleRequestResult.Fail("Failed to retrieve access token.", properties); } #region 这里是写入claims ClaimsIdentity identity = new ClaimsIdentity(ClaimsIssuer); //写入用户信息 if (Options.IsUserInfoClaim) identity = await ClaimsIdentity(identity, tokens); #endregion if (Options.SaveTokens) { var authTokens = new List (); authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken }); if (!string.IsNullOrEmpty(tokens.RefreshToken)) { authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken }); } if (!string.IsNullOrEmpty(tokens.TokenType)) { authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType }); } if (!string.IsNullOrEmpty(tokens.ExpiresIn)) { int value; if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) { // https://www.w3.org/TR/xmlschema-2/#dateTime // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value); authTokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) }); } } properties.StoreTokens(authTokens); } var ticket = await CreateTicketAsync(identity, properties, tokens); if (ticket != null) { return HandleRequestResult.Success(ticket); } else { return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties); } } /// /// 第三步 通过到code获取Token /// Exchanges the authorization code for a authorization token from the remote provider. /// /// The. /// The response protected override async Task. ExchangeCodeAsync(OAuthCodeExchangeContext context) { var queryBuilder = new QueryBuilder() { { "appid", Options.ClientId }, { "secret", Options.ClientSecret }, { "code", context.Code }, { "grant_type", "authorization_code" }, }; var url = Options.TokenEndpoint + queryBuilder.ToString(); var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await Backchannel.SendAsync(request, Context.RequestAborted); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { var JOBody = JObject.Parse(body); if (JOBody.Value ("errcode") == null&& JOBody.Value ("openid")!=null) { var tokens = OAuthTokenResponse.Success(JsonDocument.Parse(body)); tokens.TokenType = JOBody.Value ("openid"); return tokens; } else { return OAuthTokenResponse.Failed(new Exception("OAuth token endpoint failure")); } } else return OAuthTokenResponse.Failed(new Exception("OAuth token endpoint failure")); } protected async Task ClaimsIdentity(ClaimsIdentity identity, OAuthTokenResponse tokenResponse) { var queryBuilder = new QueryBuilder() { { "access_token", tokenResponse.AccessToken }, { "openid", tokenResponse.TokenType },//在第二步中,openid被存入TokenType属性 { "lang", "zh_CN" } }; var infoRequest = Options.UserInformationEndpoint + queryBuilder.ToString(); var request = new HttpRequestMessage(HttpMethod.Get, infoRequest); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await Backchannel.SendAsync(request, Context.RequestAborted); var res=await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { var JOBody = JObject.Parse(res); if (JOBody.Value ("errcode") == null && JOBody.Value ("openid") != null) { var identifier = JOBody.Value ("openid"); if (!string.IsNullOrEmpty(identifier)) { identity.AddClaim(new Claim("openid", identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); } var nickname = JOBody.Value ("nickname"); if (!string.IsNullOrEmpty(nickname)) { identity.AddClaim(new Claim("nickname", nickname, ClaimValueTypes.String, Options.ClaimsIssuer)); } var sex = JOBody.Value ("sex"); if (!string.IsNullOrEmpty(sex)) { identity.AddClaim(new Claim("sex", sex, ClaimValueTypes.String, Options.ClaimsIssuer)); } var country = JOBody.Value ("country"); if (!string.IsNullOrEmpty(country)) { identity.AddClaim(new Claim("country", country, ClaimValueTypes.String, Options.ClaimsIssuer)); } var province = JOBody.Value ("province"); if (!string.IsNullOrEmpty(province)) { identity.AddClaim(new Claim("province", province, ClaimValueTypes.String, Options.ClaimsIssuer)); } var city = JOBody.Value ("city"); if (!string.IsNullOrEmpty(city)) { identity.AddClaim(new Claim("city", city, ClaimValueTypes.String, Options.ClaimsIssuer)); } var headimgurl = JOBody.Value ("headimgurl"); if (!string.IsNullOrEmpty(headimgurl)) { identity.AddClaim(new Claim("headimgurl", headimgurl, ClaimValueTypes.String, Options.ClaimsIssuer)); } } } return identity; } }
4. 添加 IServiceCollection ,注意这里如果不设置 AddAuthentication 的 DefaultScheme 默认 Scheme 话,一定要设置Authorize的对应 Scheme ,即 [Authorize(AuthenticationSchemes="Wechat")]
builder.Services.AddAuthentication(WeChatOAuth.authenticationScheme) .AddWeChat(options => { options.AppId = "你的appid"; options.AppSecret = "你的AppSecret "; }) .AddCookie();
5. 在UseAuthorization之前添加 UseAuthentication
app.UseAuthentication();
6. 添加控制器
[Authorize] public class HomeController : Controller { public HomeController() { } public IActionResult Index() { var claims = HttpContext.User.Claims.ToList(); return View(claims); } }
7. 最后运行看看效果(这里是APP微信登录)