本文讲述如何在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://SharePointHostHeader/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("<span style="font-family:Consolas;">Country</span>"))
{
SPWeb web = site.OpenWeb(sts4Adress.WebUrl);
SPList list = web.Lists[sts4Adress.ListId];
if ((list.Fields.ContainsFieldWithStaticName("<span style="font-family:Consolas;">Country</span>"))
{
// 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]["<span style="font-family:Consolas;">Country</span>"] == 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)的代码,会降低搜索性能。