[]
        
(Showing Draft Content)

附加资源

以上的章节分模块向您介绍了安全提供程序的核心功能,为了更好地与第三方系统对接,本章节,我们将向您提供一些常见的安全提供程序的核心代码,方便您开发时参考使用。

通过本节内容,您将可以了解到:

  • 钉钉的安全提供程序的核心代码

  • Windows域认证的安全提供程序的核心代码

钉钉的安全提供程序的核心代码

public class DingTalkSecurityProvider : ISecurityProvider, IOpenIdSecurityProvider, ISupportSettings, IVerifyUserAsync
{
    public string Name => "钉钉企业内部安全提供程序";

    /// <summary>
    /// 使用 OpenId 认证
    /// </summary>
    public AuthenticationType AuthenticationType => AuthenticationType.OpenId;

    /// <summary>
    /// 使用 InMemoryCache 存储
    /// </summary>
    public UserInformationStorageMode UserInformationStorageMode => UserInformationStorageMode.InMemoryCache;

    /// <summary>
    /// 存在4个认证参数
    /// </summary>
    public List<SecurityProviderSettings> Settings
    {
        get
        {
            // 钉钉的OAuth 协议要求配置 4 个认证相关的参数
            var config = LoadConfig();
            var settings = new List<SecurityProviderSettings>
            {
                new()
                {
                    Name = "Appkey",
                    Value = config.Appkey
                },
                new()
                {
                    Name = "Secret",
                    Value = config.Secret,
                    Editor = new PasswordEditor()
                },
                new()
                {
                    Name = "AppId",
                    Value = config.AppId
                },
                new()
                {
                    Name = "AppSecret",
                    Value = config.AppSecret,
                    Editor = new PasswordEditor()
                }
            };

            return settings;
        }
    }

    /// <summary>
    /// 返回用户信息
    /// </summary>
    public UserInformations UserInformations
    {
        get
        {
            return GetUserInformationsAsync().Result;
        }
    }

    /// <summary>
    /// 调用 钉钉 接口读取所有用户相关数据
    /// 内部代码细节省略,只保留核心逻辑
    /// </summary>
    private async Task<UserInformations> GetUserInformationsAsync()
    {
        // 创建返回对象
        var userInfo = new UserInformations();

        // 获取部门相关数据,实现省略
        var departments = await GetDepartmentListAsync();
        
        // 获取用户相关数据,钉钉的用户数据依赖组织信息
        var users = await GetUsersAsync(departments);

        // 获取角色相关数据,实现省略
        var roles = await GetRolesAsync();

        // 将用户加入角色中
        AddUsersToRoles(userDic, fUserDic, fRoleDic);

        // 添加角色到返回对象中
        AddRoles(userInfo, fRoleDic.Values);

        // 添加组织到返回对象中
        foreach (var rootDepartmentId in rootDepartmentIds)
        {
            userInfo.Organizations.Add(fOrganizationDic[rootDepartmentId]);
        }

        // 返回结果
        return userInfo;
    }

    /// <summary>
    /// 调用 钉钉 接口按 部门读取用户数据
    /// 内部代码细节省略,只保留核心逻辑
    /// </summary>
    private async Task<List<User>> GetUsersAsync(List<Department> departments)
    {
        // 按照钉钉接口协议,读取信息需要验证登录人的 access_token,因此需要读取 accessToken
        var accessToken = await GetAccessTokenAsync();

        var users = new List<User>();
        var userIds = new HashSet<string>();
        foreach (var i in departments)
        {
            var request = new UserIdListRequest() { dept_id = i.id };

            // 通过 access_token 读取用户列表
            var response = await PostInfoAsync<UserIdListResponse>("https://oapi.dingtalk.com/topapi/user/listid?access_token=" + accessToken, request);

            foreach (var userId in response.result.userid_list)
            {
                userIds.Add(userId);
            }
        }

        foreach (var key in userIds)
        {
            // 通过 access_token 读取用户详情
            var userDetails = await PostInfoAsync<UserDetailsResponse>("https://oapi.dingtalk.com/topapi/v2/user/get?access_token=" + accessToken, new UserDetailsRequest() { userid = key });
            users.Add(userDetails.result);
        }

        return users;
    }

    /// <summary>
    /// 获取 AccessToken 用以访问 钉钉平台
    /// </summary>
    private async Task<string> GetAccessTokenAsync()
    {
        // 加载安全提供程序参数配置中的 AppId 与 SecretKey 相关的参数
        var config = LoadConfig();
        // 创建 Http Request
        var http = new HttpUtils();
        var result = await http.DoGetAsync("https://oapi.dingtalk.com/gettoken?appkey=" + config.Appkey + "&appsecret=" + config.Secret);

        // 将 Response 转换为 JObject 读取 access_token
        var jd = JsonConvert.DeserializeObject(result) as JObject;
        return jd["access_token"].ToString();
    }

    /// <summary>
    /// 同步 VerifyUser 方法不实现 
    /// </summary>
    public GrapeCity.Forguncy.SecurityProvider.User VerifyUser(Dictionary<string, string> properties)
    {
        throw new Exception("Please use VerifyUserAsync");
    }

