[]
以上的章节分模块向您介绍了安全提供程序的核心功能,为了更好地与第三方系统对接,本章节,我们将向您提供一些常见的安全提供程序的核心代码,方便您开发时参考使用。
通过本节内容,您将可以了解到:
钉钉的安全提供程序的核心代码
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 + "×tamp=" + 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. 登录是通过请求参数验证用户是否登录。
[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 域下的用户组织角色信息。