Asp .NetCore 微信小程序授权登录、获取用户信息
人生最高理想,在求达于真理。——李大钊
官方文档
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
环境
- .Net Core 6.0
工具
- Visual Studio 2022
- 微信开发者工具
流程
新建项目
我这里是这样新建的,当然也可以使用Visual Studio中的可视化新建。
dotnet new webapi --name AspNetCoreWeChatCode2Session
添加依赖
编辑.csproj
文件。
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>
新增WeChat类进行封装
新建WeChat.cs文件。
using Newtonsoft.Json;
using System.Security.Cryptography;
using System.Text;
namespace AspNetCoreWeChatCode2Session
{
public class WeChat
{
/// <summary>
/// 小程序 appId
/// </summary>
public string appid { get; set; }
/// <summary>
/// 小程序 appSecret
/// </summary>
public string secret { get; set; }
public class Code2SessionParamter
{
/// <summary>
/// 小程序 appId
/// </summary>
public string appid { get; set; }
/// <summary>
/// 小程序 appSecret
/// </summary>
public string secret { get; set; }
/// <summary>
/// 登录时获取的 code
/// </summary>
public string js_code { get; set; }
/// <summary>
/// 授权类型,此处只需填写 authorization_code
/// </summary>
public string grant_type { get; set; } = "authorization_code";
public Code2SessionParamter(string js_code)
{
this.js_code = js_code;
}
public Code2SessionParamter(string appid, string secret, string js_code)
{
this.appid = appid;
this.secret = secret;
this.js_code = js_code;
}
}
public class Code2SessionResult
{
/// <summary>
/// 用户唯一标识
/// </summary>
public string openid { get; set; }
/// <summary>
/// 会话密钥
/// </summary>
public string session_key { get; set; }
/// <summary>
/// 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
/// </summary>
public string unionid { get; set; }
/// <summary>
/// 错误码
/// </summary>
public int errcode { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string errmsg { get; set; }
}
public enum Gender
{
unkown = 0,
man = 1,
woman = 2
}
public class UserInfo
{
public string openId { get; set; }
/// <summary>
/// 用户昵称
/// </summary>
public string nickName { get; set; }
/// <summary>
/// 用户性别
/// </summary>
public Gender gender { get; set; }
/// <summary>
/// 用户所在国家
/// </summary>
public string country { get; set; }
/// <summary>
/// 用户所在省份。
/// </summary>
public string province { get; set; }
/// <summary>
/// 用户所在城市。
/// </summary>
public string city { get; set; }
/// <summary>
/// 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。
/// </summary>
public string unionId { get; set; }
/// <summary>
/// 用户头像图片的 URL。URL 最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640x640 的正方形头像,46 表示 46x46 的正方形头像,剩余数值以此类推。默认132),用户没有头像时该项为空。若用户更换头像,原有头像 URL 将失效。
/// </summary>
public string avatarUrl { get; set; }
/// <summary>
///
/// </summary>
public Watermark watermark { get; set; }
}
public class Watermark
{
/// <summary>
/// 敏感数据归属 appId,开发者可校验此参数与自身 appId 是否一致
/// </summary>
public string appid { get; set; }
/// <summary>
/// 敏感数据获取的时间戳, 开发者可以用于数据时效性校验
/// </summary>
public string timestamp { get; set; }
}
public WeChat(string appid, string secret)
{
this.appid = appid;
this.secret = secret;
}
/// <summary>
/// 登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。更多使用方法详见 小程序登录。
/// https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
/// </summary>
/// <param name="code2SessionParamter">参数</param>
/// <returns></returns>
public async Task<Code2SessionResult?> GetCode2Session(Code2SessionParamter code2SessionParamter)
{
if (string.IsNullOrEmpty(code2SessionParamter.appid)) code2SessionParamter.appid = appid;
if (string.IsNullOrEmpty(code2SessionParamter.secret)) code2SessionParamter.secret = secret;
var result = await Get($"https://api.weixin.qq.com/sns/jscode2session?appid={code2SessionParamter.appid}&secret={code2SessionParamter.secret}&js_code={code2SessionParamter.js_code}&grant_type={code2SessionParamter.grant_type}");
return JsonConvert.DeserializeObject<Code2SessionResult>(result);
}
/// <summary>
/// 内部使用的通用方法
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
static async Task<string> Get(string url)
{
try
{
var httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.GetAsync(url);
return response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : ""; ;
}
catch (Exception ex)
{
throw new Exception("Get 请求出错:" + ex.Message);
}
}
/// <summary>
/// 解密获取用户信息 (不验证签名)
/// </summary>
/// <param name="iv">加密算法的初始向量</param>
/// <param name="encryptedData">包括敏感数据在内的完整用户信息的加密数据</param>
/// <param name="session_key">会话密钥</param>
/// <returns></returns>
public UserInfo? GetUserInfo(string iv, string encryptedData, string session_key)
{
return JsonConvert.DeserializeObject<UserInfo>(AESDecrypt(encryptedData, session_key, iv));
}
/// <summary>
/// 解密获取用户信息 (验证签名)
/// </summary>
/// <param name="iv">加密算法的初始向量</param>
/// <param name="encryptedData">包括敏感数据在内的完整用户信息的加密数据</param>
/// <param name="session_key">会话密钥</param>
/// <param name="rawData">不包括敏感信息的原始数据字符串,用于计算签名</param>
/// <param name="signature">使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息</param>
/// <returns></returns>
public UserInfo? GetUserInfo(string iv, string encryptedData, string session_key, string rawData, string signature)
{
CheckSignature(rawData, session_key, signature);
return GetUserInfo(iv, encryptedData, session_key);
}
/// <summary>
/// 检查签名
/// </summary>
/// <param name="rawData">不包括敏感信息的原始数据字符串,用于计算签名</param>
/// <param name="session_key"></param>
/// <param name="signature">使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息</param>
/// <exception cref="Exception"></exception>
void CheckSignature(string rawData, string session_key, string signature)
{
Console.WriteLine(SHA1Encryption(rawData + session_key));
Console.WriteLine(signature);
if (SHA1Encryption(rawData + session_key).ToUpper() != signature.ToUpper())
{
throw new Exception("CheckSignature 签名校验失败,数据可能损坏。");
}
}
/// <summary>
/// SHA1 加密,返回大写字符串
/// </summary>
/// <param name="content">需要加密字符串</param>
/// <param name="encode">指定加密编码</param>
/// <returns>返回40位大写字符串</returns>
string SHA1Encryption(string content, Encoding encode = null)
{
try
{
if (encode == null) encode = Encoding.UTF8;
SHA1 sha1 = SHA1.Create();
byte[] bytes_in = encode.GetBytes(content);
byte[] bytes_out = sha1.ComputeHash(bytes_in);
sha1.Dispose();
string result = BitConverter.ToString(bytes_out);
result = result.Replace("-", "");
return result;
}
catch (Exception ex)
{
throw new Exception("SHA1Encryption加密出错:" + ex.Message);
}
}
/// <summary>
/// Aes 解密
/// </summary>
/// <param name="encryptedData"></param>
/// <param name="sessionKey"></param>
/// <param name="iv"></param>
/// <returns></returns>
string AESDecrypt(string encryptedData, string sessionKey, string iv)
{
try
{
var encryptedDataByte = Convert.FromBase64String(encryptedData);
var aes = Aes.Create();
aes.Key = Convert.FromBase64String(sessionKey);
aes.IV = Convert.FromBase64String(iv);
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
var transform = aes.CreateDecryptor();
var plainText = transform.TransformFinalBlock(encryptedDataByte, 0, encryptedDataByte.Length);
var result = Encoding.Default.GetString(plainText);
return result;
}
catch (Exception ex)
{
throw new Exception("AESDecrypt解密出错:" + ex.Message);
}
}
}
}
编辑Program文件
修改Program.cs文件。
using AspNetCoreWeChatCode2Session;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 添加WeChat单例服务
builder.Services.AddSingleton<WeChat>(new WeChat("appid", "secret"));
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
app.Run();
获取appid和secret
-
进入微信公众平台 https://mp.weixin.qq.com/ 。
-
进入开发设置中获取。
新增接口进行测试
新建LoginController.cs文件放在Controllers文件夹中。
using Microsoft.AspNetCore.Mvc;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace AspNetCoreWeChatCode2Session.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
readonly WeChat _weChat;
static string session_key = "";
public LoginController(WeChat weChat)
{
_weChat = weChat;
}
[HttpGet("{code}")]
public async Task<IActionResult> Get(string code)
{
var res = await _weChat.GetCode2Session(new WeChat.Code2SessionParamter(code));
session_key = res.session_key;
// 注意: 这里是为了方便演示,开发的时候不应该去传给前端。
// 为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。
return new JsonResult(res);
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] LoginParameter parameter)
{
if (!string.IsNullOrEmpty(parameter.code)) await Get(parameter.code);
return new JsonResult(_weChat.GetUserInfo(parameter.iv, parameter.encryptedData, session_key, parameter.rawData, parameter.signature));
}
public class LoginParameter
{
/// <summary>
/// 用于解析session_key
/// </summary>
public string? code { get; set; }
/// <summary>
/// 不包括敏感信息的原始数据字符串,用于计算签名
/// </summary>
public string rawData { get; set; }
/// <summary>
/// 包括敏感数据在内的完整用户信息的加密数据
/// </summary>
public string encryptedData { get; set; }
/// <summary>
/// 加密算法的初始向量
/// </summary>
public string iv { get; set; }
/// <summary>
/// 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息
/// </summary>
public string signature { get; set; }
}
}
}
新建小程序
修改app.ts
在app.ts中调用获取openid接口
// app.ts
App<IAppOption>({
globalData: {},
onLaunch() {
wx.hideHomeButton();
wx.login({
success: (res) => {
wx.request({
url: `http://localhost:5000/api/login/${res.code}`,
method: "GET",
success: (res) => {
console.log(res);
}
})
}
})
},
})
新建test页面
index.wxml
<view bindtap="getUserInfo">获取用户信息</view>
index.ts
let checkSession = (): Promise<WechatMiniprogram.GeneralCallbackResult> => {
return new Promise((resolve, reject) => {
wx.checkSession({
success: resolve,
fail: reject
})
})
}
let getUserProfile = (): Promise<WechatMiniprogram.GetUserProfileSuccessCallbackResult & { iv: string, signature: string, encryptedData: string, rawData: string }> => {
return new Promise((resolve, reject) => {
wx.getUserProfile({
desc: "用于更好的展示。",
success: (res: any) => resolve(res),
fail: reject
})
})
}
let login = (): Promise<WechatMiniprogram.LoginSuccessCallbackResult> => {
return new Promise((resolve, reject) => {
wx.login({
success: resolve,
fail: reject
})
})
}
Page({
data: {
code: '',
},
async onLoad() {
try {
await checkSession();
} catch (error) {
this.setData({ code: (await login()).code })
}
},
async getUserInfo() {
let code = this.data.code;
let userRes = await getUserProfile();
let data = {
encryptedData: userRes.encryptedData,
iv: userRes.iv,
signature: userRes.signature,
rawData: userRes.rawData,
code,
}
wx.request({
url: `http://localhost:5000/api/login`,
method: 'POST',
data,
success: async (res) => {
if (res.statusCode != 200) {
console.log("error", res);
this.setData({ code: (await login()).code })
}
else {
// 防止code重复使用
this.setData({ code: "" })
console.log("success", res);
}
},
fail: async (err) => {
console.log("fail", err);
this.setData({ code: (await login()).code })
}
})
console.log(userRes);
}
})
稍微测试一下
没啥大问题~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?