看好你的代码之 - SPSite.CatchAccessDeniedException

一个偶然的机会,在 SharePoint 中使用第三方 Ajax 框架(比如 Ajax.NET),页面上使用 Ajax 方法去访问当前用户没有权限的 SharePoint 资源(比如没有访问权限的 SPWeb,删除)......

看好你的代码之 - SPSite.CatchAccessDeniedException

先看看MSDN 官方解释

If set to true, access denied exceptions inside page requests are explicitly handled by the platform. For example, when Forms-based authentication is used, anonymous users are redirected to the login page. If the user is already authenticated, he may be redirected to an error message page such as _layouts/AccessDenied.aspx.
If you want to handle access denied exceptions with your own code, you should save the original value in a variable. Set CatchAccessDeniedException to false just before the beginning of your try block. At the end of your code, restore the original value in a finally block, so that other pieces in the system still behave the same way.

起因

一个偶然的机会,在 SharePoint 中使用第三方 Ajax 框架(比如 Ajax.NET),页面上使用 Ajax 方法去访问当前用户没有权限的 SharePoint 资源(比如没有访问权限的 SPWeb,删除只有读取权限的 SPListItem等),会引发 ASP.NET 应用程序池崩溃(对,你没有看错,就是崩溃,导致应用程序池重启),异常日志如下:

发生了未经处理的异常,已终止进程。
Application ID: /LM/W3SVC/307249834/ROOT
Process ID: 4604
Exception: System.Threading.ThreadAbortException
Message: 正在中止线程。
StackTrace:    在 System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)
   在 System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)
   在 System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)

进过排查,发现和SPSite.CatchAccessDeniedException有密切的关系,下面我就来列举一下此属性用法上的悲欢离合。

SPSite.CatchAccessDeniedException用法列举

首先介绍下 SharePiont 权限体系,简单说SharePoint 权限体系就三个元素:人(包括组)权限(包括权限级别)安全对象(如网站、列表、库、列表或库中的文件夹、项目或文件)。通常的权限授予模式是:在某个安全对象上授予某人某种权限,比如在 A 网站上给张三管理网站的权限。在 SharePoint 开箱即用的体系中,这样的权限体系虽显粗糙,但也相安无事,有权限就能访问,无权限就一刀切,比如跳转到传统的_layouts/AccessDenied.aspx页面。一旦引入了自定义开发,这种粗放式的模式可能就和业务有冲突,如果不小心使用,甚至会有坑。下面就列举几种未授权模式下结合此属性的使用方式。

示例代码场景预设:应用程序使用 Windows 认证,根网站下有名为CrashList的列表,其中包含一条列表项数据,用户test对网站拥有读取权限。网站页面下包含一个自定义控件,自定义控件的 PageLoad 事件中去删除CrashList列表的第一条数据。

不做权限处理,直接删除

protected override void OnLoad(EventArgs e)
{
     base.OnLoad(e);
     bool allowUnsafeUpdates = this.SPContext.Web.AllowUnsafeUpdates;
     this.SPContext.Web.AllowUnsafeUpdates = true;
     SPList list = this.SPContext.Web.GetList("/Lists/CrashList");
     SPListItem item = list.GetItemById(1);
     item.Recycle();
     this.SPContext.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
}

运行上面的代码,可以看到页面跳转到_layouts/AccessDenied.aspx且显示经典的提示:抱歉,此网站尚未共享。

1

由自己来处理

