.net core 基于公众号的网页微信登录
前言
这里是基于 AddOAuth 实现了微信客户端三方网页授权实现的登陆(网站的微信扫码登录没有找到测试账号,需要添加应用才能演示,想要实现网站扫码登录,只能查看官方文档),如果不清楚 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,Action<WeChatOptions> configureOptions)
{
return authenticationBuilder.AddOAuth<WeChatOptions, WeChatHandler>(authenticationScheme, configureOptions);
}
}
public class WeChatOptions : OAuthOptions
{
/// <summary>
/// Initializes a new <see cref="WeChatOptions"/>.
/// </summary>
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; }
/// <summary>
/// 是否将用户信息写入Claims
/// </summary>
public bool IsUserInfoClaim { get; set; }
}
public class WeChatHandler : OAuthHandler<WeChatOptions>
{
public WeChatHandler(IOptionsMonitor<WeChatOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
/// <summary>
/// OAuth每次请求的Handle
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public override async Task<bool> 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;
}
/// <summary>
/// 第一步 获取Code之前,需要重定向的地址,可以再这里重写跳转地址的参数
/// </summary>
/// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
/// <param name="redirectUri">The url to redirect to once the challenge is completed.</param>
/// <returns>The challenge url.</returns>
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
redirectUri = $"{redirectUri}?property={Options.StateDataFormat.Protect(properties)}";
var parameters = new Dictionary<string, string>
{
{ "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!);
}
/// <summary>
/// 第二步 接收到code后,获取token,再创建票据 这里可以重写需要的claims信息等等
/// </summary>
/// <returns></returns>
protected override async Task<HandleRequestResult> 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<AuthenticationToken>();
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);
}
}
/// <summary>
/// 第三步 通过到code获取Token
/// Exchanges the authorization code for a authorization token from the remote provider.
/// </summary>
/// <param name="context">The <see cref="OAuthCodeExchangeContext"/>.</param>
/// <returns>The response <see cref="OAuthTokenResponse"/>.</returns>
protected override async Task<OAuthTokenResponse> 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<string>("errcode") == null&& JOBody.Value<string>("openid")!=null)
{
var tokens = OAuthTokenResponse.Success(JsonDocument.Parse(body));
tokens.TokenType = JOBody.Value<string>("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(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);
var res=await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var JOBody = JObject.Parse(res);
if (JOBody.Value<string>("errcode") == null && JOBody.Value<string>("openid") != null)
{
var identifier = JOBody.Value<string>("openid");
if (!string.IsNullOrEmpty(identifier))
{
identity.AddClaim(new Claim("openid", identifier, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var nickname = JOBody.Value<string>("nickname");
if (!string.IsNullOrEmpty(nickname))
{
identity.AddClaim(new Claim("nickname", nickname, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var sex = JOBody.Value<string>("sex");
if (!string.IsNullOrEmpty(sex))
{
identity.AddClaim(new Claim("sex", sex, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var country = JOBody.Value<string>("country");
if (!string.IsNullOrEmpty(country))
{
identity.AddClaim(new Claim("country", country, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var province = JOBody.Value<string>("province");
if (!string.IsNullOrEmpty(province))
{
identity.AddClaim(new Claim("province", province, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var city = JOBody.Value<string>("city");
if (!string.IsNullOrEmpty(city))
{
identity.AddClaim(new Claim("city", city, ClaimValueTypes.String, Options.ClaimsIssuer));
}
var headimgurl = JOBody.Value<string>("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(CookieAuthenticationDefaults.AuthenticationScheme, cookie => //{ //builder.Configuration.Bind("CookieSettings", cookie); //cookie.LoginPath = "/Home/Challenge/"; //})
.AddCookie() ;
5. 在UseAuthorization之前添加 UseAuthentication
app.UseAuthentication();
6. 添加控制器
public class HomeController : Controller { public HomeController() { } public IActionResult Challenge() { return Challenge("Wechat"); } [Authorize] public IActionResult Index() { var claims = HttpContext.User.Claims.ToList(); return View(claims); } }
7. 最后运行看看效果(这里是APP微信登录)

注意:如果要授权可以不用基于AddOAuth 来实现,可以手动去实现,大概步骤为: 1. 直接用户授权地址:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirec,将参数组装好后,直接进行跳转 。这里回调地址redirect_uri需要使用 urlEncode 对链接进行处理 2. 用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。 3. 通过code获取到access_token 和 openid :https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code 4. 需要获取用户信息,再调用接口:https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 到上面这里就已经获取到用户信息了,如果想要实现登录,可以分为两种情况 1. 如果没有用三方授权认证框架,可以直接手动调用Context.SignInAsync,将微信用户信息写进去 2. 如果用了三方认证框架,可以用opnid直接调用三方框架生成token,将用户信息都写入token,很多微信小程序的登录就是用的这个逻辑。 以上只是个人结合实践经验的理解,如有问题,欢迎指出

浙公网安备 33010602011771号