Upgrade Nuget
大石头 编写于 2024-09-28 08:10:09
NewLife.Cube
using System.Web;
using NewLife.Cube.Entity;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Serialization;
using NewLife.Web;
using XCode.Membership;

namespace NewLife.Cube.Web;

/// <summary>钉钉身份验证提供者</summary>
/// <remarks>
/// 平台 https://open-dev.dingtalk.com/
/// </remarks>
public class DingTalkClient : OAuthClient
{
    private static Int32 _show;
    static void ShowHelp()
    {
        if (_show > 0 || Interlocked.CompareExchange(ref _show, 1, 0) != 0) return;

        // 输出帮助日志
        XTrace.WriteLine("钉钉登录分多种方式,由Scope参数区分。");
        XTrace.WriteLine("Scope=snsapi_qrlogin, 扫码登录");
        XTrace.WriteLine("Scope=snsapi_login, 密码登录");
        XTrace.WriteLine("Scope=snsapi_auth, 钉钉内免登");
    }

    /// <summary>实例化</summary>
    public DingTalkClient()
    {
        Name = "Ding";
        Server = "https://oapi.dingtalk.com/connect/oauth2/";

        AuthUrl = "sns_authorize?appid={key}&response_type=code&scope={scope}&state={state}&redirect_uri={redirect}";
        AccessUrl = "https://oapi.dingtalk.com/sns/getuserinfo_bycode?accessKey={key}&timestamp={timestamp}&signature={signature}";
        OpenIDUrl = null;
        UserUrl = "https://oapi.dingtalk.com/user/get?access_token={token}&userid={userid}";
    }

    /// <summary>应用参数</summary>
    /// <param name="mi"></param>
    public override void Apply(OAuthConfig mi)
    {
        base.Apply(mi);

        SetMode(Scope);
    }

    /// <summary>设置工作模式</summary>
    /// <param name="mode"></param>
    public virtual void SetMode(String mode)
    {
        switch (mode)
        {
            // 扫码登录
            case "snsapi_qrlogin":
                Server = "https://oapi.dingtalk.com/connect/";
                AuthUrl = "qrconnect?appid={key}&response_type=code&scope=snsapi_login&state={state}&redirect_uri={redirect}";
                break;
            // 密码登录
            case "snsapi_login":
                Server = "https://oapi.dingtalk.com/connect/oauth2/";
                AuthUrl = "sns_authorize?appid={key}&response_type=code&scope=snsapi_login&state={state}&redirect_uri={redirect}";
                break;
            // 钉钉内免登
            case "snsapi_auth":
                Server = "https://oapi.dingtalk.com/connect/oauth2/";
                AuthUrl = "sns_authorize?appid={key}&response_type=code&scope=snsapi_auth&state={state}&redirect_uri={redirect}";
                break;
            default:
                break;
        }
    }

    /// <summary>是否支持指定用户端,也就是判断是否在特定应用内打开,例如QQ/DingDing/WeiXin</summary>
    /// <remarks>
    /// Mozilla/5.0 (Linux; U; Android 10; zh-CN; YAL-AL10 Build/HUAWEIYAL-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 UWS/3.22.0.36 Mobile Safari/537.36 AliApp(DingTalk/6.0.7) com.alibaba.android.rimet/14599822 Channel/227200 language/zh-CN UT4Aplus/0.2.25 colorScheme/light
    /// </remarks>
    /// <param name="userAgent"></param>
    /// <returns></returns>
    public override Boolean Support(String userAgent) => !userAgent.IsNullOrEmpty() && userAgent.Contains("DingTalk/");

    /// <summary>针对指定客户端进行初始化</summary>
    /// <param name="userAgent"></param>
    public override void Init(String userAgent)
    {
        // 钉钉内打开时,自动切换为应用内免登
        if (Support(userAgent))
        {
            Scope = "snsapi_auth";
            SetMode(Scope);
        }
    }

    /// <summary>获取令牌</summary>
    /// <param name="code"></param>
    /// <returns></returns>
    public override String GetAccessToken(String code)
    {
        ShowHelp();

        var url = AccessUrl;
        if (url.IsNullOrEmpty()) throw new ArgumentNullException(nameof(UserUrl), "未设置用户信息地址");

        var ts = DateTime.UtcNow.ToLong() + "";
        var sign = ts.GetBytes().SHA256(Secret.GetBytes()).ToBase64();
        url = url.Replace("{timestamp}", ts).Replace("{signature}", HttpUtility.UrlEncode(sign));

        url = GetUrl(nameof(GetAccessToken), url);

        var tmp_code = new { tmp_auth_code = code };
        WriteLog("GetUserInfo {0} {1}", url, tmp_code.ToJson());

        // 请求OpenId
        var http = CreateClient();
        var dic = Task.Run(() => http.InvokeAsync<IDictionary<String, Object>>(HttpMethod.Post, url, tmp_code, null, "user_info")).Result;

        if (dic != null)
        {
            XTrace.WriteLine(dic.ToJson());

            //NickName = dic["nick"] as String;
            //OpenID = dic["openid"] as String;
            //UnionID = dic["unionid"] as String;

            Items = dic.ToDictionary(e => e.Key, e => e.Value as String);

            OnGetInfo(Items);
        }

        return null;
    }