    /// <summary>
    /// 实现 IVerifyUserAsync 接口验证并获取用户详情
    /// </summary>
    public async Task<GrapeCity.Forguncy.SecurityProvider.User> VerifyUserAsync(Dictionary<string, string> properties)
    {
        // 构建请求参数
        var code = properties["code"];
        var accessToken = await GetAccessTokenAsync();
        var config = LoadConfig();
        string timestamp = GetTimeStamp().ToString();
        string accessKey = config.AppId;
        string appSecret = config.AppSecret;
        var codeJson = new JObject();
        codeJson["tmp_auth_code"] = code;
        string signature = hash_hmac2(timestamp, appSecret);
        signature = UrlEncode(signature);

        // 调用 钉钉 接口获取用户登录信息
        var url = "https://oapi.dingtalk.com/sns/getuserinfo_bycode?signature=" + signature + "&timestamp=" + timestamp + "&accessKey=" + accessKey;
        var result = await (new HttpUtils()).DoPostAsync(url, codeJson.ToString());
        var response = JsonConvert.DeserializeObject<LoginResponse>(result);

        // 通过登录信息读取用户详细信息
        var request = new UserIdRequest() { unionid = response.user_info.unionid };
        UserIdResponse userIdResponse = await PostInfoAsync<UserIdResponse>("https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=" + accessToken, request);

        // 返回用户信息到活字格服务器
        return new GrapeCity.Forguncy.SecurityProvider.User()
        {
            UserId = userIdResponse.result.userid
        };
    }

    /// <summary>
    /// 实现 IOpenIdSecurityProvider 接口返回第三方登录接口
    /// </summary>
    public string GetRedirectUrl(string redirect_uri, string state, string userAgent)
    {
        // 获取 钉钉 OAuth2 协议下的登录地址
        var queryString = System.Web.HttpUtility.ParseQueryString(string.Empty);
        var config = LoadConfig();
        queryString["appid"] = config.AppId;
        queryString["response_type"] = "code";
        queryString["scope"] = "snsapi_auth";
        queryString["state"] = state;
        // 在登录接口中加入了回调地址,此地址是活字格服务器中的一个用户活字格站点登录的地址
        queryString["redirect_uri"] = redirect_uri;
        return "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?" + queryString.ToString(); // Returns "key1=value1&key2=value2", all URL-encoded
    }
}

从代码中我们可以知道:

  • 钉钉集成使用了 OpenId 的认证模式,因此安全提供程序需要实现 IOpenIdSecurityProvider 接口返回 OAuth 登录地址;

  • 钉钉的 OAuth 接口需要大量的认证相关的参数,因此需要实现 ISupportSettings 在管理控制平台配置相关参数;

  • 为了提供性能使用到了 IVerifyUserAsync 接口;

  • 安全提供程序只有两个任务:1. 请求钉钉的接口获取用户数据; 2. 登录是通过请求参数验证用户是否登录。

Windows域认证的安全提供程序的核心代码

[SupportedOSPlatform("windows")]
public class ADSecurityProvider : ISecurityProvider, ISupportSettings
{
    /// <summary>
    /// AD 表示 ActiveDirectory 是微软开发的目录服务
    /// Windows 域相关的数据都需要通过此服务进行读取
    /// </summary>
    public string Name => "ADSecurityProvider";

    /// <summary>
    /// 使用 WindowsAuthentication 认证
    /// </summary>
    public AuthenticationType AuthenticationType => AuthenticationType.WindowsAuthentication;

    /// <summary>
    /// 使用 InMemoryCache 存储
    /// </summary>
    public UserInformationStorageMode UserInformationStorageMode => UserInformationStorageMode.InMemoryCache;

    /// <summary>
    /// Windows域安全提供程序可以配置对域的查询相关策略参数
    /// </summary>
    public List<SecurityProviderSettings> Settings
    {
        get
        {
            // 省略 对域的查询相关策略参数
        }
    }

    /// <summary>
    /// 通过 ActiveDirectory Service 读取 Windows 域下的用户数据
    /// 只保留核心逻辑,具体实现细节省略
    /// </summary>
    public UserInformations UserInformations
    {
        get
        {
            // 结果对象
            var userInformation = new UserInformations();

            // 获取 AD 服务实例
            var ad = GetAd(LoadConfig());

            // 通过 AD Service 读取用户信息
            AddUserToGroup(ad, userDic, roleDic);

            // 通过 AD Service 读取组织信息
            AddOrganizations(userInformation.Organizations, ad.Organizations, userDic);

            return userInformation;
        }
    }

    private AD GetAd(Config config)
    {
        // 获取 Active Directory 中的 DirectoryEntry
        DirectoryEntry domain = GetDomainDirectoryEntry(CurrentDomainName);

        var ad = new AD(config);
        
        // 省略通过各种 Active Directory 接口查询 Windows 域下的用户数据
        domain.Dispose();
        return ad;
    }

    private DirectoryEntry GetDomainDirectoryEntry(string domainName)
    {
        string[] domains = domainName.Split(new char[] { '.' });
        StringBuilder ldapStr = new StringBuilder();
        ldapStr.Append("LDAP://");
        // 省略构建 LDAP 协议地址
        DirectoryEntry domain = new DirectoryEntry(ldapStr.ToString());
        return domain;
    }

    /// <summary>
    /// 无需实现 VerifyUser 方法
    /// 因为 WindowsAuthentication 认证模式中,登录逻辑被 Windows 域自动实现
    /// </summary>
    public User VerifyUser(Dictionary<string, string> properties)
    {
        return null;
    }
}

从代码中我们可以知道:

  • Windows域认证模式需要使用 WindowsAuthentication 的认证模式,因此无需实现 VerifyUser 方法交由Windows域完成用户登录认证;

  • Windows域可以配置对域的查询策略,因此实现了 ISupportSettings 接口;

  • 安全提供程序只有一个任务:通过 Active Directory 服务的相关接口,查询 Windows 域下的用户组织角色信息。