设想一下,如果当前页面是某种业务的业务表单,想删除某一条相关的业务数据,不小心当前用户对数据没有删除权限(凡事想到提升权限来删除的请绕道),然后 SharePoint 却托管了无权限异常后的所有操作,私自跳转到一个页面,这是多么的不爽,有没有办法自己来处理无权限异常?答案肯定是有,就是使用SPSite.CatchAccessDeniedException属性。

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    bool catchAccessDeniedException = this.SPContext.Site.CatchAccessDeniedException;
    this.SPContext.Site.CatchAccessDeniedException = false;
    bool allowUnsafeUpdates = this.SPContext.Web.AllowUnsafeUpdates;
    this.SPContext.Web.AllowUnsafeUpdates = true;
    try
    {
        SPList list = this.SPContext.Web.GetList("/Lists/CrashList");
        SPListItem item = list.GetItemById(1);
        item.Recycle();
    }
    catch (UnauthorizedAccessException)
    {
        //放开胆子,做你想做的。
    }
    finally
    {
        this.SPContext.Site.CatchAccessDeniedException = catchAccessDeniedException;
        this.SPContext.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
    }
}

运行上面代码,发现页面还是页面,似乎什么也没有发生。上面代码是官方推荐唯一代码模式,也就是一定要结合try block 来用,如果任性不呢?我们来看看。

任性的代码一

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    bool catchAccessDeniedException = this.SPContext.Site.CatchAccessDeniedException;
    this.SPContext.Site.CatchAccessDeniedException = false;  
    bool allowUnsafeUpdates = this.SPContext.Web.AllowUnsafeUpdates;
    this.SPContext.Web.AllowUnsafeUpdates = true;
    SPList list = this.SPContext.Web.GetList("/Lists/CrashList");
    SPListItem item = list.GetItemById(1);
    item.Recycle();
    this.SPContext.Site.CatchAccessDeniedException = catchAccessDeniedException;
    this.SPContext.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
}

运行上面代码,页面不会发生跳转,页面上只显示403 FORBIDDEN。如下图:

2

任性的代码二

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    bool allowUnsafeUpdates = this.SPContext.Web.AllowUnsafeUpdates;
    this.SPContext.Web.AllowUnsafeUpdates = true;
    try
    {
        SPList list = this.SPContext.Web.GetList("/Lists/CrashList");
        SPListItem item = list.GetItemById(1);
        item.Recycle();
    }
    catch (UnauthorizedAccessException)
    {
    }
    finally
    {
        this.SPContext.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
    }
}

这段代码看起来挺好,你不是有异常吗?那就 catch 住,实际结果呢?和不加 try block 一样,会由 SharePoint 框架接管,跳转到_layouts/AccessDenied.aspx页面。这就是因为少了对Site.CatchAccessDeniedException参数的处理。

黑洞级代码

之所以称之为黑洞级代码,是因为一不小心,极有可能造成应用程序池崩溃这种级别的效果,所以在实际开发过程中一定要小心应对。目前遇到的一个场景是在 SharePoint 中引入 Ajax.NET,在 Ajax 对应的服务器方法中使用下面代码,会使应用程序池崩溃,童叟无欺。

[AjaxPro.AjaxMethod]
public void DeleteItem()
{
    bool allowUnsafeUpdates = this.SPContext.Web.AllowUnsafeUpdates;
    this.SPContext.Web.AllowUnsafeUpdates = true;
    SPList list = this.SPContext.Web.GetList("/Lists/CrashList");
    SPListItem item = list.GetItemById(1);
    item.Recycle();
    this.SPContext.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
}

产生的原因是因为 Ajax.NET 使用了反射机制来处理浏览器传递过来的 Ajax 请求,让浏览器的一个 js 资源请求变为托管代码实例来处理,所以逃脱了 SharePoint 框架的处理,由于 Ajax.NET 本身没有对此类由 win32 com 出来的异常做充分准备,于是 ASP.NET 进程就遭殃了。

总结

Site.CatchAccessDeniedException参数的使用,主要以下几种场景:

  • 让 SharePoint 自己处理未授权,那么我们什么也不需要做。
  • 在实际业务中需要自己处理未授权操作带来的一系列业务方面的处理,比如准确的信息提示,未授权异常后附加业务逻辑处理,则需要设置此属性为false,且代码需要使用 try block。
  • 在其他基于 SharePoint 平台上的第三方框架中使用,尽量使用自己处理的模式,避免无妄之灾。