    private static String _ding_access_token;
    private static DateTime _ding_expire;
    /// <summary>获取用户信息</summary>
    /// <returns></returns>
    public override String GetUserInfo()
    {
        //return base.GetUserInfo();

        if (UnionID.IsNullOrEmpty()) return null;

        // 如果配置了企业级账号,可以获取更详细信息
        var token = _ding_access_token;
        if (token.IsNullOrEmpty() || _ding_expire < DateTime.Now)
        {
            var key = Parameter.GetOrAdd(0, "钉钉", "appkey");
            var secret = Parameter.GetOrAdd(0, "钉钉", "appsecret");

            _ding_access_token = null;
            if (!key.Value.IsNullOrEmpty() && !secret.Value.IsNullOrEmpty())
            {
                token = _ding_access_token = GetToken(key.Value, secret.Value);
            }

            _ding_expire = DateTime.Now.AddSeconds(7200 - 60);
        }

        if (!token.IsNullOrEmpty())
        {
            try
            {
                // 根据UnionId换取员工Id
                var userid = GetUseridByUnionid(token, UnionID);
                if (!userid.IsNullOrEmpty())
                {
                    // 钉钉Id一般不是自己设置的,很乱,不可取
                    //client.UserName = userid;

                    //client.GetUserInfo(token, userid);
                    GetUserInfo(token, userid);
                }
            }
            catch (AggregateException ex)
            {
                // 某些用户不是本团队成员,此处会抛出异常
                if (ex.GetTrue() is not ApiException) throw;
            }
        }

        return null;
    }

    #region 服务端Api
    /// <summary>企业内部应用获取凭证,有效期7200秒</summary>
    /// <param name="appkey"></param>
    /// <param name="appsecret"></param>
    /// <returns></returns>
    public static String GetToken(String appkey, String appsecret)
    {
        var url = $"https://oapi.dingtalk.com/gettoken?appkey={appkey}&appsecret={appsecret}";

        var http = new DingTalkClient().CreateClient();
        return Task.Run(() => http.InvokeAsync<String>(HttpMethod.Get, url, null, null, "access_token")).Result;
    }

    /// <summary>企业内部应用获取用户信息</summary>
    /// <param name="access_token"></param>
    /// <param name="userid">员工id</param>
    /// <returns></returns>
    public IDictionary<String, Object> GetUserInfo(String access_token, String userid)
    {
        var url = UserUrl.Replace("{token}", access_token).Replace("{userid}", userid);

        var http = CreateClient();
        var buf = Task.Run(() => http.GetAsync<Byte[]>(url)).Result;
        var str = buf.ToStr();
        var js = new JsonParser(str).Decode() as IDictionary<String, Object>;

        UserName = js["name"] + "";
        NickName = js["nick"] + "";
        Avatar = js["avatar"] + "";
        Mail = js["email"] + "";
        Mobile = js["mobile"] + "";
        Code = js["jobnumber"] + "";

        // 合并字典
        var dic = Items;
        if (dic == null)
            Items = js.ToDictionary(e => e.Key, e => e.Value as String);
        else
        {
            foreach (var item in js)
            {
                if (!dic.ContainsKey(item.Key)) dic[item.Key] = item.Value as String;
            }
        }

        return js;
    }

    ///// <summary>填充信息</summary>
    ///// <param name="user"></param>
    //public override void Fill(IManageUser user)
    //{
    //    var dic = Items;
    //    if (dic != null && user is User user2)
    //    {
    //        if (user2.Mail.IsNullOrEmpty() && dic.TryGetValue("email", out var email)) user2.Mail = email;
    //        if (user2.Mobile.IsNullOrEmpty() && dic.TryGetValue("mobile", out var mobile)) user2.Mobile = mobile;
    //        if (user2.Code.IsNullOrEmpty() && dic.TryGetValue("jobnumber", out var code)) user2.Code = code;
    //    }

    //    base.Fill(user);
    //}

    /// <summary>根据unionid获取userid</summary>
    /// <param name="access_token"></param>
    /// <param name="unionId">员工在当前开发者企业账号范围内的唯一标识,系统生成,固定值,不会改变</param>
    /// <returns></returns>
    public static String GetUseridByUnionid(String access_token, String unionId)
    {
        var url = $"https://oapi.dingtalk.com/user/getUseridByUnionid?access_token={access_token}&unionid={unionId}";

        var http = new DingTalkClient().CreateClient();
        return Task.Run(() => http.InvokeAsync<String>(HttpMethod.Get, url, null, null, "userid")).Result;
    }
    #endregion
}