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

来源:互联网 发布:python内存管理机制 编辑:程序博客网 时间:2024/06/10 00:12

存在的问题

在 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:

view source
print?
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 异常进行了特殊处理:

view source
print?
01private 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");

view source
print?
01internal 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("

view source
print?
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:

 

view source
print?
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 方法,从而引发 403Forbidden。因此,碰到这个错误,如果不是 UnauthorizedAccessException 异常,那么可以通过Reflector 去查看页面的后台源码,分析内部实现,就能找到问题所在了。比如,在非缺省级网站集访问“自助式网站创建”页面/_layouts/scsignup.aspx 时,就会得到 403 Forbidden。

测试我们的自定义的 Module

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

expose-sp-403-02

view source
print?
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。只是需要注意 SharePoint2010 默认将所有的 httpModule 配置在 IIS7 专有的 system.webServer/modules 节中,因此我们的LeoWorks.SharePoint.ErrorHandlerModule 也要在此配置,目的是保证其配置在 SharePoint 的SPRequestModule 之前。(当然如果你喜欢,可以将所有 module 移回 system.web/httpModules。)。SharePoint 2010 配置如下:

view source
print?
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)

原创粉丝点击