asp.netcore 微信登录(ASP.NETCore学习记录)
asp.netcore 微信登录(ASP.NETCore学习记录)说明必须开发者需要构造如下的链接来获取code参数:https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=CORPID&agentid=AGENTID&redirect_uri=REDIRECT_URI&state=STATE参数说明:参数
1、开始开发https://developer.work.weixin.qq.com/Document/path/91025
企业微信提供了OAuth的扫码登录授权方式,可以让企业的网站在浏览器内打开时,引导成员使用企业微信扫码登录授权,从而获取成员的身份信息,免去登录的环节。(注:此授权方式需要用户扫码,不同于“网页授权登录”;仅企业内可以使用此种授权方式,第三方服务商不支持使用。)在进行企业微信授权登录之前,需要先在企业的管理端后台创建一个具备“企业微信授权登录”能力的应用。
1.1 企业微信扫码登陆接入流程 1.2 开启网页授权登陆登录 企业管理端后台->进入需要开启的自建应用->点击 “企业微信授权登录”,进入如下页面
然后点击 "设置授权回调域",输入回调域名,点击“保存”。(域名:需要找运维做解析)
要求配置的授权回调域,必须与访问链接的域名完全一致,如下图:
1.3 构造独立窗口登陆二维码开发者需要构造如下的链接来获取code参数:
https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=CORPID&agentid=AGENTID&redirect_uri=REDIRECT_URI&state=STATE
参数说明:
参数 |
必须 |
说明 |
appid |
是 |
企业微信的CorpID,在企业微信管理端查看 |
agentid |
是 |
授权方的网页应用ID,在具体的网页应用中查看 |
redirect_uri |
是 |
重定向地址,需要进行UrlEncode |
state |
否 |
用于保持请求和回调的状态,授权请求后原样带回给企业。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议企业带上该参数,可设置为简单的随机数加session进行校验 |
lang |
否 |
自定义语言,支持zh、en;lang为空则从Headers读取Accept-Language,默认值为zh |
若提示“该链接无法访问”,请检查参数是否填写错误,如redirect_uri的域名与网页应用的可信域名不一致。
若用户不在agentid所指应用的可见范围,扫码时会提示无权限。
返回说明:
用户允许授权后,将会重定向到redirect_uri的网址上,并且带上code和state参数
redirect_uri?code=CODE&state=STATE
若用户禁止授权,则重定向后不会带上code参数,仅会带上state参数
redirect_uri?state=STATE
示例:
假定当前
企业CorpID:wxCorpId
开启授权登录的应用ID:1000000
登录跳转链接:http://api.3dept.com
state设置为:weblogin@gyoss9
需要配置的授权回调域为:api.3dept.com
根据URL规范,将上述参数分别进行UrlEncode,得到拼接的OAuth2链接为:
https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=wxCorpId&agentid=1000000&redirect_uri=回调域名&state=web_login@gyoss9
1.4 构造内嵌登陆二维码
在需要展示企业微信网页登录二维码的网站引入如下JS文件(支持https):
步骤一:引入JS文件 (vue框架的话,放在index.html文件中)
<script src="https://rescdn.qqmail.com/node/ww/wwopenmng/js/sso/wwLogin-1.0.0.js" type="text/javascript"></script>
版本:
旧版:http://rescdn.qqmail.com/node/ww/wwopenmng/js/sso/wwLogin-1.0.0.js
新版(20220415更新):http://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js
步骤二:在需要使用微信登录的地方实例JS对象(React同理)
注意:从wwLogin-1.2.5.js开始需要使用new WwLogin进行实例化
<template>
<el-tabs v-model="activeName" @tab-click="handleClick" >
<el-tab-pane label="账户密码登录" name="first" class="wechart-pane">
<el-form-item prop="tenant">
<el-input
v-model="loginForm.tenant"
type="text"
auto-complete="off"
placeholder="租户"
>
<i
slot="prefix"
class="el-input__icon el-icon-office-building"
></i>
</el-input>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
icon="el-icon-user"
type="text"
auto-complete="off"
placeholder="账号"
>
<i slot="prefix" class="el-input__icon el-icon-user"></i>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
icon="el-icon-unlock"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<i slot="prefix" class="el-input__icon el-icon-unlock"></i>
</el-input>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
size="medium"
type="primary"
style="width: 100%"
@click.native.prevent="handleLogin"
>
登 录
</el-button>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="扫码登录" name="second" class="wechart-pane" >
<div id="wx_qrcode"></div>
</el-tab-pane>
</el-tabs>
</template>
脚本部分:定义全局变量wwLogin,方便后面销毁
handleClick(tab event) {
const that = this;
if (tab){
switch (tab.name) {
case 'first':
if (that.wwLogin != null){
that.wwLogin.destroyed(); // 注意wwLogin为实例对象,无需登录时,可手动销毁实例
}
break;
case 'second':
that.wwLogin = new WwLogin({
'id': 'wx_qrcode' //二维码显示区域div的id值
'appid': '企业微信后台的corpid'
'agentid': '企业微信后台的agentid'
'redirect_uri': '回调地址(必须为域名模式)' //http://localhost:53362/connect/token
'state': ''
'href': ''
'lang': 'zh'
})
break;
default:break;
}
}
}
@@登陆顺序:
此处先介绍一下abpvnext登陆时访问接口或者服务顺序:
1. 发现文档配置http://localhost:53362/.well-known/openid-configuration
访问结果如下所示:
{
"issuer": "http://localhost:53362"
"jwks_uri": "http://localhost:53362/.well-known/openid-configuration/jwks"
"authorization_endpoint": "http://localhost:53362/connect/authorize"
"token_endpoint": "http://localhost:53362/connect/token"
"userinfo_endpoint": "http://localhost:53362/connect/userinfo"
"end_session_endpoint": "http://localhost:53362/connect/endsession"
"check_session_iframe": "http://localhost:53362/connect/checksession"
"revocation_endpoint": "http://localhost:53362/connect/revocation"
"introspection_endpoint": "http://localhost:53362/connect/introspect"
"device_authorization_endpoint": "http://localhost:53362/connect/deviceauthorization"
"frontchannel_logout_supported": true
"frontchannel_logout_session_supported": true
"backchannel_logout_supported": true
"backchannel_logout_session_supported": true
"scopes_supported": [
"openid"
"profile"
"email"
"address"
"phone"
"role"
"BaseService"
"InternalGateway"
"WebAppGateway"
"BusinessService"
"offline_access"
]
"claims_supported": [
"sub"
"birthdate"
"family_name"
"gender"
"given_name"
"locale"
"middle_name"
"name"
"nickname"
"picture"
"preferred_username"
"profile"
"updated_at"
"website"
"zoneinfo"
"email"
"email_verified"
"address"
"phone_number"
"phone_number_verified"
"role"
]
"grant_types_supported": [
"authorization_code"
"client_credentials"
"refresh_token"
"implicit"
"password"
"urn:ietf:params:oauth:grant-type:device_code"
]
"response_types_supported": [
"code"
"token"
"id_token"
"id_token token"
"code id_token"
"code token"
"code id_token token"
]
"response_modes_supported": [
"form_post"
"query"
"fragment"
]
"token_endpoint_auth_methods_supported": [
"client_secret_basic"
"client_secret_post"
]
"id_token_signing_alg_values_supported": [
"RS256"
]
"subject_types_supported": [
"public"
]
"code_challenge_methods_supported": [
"plain"
"S256"
]
"request_parameter_supported": true
}
代码方式获取(Url可配置在appsettings.json或者nacos配置中心):
var client = new HttpClient() ;
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:53362/.well-known/openid-configuration");
2. 获取token的Url地址
http://localhost:53362/connect/token
3. 根据token获取用户信息地址http://localhost:53362/connect/userinfo
4. vue-element-admin菜单权限是使用用户角色来控制的,我们不需要role,通过接口:http://localhost:53362/api/abp/application-configuration
返回结果中的auth.grantedPolicies字段,与对应的菜单路由绑定,就可以实现权限的控制。
2、企业微信扫码成功后回调/connect/token原理:通过查看IdentityServer4的源码发现,通过GrantType来区分不同的授权方式,除了常规的授权方式之外,在defaut条件中,有自定义授权生成token的方式(ProcessExtensionGrantRequestAsync),可以通过这种方式集成旧的业务系统验证,比如,企业微信扫码、小程序授权、短信登陆、微信登陆、钉钉登陆 等等不同第三方进行集成。
2.1自定义授权实现public class ExtensionGrantTypes
{
//扩展授权名称
public const string WeChatQrCodeGrantType = "WeChat";
}
public class WeChatQrCodeGrantValidator : IExtensionGrantValidator
{
public string GrantType => ExtensionGrantTypes.WeChatQrCodeGrantType;
private readonly DateTime DateTime1970 = new DateTime(1970 1 1).ToLocalTime();
private readonly UserManager<Volo.Abp.Identity.IdentityUser> _userManager;
private readonly IjsonSerializer _jsonSerializer;
public WeChatQrCodeGrantValidator(
UserManager<Volo.Abp.Identity.IdentityUser> userLoginManager
IJsonSerializer jsonSerializer)
{
_userManager = userLoginManager;
_jsonSerializer = jsonSerializer;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
string code = context.Request.Raw.Get("Code");
if (string.IsNullOrEmpty(code))
{
context.Result = new GrantValidationResult(IdentityServer4.Models.TokenRequestErrors.InvalidGrant);
}
//下面第1、2可以封装成接口或服务 参考下面3.1、3.2 部分,方便后期接入
//1、获取企业微信访问令牌access_token
string accessToken = "123123123123";
//2、获取企业微信访问用户身份(企业微信号) UserId
string userId = "ZhangSan";
//3、根据企业微信用户身份userId找到业务库用户表对比,找到真实的用户信息
if (!string.IsNullOrEmpty(userId))
{
context.Result = await ServerValidate("" ""); //可以把UserId传进去
}
else
context.Result = new GrantValidationResult(IdentityServer4.Models.TokenRequestErrors.InvalidGrant);
}
/// <summary>
/// 服务器端验证并输出用户信息,后续自动生成token
/// </summary>
/// <param name="loginProvider"></param>
/// <param name="providerKey"></param>
/// <returns></returns>
private async Task<GrantValidationResult> ServerValidate(string loginProvider string providerKey)
{
var user = await _userManager.FindByLoginAsync(loginProvider providerKey); //业务库用户
if (user == null)
return new GrantValidationResult(IdentityServer4.Models.TokenRequestErrors.InvalidGrant);
var principal = new ClaimsPrincipal();
List<ClaimsIdentity> claimsIdentity = new List<ClaimsIdentity>();
ClaimsIdentity identity = new ClaimsIdentity();
identity.AddClaim(new Claim("sub" user.Id.ToString()));
identity.AddClaim(new Claim("tenantid" user.TenantId.ToString())); //租户Id
identity.AddClaim(new Claim("idp" "local"));
identity.AddClaim(new Claim("amr" loginProvider));
long authTime = (long)(DateTime.Now.ToLocalTime() - DateTime1970).TotalSeconds;
identity.AddClaim(new Claim("auth_time" authTime.ToString()));
claimsIdentity.Add(identity);
principal.AddIdentities(claimsIdentity);
return new GrantValidationResult(principal);
}
}
2.2 添加扩展方法(在实现AbpModel类中)
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.PreConfigure<IIdentityServerBuilder>(builder => {
builder.AddExtensionGrantValidator<WeChatQrCodeGrantValidator>();
});
}
2.3 在Domain项目中的identityServer 文件夹中的种子数据添加grantTypes(CreateClientAsync()下)
await CreateClientAsync(
name: "wechat-web"
scopes: commonScopes.Union(new[] {
"IdentityService" "InternalGateway" "WebAppGateway" "BusinessService" "WeChat"
})
grantTypes: new[] { "WeChat" }
//redirectUri: $"http://localhost:44307/authentication/login-callback"
requireClientSecret: false
);
2.4 前三步执行后,无需执行Add-Migration/Update-Database命令,直接启动服务,种子数据会自动入库并配置好。2.5 访问token
http://localhost:53362/connect/token
是不是发现这个链接熟悉,没错就是上面“@@登陆顺序”部分,前端按之前账号、密码登陆方式调用即可,切换为下面的参数,后续同@@登陆顺序部分一致。
3、企业微信获取token和用户账号3.1 获取访问令牌access_token请求地址:
https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
#corpid、corpsecret换为自己的corpid、应用secret
返回结果:
{"access_token":"sdfadsf" "expires_in":15 "errcode":0 "errmsg":"ok"}
3.2 获取访问用户身份(企业微信号)
请求地址:
https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
返回结果:
{"UserId":"WangWu" "DeviceId":"" "errcode":0 "errmsg":"ok"}
3.3获取UserId与本地库User表比对,找到真实的用户信息
获取用户信息(账号、密码)去取token(类似用户账号、密码登录的token)