1、开始开发

https://developer.work.weixin.qq.com/document/path/91025

企业微信提供了OAuth的扫码登录授权方式,可以让企业的网站在浏览器内打开时,引导成员使用企业微信扫码登录授权,从而获取成员的身份信息,免去登录的环节。(注:此授权方式需要用户扫码,不同于“网页授权登录”;仅企业内可以使用此种授权方式,第三方服务商不支持使用。)在进行企业微信授权登录之前,需要先在企业的管理端后台创建一个具备“企业微信授权登录”能力的应用。

1.1 企业微信扫码登陆接入流程

asp.net4.5基础教程(ASP.NETCore学习记录)(1)

1.2 开启网页授权登陆

登录 企业管理端后台->进入需要开启的自建应用->点击 “企业微信授权登录”,进入如下页面

asp.net4.5基础教程(ASP.NETCore学习记录)(2)

然后点击 "设置授权回调域",输入回调域名,点击“保存”。(域名:需要找运维做解析)

要求配置的授权回调域,必须与访问链接的域名完全一致,如下图:

asp.net4.5基础教程(ASP.NETCore学习记录)(3)

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

是不是发现这个链接熟悉,没错就是上面“@@登陆顺序”部分,前端按之前账号、密码登陆方式调用即可,切换为下面的参数,后续同@@登陆顺序部分一致。

asp.net4.5基础教程(ASP.NETCore学习记录)(4)

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)

,