如何捕获 SharePoint 2007/2010 的 403 Forbidden 的真实错误

存在的问题

在 SharePoint 2007 上你可能会碰到莫名其妙的 403 Forbidden 错误,SharePoint 丢出这个错误时简单得不能再简单,空白一片就 403 Forbidden 两个词,如下图

expose-sp-403-01

你 还可能看到是“网站拒绝显示此网页(The website declined to show this webpage) ” 的错误信息,如下图。你可以通过 IE 菜单栏 > 工具 > Internet 选项 > 高级 > 浏览 > 去掉“显示友好 HTTP 错误信息” 来看到内部错误。

expose-sp-403-03

可 惜的是,这个简单一点都不美,而且还很可恨。因为我们不知道到底背后发生了什么错误,如果此时能在系统日志或 SharePoint 日志(12/logs下)找到详细错误还好点(实际上从下面的源码分析中,对于 403 错误,SharePoint 没有机会写ULS日志,因为直接 Response.End 了),否则就郁闷了,可能得调试诊断半天甚至几天才能找到问题所在。

解密这个问题

我 曾经就有刚加载一个自定义 WebPart 之后就 403,因为 Web Part 比较复杂,找到不跟踪信息,只好一个方法一个方法的去注释,去调试,最终发现是写一个没有权限的文件引起的(抛出 UnauthorizedAccessException),足足花了一个下午。因此,猜想 UnauthorizedAccessException 可能是被 SharePoint 截获了,然后就直接就返回 403 了。发现 sharepoint 的站点 web.config 中,其 httpModules 配置节移除了所有继承的 module,而将 Microsoft.SharePoint.ApplicationRuntime.SPRequestModule 配置为第一个 module:

1 < httpmodules >
2    < clear >  
3    < add type = "Microsoft.SharePoint.ApplicationRuntime.SPRequestModule, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" name = "SPRequest" >
4    < add type = "System.Web.Caching.OutputCacheModule" name = "OutputCache" >
5    < add type = "System.Web.Security.FormsAuthenticationModule" name = "FormsAuthentication" >
6    <!-- omitted ... ->
7 </ add ></ add ></ add ></ clear ></ httpmodules >

通过 Reflector 查看 Microsoft.SharePoint.ApplicationRuntime.SPRequestModule 类源码,可以看到作为 HttpApplication.Error 的处理程序的 ErrorAppHandler 方法中确实对 UnauthorizedAccessException 异常进行了特殊处理:

01 private void ErrorAppHandler( object oSender, EventArgs ea)
02 {
03      HttpApplication app = oSender as HttpApplication;
04      if (app != null )
05      {
06          HttpContext context = app.Context;
07          if (context != null )
08          {
09              Exception error = context.Error;
10              if (error != null )
11              {
12                  context.Items[ "HttpHandlerException" ] = "1" ;
13                  while (error.InnerException != null )
14                  {
15                      error = error.InnerException;
16                  }
17                  if (error is UnauthorizedAccessException)
18                  {
19                      SPUtilityInternal.Send403(context.Response);
20                  }
21                  // omitted....
22              }
23          }
24      }
25 }

SPUtilityInternal.Send403 内部则是调用了 SendResponse(response, 0x193, "403 FORBIDDEN");

01 internal static void SendResponse(HttpResponse response, int code, string strBody)
02 {
03      HttpContext current = HttpContext.Current;
04      object obj2 = current.Items[ "ResponseEnded" ];
05      if ((obj2 == null ) || !(( bool ) obj2))
06      {
07          current.Items[ "ResponseEnded" ] = true ;
08          response.StatusCode = code;
09          response.Clear();
10          if (strBody != null )
11          {
12              response.Write(strBody);
13          }
14          response.End();
15      }
16 }

解决这个问题

那么,我们是不是也可以自己写一个 HttpModule 订阅 HttpApplication.Error 事件,捕获 UnauthorizedAccessException 异常,并按我们期望的格式输出呢?当然可以,很关键的一步是要在 SPRequestModule 之前注册我们自定义的 HttpModule。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Diagnostics;

namespace LeoWorks.SharePoint.ErrorHandlerModule
{
    class ErrorHandlerModule : IHttpModule
    {
        #region IHttpModule Members

