首页 > 代码库 > SharePoint 2013/2010 根据当前用户的某个属性过滤搜索结果

SharePoint 2013/2010 根据当前用户的某个属性过滤搜索结果

本文讲述如何在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)的代码,会降低搜索性能。



SharePoint 2013/2010 根据当前用户的某个属性过滤搜索结果