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%40gyoss9
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: #34;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)
(此處已添加書籍卡片,請到今日頭條用戶端檢視)
(此處已添加紀錄片卡片,請到今日頭條用戶端檢視)