        public void Dispose()
        {
        }

        public void Init(HttpApplication app)
        {
            app.Error += new EventHandler(app_Error);          
        }

        void app_Error(object sender, EventArgs e)
        {
            //Debug.Assert(false);
            HttpApplication app = (HttpApplication)sender;
            HttpContext ctx = app.Context;
            if (ctx != null)
            {
                Exception err = ctx.Error;
                if (err != null)
                {
                    // it's helpful to handle all the types of exception especially when the application
                    // is throwing a chain of exceptions, because by default SharePoint will only show you
                    // the root exepction but you may want to get the full stack trace.
                    if (Convert.ToBoolean(ctx.Items["LW_HandleAllErrors"]))
                    {
                        HandleError(ctx, err); // output the full stack trace
                    }
                    else
                    {
                        Exception innerErr = err;
                        while (innerErr.InnerException != null)
                        {
                            innerErr = innerErr.InnerException;
                        }
                        if (innerErr is UnauthorizedAccessException)
                        {
                            HandleError(ctx, err);  // output the full stack trace
                        }
                    }
                }
            }
        }

        void HandleError(HttpContext ctx, Exception err)
        {
            ctx.Response.Clear();
            ctx.Response.Write(String.Format("

1 <div style= "background: rgb(255, 255, 204) none repeat scroll 0% 0%; -moz-background-clip: border; -moz-background-origin: padding; -moz-background-inline-policy: continuous;" ><code></code><pre>{0}</pre><code></code></div>", err.ToString()));
2              ctx.ClearError();
3              ctx.Response.End();
4          }
5  
6          #endregion
7      }
8 }

将编译好的的 LeoWorks.SharePoint.ErrorHandlerModule.dll 放入sharepoint站点所在的目录的 bin 文件夹,并在 web.config 配置此 module:

 

1 < httpmodules >
2        < clear >
3        < add type = "LeoWorks.SharePoint.ErrorHandlerModule.ErrorHandlerModule, LeoWorks.SharePoint.ErrorHandlerModule" name = "LWSPErrorHandler" >
4        < add type = "Microsoft.SharePoint.ApplicationRuntime.SPRequestModule, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" name = "SPRequest" >
5        <!-- ... -->
6      </ add ></ add ></ clear ></ httpmodules >

 

update 2010.4.13

今天发现 SharePoint 内部有些权限验证也会直接调用 SPUtilityInternal.Send403 方法,从而引发 403 Forbidden。因此,碰到这个错误,如果不是 UnauthorizedAccessException 异常,那么可以通过 Reflector 去查看页面的后台源码,分析内部实现,就能找到问题所在了。比如,在非缺省级网站集访问“自助式网站创建”页面 /_layouts/scsignup.aspx 时,就会得到 403 Forbidden。

测试我们的自定义的 Module

在 12/templates/layouts 目录中建立一个自己的 .aspx 文件,这里我命名为 LWK_Throws403.aspx,并放入了专享文件夹 leoworks.net。粘贴如下代码,在浏览器中打开该页面,并点击 Throws 按钮,此时我们就可以看到浏览器输出了调用堆栈。

expose-sp-403-02

01 <%@ Page Language="C#" AutoEventWireup="true" %>
02  
03  
04  
05 < script type = "text/C#" runat = "server" >
06  
07      protected void btnThrow_Click(object sender, EventArgs e)
08      {
09          // An UnauthorizedAccessException will cause the SharePoint engine to response
10          // a simple HTTP 403 error,
11          // for example,
12          // try to write to a file that current user has no permission to access
13          // System.IO.File.AppendAllText(@"C:/nobodycanwrite/test.txt", "I want to...");
14          // or explicitly throws an UnauhorizedAccessException
15          Context.Items["LW_HandleAllErrors"] = rblHandleType.SelectedIndex == 1;
16          if (rblExType.SelectedIndex == 3)
17          {
18              ThrowExceptionChain(false);
19          }
20          else if (rblExType.SelectedIndex == 2)
21          {
22              ThrowExceptionChain(true);
23          }
24          else if (rblExType.SelectedIndex == 1)
25          {
26              throw new UnauthorizedAccessException("mo xu you");
27          }
28          else
29          {
30              throw new ApplicationException("generic error!");
31          }
32      }
33  
34      void ThrowExceptionChain(bool throws403)
35      {
36          try
37          {
38              if (!throws403)
39              {
40                  throw new ApplicationException("generic error");
41              }
42              else
43              {
44                  throw new UnauthorizedAccessException("mo xu you");
45              }
46          }
47          catch (Exception ex)
48          {
49              throw new Exception("yuan wang!", ex);
50          }
51      }
52  
53 </ script >
54  
55  
56  
57       
58  
59  
60       
61      < div >
62          Use LWSPErrorHandlerModule to handle
63          < asp:radiobuttonlist id = "rblHandleType" runat = "server" repeatdirection = "Horizontal" repeatlayout = "Flow" >
64              < asp:listitem selected = "True" >Only Unauhorized</ asp:listitem >
65              < asp:listitem >All Exception</ asp:listitem >
66          </ asp:radiobuttonlist >
67          < br >
68          Throws
69          < asp:radiobuttonlist id = "rblExType" runat = "server" repeatdirection = "Horizontal" repeatlayout = "Flow" >
70              < asp:listitem >Generic Exception</ asp:listitem >
71              < asp:listitem selected = "True" >Unauthorized Access Exception</ asp:listitem >
72              < asp:listitem >Nested Unauthorized Exceptions</ asp:listitem >
73              < asp:listitem >Nested Generic Exceptions</ asp:listitem >
74          </ asp:radiobuttonlist >
75          < br >
76          < asp:button id = "Button1" onclick = "btnThrow_Click" runat = "server" text = "Throw" >
77      </ asp:button ></ div >

关于 SharePoint 2010

SharePoint 2010 依然存在此问题,上面的方法依然适用于 SharePoint 2010。只是需要注意 SharePoint 2010 默认将所有的 httpModule 配置在 IIS7 专有的 system.webServer/modules 节中,因此我们的 LeoWorks.SharePoint.ErrorHandlerModule 也要在此配置,目的是保证其配置在 SharePoint 的 SPRequestModule 之前。(当然如果你喜欢,可以将所有 module 移回 system.web/httpModules。)。 SharePoint 2010 配置如下:

1 < system.webserver >   
2      < modules runallmanagedmodulesforallrequests = "true" >
3        <!-- ... -->
4        < add type = "LeoWorks.SharePoint.ErrorHandlerModule.ErrorHandlerModule, LeoWorks.SharePoint.ErrorHandlerModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=24fb2cde482f6824" name = "LWSPErrorHandler" >
5        < add type = "Microsoft.SharePoint.ApplicationRuntime.SPRequestModule, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" name = "SPRequestModule" precondition = "integratedMode" >
6        <!-- ... -->
7      </ add >
8      <!-- ... -->
9    </ add ></ modules ></ system.webserver >


其他说明

1. 仔细分析 app_Error 方法,会发现我们通过 if (Convert.ToBoolean(ctx.Items["LW_HandleAllErrors"])) 判断是处理所有的异常还是仅仅处理 Unauthorized 异常。这个标识是我们再 LWK_Throws403.aspx 中调用 throw 之前写入的,这里仅仅是为了演示方便,你可能更喜欢写在配置文件中,比如 web.config/appSettings 中。

2. 此外,在调用 HandleError 方法时,我们总是传入 Context.Error ,而不是其内部根异常,这主要是为了输出完整的异常链信息。而 SharePoint 内部总是仅仅输出根异常。

3. 对于同时调试多个 SharePoint web app,你可能更喜欢将 LeoWorks.SharePoint.ErrorHandlerModule.dll 部署在 GAC 中而不是每个 web app 的 bin 中。此时 web.config 中应该用完整的程序集信息:

 

4. 如果你的堆栈信息无法看到出错的代码行数,除了你要确保你同时部署了 debug 的 dll 和对应版本的 pdb 外,要将 web.config 的 trust level 配置为 Full,sharepoint 默认是 WSS_Minimal,假如是 .aspx 页面错误,还要将 system.web/compilation 的 debug 配置为 true 。

5. 这个 Module 仅应该在开发环境中使用,生产环境应该移除掉,避免暴露过多的信息给用户。

源码下载

LeoWorks.SharePoint.ErrorHandlerModule.zip (18.37 kb)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值