天天看點

ASP.NET Core 學習記錄 - AbpVnext 擴充企業微信掃碼登入-Vue架構

作者:老碼農

1、開始開發

https://developer.work.weixin.qq.com/document/path/91025           
企業微信提供了OAuth的掃碼登入授權方式,可以讓企業的網站在浏覽器内打開時,引導成員使用企業微信掃碼登入授權,進而擷取成員的身份資訊,免去登入的環節。(注:此授權方式需要使用者掃碼,不同于“網頁授權登入”;僅企業内可以使用此種授權方式,第三方服務商不支援使用。)在進行企業微信授權登入之前,需要先在企業的管理端背景建立一個具備“企業微信授權登入”能力的應用。

1.1 企業微信掃碼登陸接入流程

ASP.NET Core 學習記錄 - AbpVnext 擴充企業微信掃碼登入-Vue架構

1.2 開啟網頁授權登陸

登入 企業管理端背景->進入需要開啟的自建應用->點選 “企業微信授權登入”,進入如下頁面

ASP.NET Core 學習記錄 - AbpVnext 擴充企業微信掃碼登入-Vue架構

然後點選 "設定授權回調域",輸入回調域名,點選“儲存”。(域名:需要找運維做解析)

要求配置的授權回調域,必須與通路連結的域名完全一緻,如下圖:

ASP.NET Core 學習記錄 - AbpVnext 擴充企業微信掃碼登入-Vue架構

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

是不是發現這個連結熟悉,沒錯就是上面“@@登陸順序”部分,前端按之前賬号、密碼登陸方式調用即可,切換為下面的參數,後續同@@登陸順序部分一緻。

ASP.NET Core 學習記錄 - AbpVnext 擴充企業微信掃碼登入-Vue架構

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)

(此處已添加書籍卡片,請到今日頭條用戶端檢視)

(此處已添加紀錄片卡片,請到今日頭條用戶端檢視)