本文讲述如何在SharePoint 2013/2010 中根据当前用户的某个属性过滤搜索结果。
最近客户有一个需求,就是根据用户所在的国家(User Info List里面有Country字段),在搜索时只显示该用户所在国家的记录(对应的list 有Country 字段)。
一般来说SharePoint 搜索是根据当前用户的权限来决定是否可以搜索到对应的记录,但是过是这样的话,需要将列表的所有记录都打破权限记录,这是非常损耗性能的,而且这样的权限结构维护起来很复杂。
本文将使用 ISecurityTrimmerPost 来实现,根据微软官方的文档说明,这个接口是用于在返回之前过滤查询结果的:
1. 在管理中心新建一个Crawl Rule, ISecurityTrimmerPost 必须挂接在某个Crawl
Rule,只有匹配这个Crawl Rule的查询结果才会调用ISecurityTrimmerPost 去检查:
2. 启动一个 Full Crawl (否则Crawl Rule不会生效)
3. 新建一个SharePoint farm solution, 命名为 CustomSecurityPostTrimmer
4. SharePoint List item搜索结果的地址格式为
sts4://msstoresp12013/siteurl=sites/ap/siteid={cb7ed81a-cce4-4b54-83a8-3b1eadcf2611}/weburl=/webid={199327ad-b16a-4c92-86cd-ec684c847234}/listid={39c30ef9-80c4-46bf-b391-028681191ca0}/folderurl=/itemid=22
这个地址不是程序可以理解的地址,因此需要创建一个STS4Adress类来解吸这个地址 STS4Adress.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace CustomSecurityTrimmerSample { class STS4Adress { public string SiteUrl { get; private set; } public string WebUrl { get; private set; } public Guid ListId { get; private set; } public int ItemId { get; private set; } public bool Matched { get; private set; } public STS4Adress(string adress) { string prefix = "sts4://"; int prefixStartIndex = adress.IndexOf(prefix); if (prefixStartIndex >= 0) { try { int firstEqual = adress.IndexOf("="); int endHost = adress.Substring(0, firstEqual).LastIndexOf("/"); string host = adress.Substring(prefixStartIndex + prefix.Length, endHost - (prefixStartIndex + prefix.Length)); // siteurl= string siteUrlPrefix = "siteurl="; int siteUrlStartIndex = adress.IndexOf(siteUrlPrefix); int nextEqual = adress.IndexOf("=", siteUrlStartIndex + siteUrlPrefix.Length); int siteUrlEndIndex = adress.Substring(0, nextEqual).LastIndexOf("/"); this.SiteUrl = "http://" + host + "/" + adress.Substring(siteUrlStartIndex + siteUrlPrefix.Length, siteUrlEndIndex - (siteUrlStartIndex + siteUrlPrefix.Length)); // weburl= string weburlPrefix = "weburl="; int weburlStartIndex = adress.IndexOf(weburlPrefix); nextEqual = adress.IndexOf("=", weburlStartIndex + weburlPrefix.Length); int webUrlEndIndex = adress.Substring(0, nextEqual).LastIndexOf("/"); this.WebUrl = adress.Substring(weburlStartIndex + weburlPrefix.Length, webUrlEndIndex - (weburlStartIndex + weburlPrefix.Length)); // listid= string listIdPrefix = "listid="; int listIdStartIndex = adress.IndexOf(listIdPrefix); nextEqual = adress.IndexOf("=", listIdStartIndex + listIdPrefix.Length); int listIdEndIndex = adress.Substring(0, nextEqual).LastIndexOf("/"); this.ListId = new Guid(adress.Substring(listIdStartIndex + listIdPrefix.Length, listIdEndIndex - (listIdStartIndex + listIdPrefix.Length))); // itemid= string itemIdPrefix = "itemid="; int itemIdStartIndex = adress.IndexOf(itemIdPrefix); this.ItemId = int.Parse(adress.Substring(itemIdStartIndex + itemIdPrefix.Length)); Matched = true; } catch (Exception ex) { this.Matched = false; } } else { this.Matched = false; } } } }
4. 创建CustomSecurityPostTrimmer 来实现 ISecurityTrimmerPost, CustomSecurityPostTrimmer.cs:
using Microsoft.IdentityModel.Claims; using Microsoft.Office.Server.Search.Administration; using Microsoft.Office.Server.Search.Query; using Microsoft.SharePoint; using Microsoft.SharePoint.Administration.Claims; using Microsoft.SharePoint.Taxonomy; using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Security.Principal; using System.Text; using System.Threading.Tasks; namespace CustomSecurityTrimmerSample { class CustomSecurityPostTrimmer : ISecurityTrimmerPost { private const string userInfoCaml = @"<Where> <Contains><FieldRef Name=‘Name‘/> <Value Type=‘Text‘>{0}</Value></Contains> </Where>"; public void Initialize(NameValueCollection staticProperties, SearchServiceApplication searchApplication) { } public BitArray CheckAccess(IList<string> documentCrawlUrls, IList<string> documentAcls, IDictionary<string, object> sessionProperties, IIdentity passedUserIdentity) { var urlStatusArray = new BitArray(documentCrawlUrls.Count, true); try { //CheckAccess method implementation, see steps 3-5. if (documentCrawlUrls == null) { // throw new ArgumentException("CheckAccess method is called with invalid URL list", "documentCrawlUrls"); return urlStatusArray; } if (documentAcls == null) { // throw new ArgumentException("CheckAccess method is called with invalid documentAcls list", "documentAcls"); } if (passedUserIdentity == null) { return urlStatusArray; } // Initialize the bit array with TRUE value which means all results are visible by default. var claimsIdentity = (IClaimsIdentity)passedUserIdentity; if (claimsIdentity != null) { // var userGroups = GetGroupList(claimsIdentity.Claims); string loginName = GetLonginName(claimsIdentity.Claims); var numberDocs = documentCrawlUrls.Count; for (var i = 0; i < numberDocs; ++i) { // try to parse the url Helper.WriteLog("documentCrawlUrls:" + documentCrawlUrls[i], "CustomSecurityPostTrimmer"); STS4Adress sts4Adress = new STS4Adress(documentCrawlUrls[i]); if (sts4Adress.Matched) { string template = "sts4Adress.SiteUrl{0}, sts4Adress.WebUrl{1}, sts4Adress.ListId{2}, sts4Adress.ItemId{3}, documentCrawlUrls:{4}"; string log = string.Format(template, sts4Adress.SiteUrl, sts4Adress.WebUrl, sts4Adress.ListId, sts4Adress.ItemId, documentCrawlUrls[i]); Helper.WriteLog(log, "CustomSecurityPostTrimmer"); using (SPSite site = new SPSite(sts4Adress.SiteUrl)) { if (site.RootWeb.SiteUserInfoList.Fields.ContainsFieldWithStaticName("Country")) { SPWeb web = site.OpenWeb(sts4Adress.WebUrl); SPList list = web.Lists[sts4Adress.ListId]; if ((list.Fields.ContainsFieldWithStaticName("Country")) { // get the current user‘s country and storetype SPQuery userInfoQuery = new SPQuery(); userInfoQuery.ViewFields = @"<FieldRef Name=‘Country‘ />"; userInfoQuery.Query = string.Format(userInfoCaml, loginName); SPListItemCollection userInfoItems = site.RootWeb.SiteUserInfoList.GetItems(userInfoQuery); if (userInfoItems.Count > 0) { string currentCountry = userInfoItems[0]["Country"] == null ? null : userInfoItems[0]["Region"].ToString().Trim(); if ((currentCountry != null)) { SPItem currentItem = list.GetItemById(sts4Adress.ItemId); List<string> countries = GetTaxonomyFieldValueCollection(currentItem, "Country"); if (currentCountry != null && countries.Count != 0 && !countries.Contains(currentCountry)) { urlStatusArray[i] = false; } } } } } } } } } } catch (Exception ex) { Helper.WriteException(ex, "CustomSecurityPostTrimmer"); } return urlStatusArray; } public List<string> GetTaxonomyFieldValueCollection(SPItem item, string field) { List<string> result = new List<string>(); if (item[field] != null) { if ((item[field] as TaxonomyFieldValue) != null) { result.Add((item[field] as TaxonomyFieldValue).Label); } else if ((item[field] as TaxonomyFieldValueCollection) != null) { TaxonomyFieldValueCollection taxonomyFieldValues = item[field] as TaxonomyFieldValueCollection; foreach (TaxonomyFieldValue taxonomyFieldValue in taxonomyFieldValues) { result.Add(taxonomyFieldValue.Label); } } } return result; } public string GetLonginName(ClaimCollection claims) { string loginName = string.Empty; foreach (var claim in claims) { if (SPClaimTypes.Equals(claim.ClaimType, SPClaimTypes.UserLogonName)) { loginName = claim.Value; break; } } return loginName; } } }
CheckAccess返回的urlStatusArray决定每条记录现实与否,true,表示现实, flase表示不现实
5. 部署改解决方案
6.注册CustomSecurityPostTrimmer,以管理员身份运行SharePoint
2013 Management Shell, 执行下列命令:
New-SPEnterpriseSearchSecurityTrimmer -SearchApplication "Search Service Application" -typeName "CustomSecurityTrimmerSample.CustomSecurityPostTrimmer, CustomSecurityTrimmerSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=472c45eb3e5aacc9" -RulePath "http://*"
注意替换 PublicKeyToken
CustomSecurityTrimmerSample 为dll 的名
CustomSecurityTrimmerSample.CustomSecurityPostTrimmer为实现了ISecurityTrimmerPost的类名
可以通过下列命令来查看客户化的SecurityTrimmer
$searchApp = Get-SPEnterpriseSearchServiceApplication $searchApp | Get-SPEnterpriseSearchSecurityTrimmer
可以通过 Remove-SPEnterpriseSearchSecurityTrimmer 来删除SecurityTrimmer,如:
$searchApp = Get-SPEnterpriseSearchServiceApplication $trimmer = $searchApp | Get-SPEnterpriseSearchSecurityTrimmer Remove-SPEnterpriseSearchSecurityTrimmer -identity $trimmer
7. 以不同用户搜索不同的结果:
a. 没有指定Country 的用户可以搜索到所有记录:
b. 指定Country为US的用户,只能搜索到和US匹配的记录:
8.使用本方案的缺点
a. 用户通过搜索list view等显示有不匹配item的页面,点击进入这些页面后仍然可以看到不匹配item
b. SharePoint 2013 调用ISecurityTrimmerPost后不会重新分页,也就是说,本来没有调用ISecurityTrimmerPost之前当前页有10条记录,但有5条不匹配当前用户的属性(Country),当前用户只能看到五条记录,尽管可能下一页还有内容,SharePoint
2013不会把下也内容补齐到本页,也就是说可能有极端情况当前页的10条记录都不匹配,用户可能在当前页什么都看不到,但可以点下一页,看到有搜索结果。
c.
由于在返回搜索结果给用户之前,需要运行检查搜索记录是否匹配当前用户属性(Country)的代码,会降低搜索性能。