存在的问题
在 SharePoint 2007 上你可能会碰到莫名其妙的 403 Forbidden 错误,SharePoint 丢出这个错误时简单得不能再简单,空白一片就 403 Forbidden 两个词,如下图
你 还可能看到是“网站拒绝显示此网页(The website declined to show this webpage) ” 的错误信息,如下图。你可以通过 IE 菜单栏 > 工具 > Internet 选项 > 高级 > 浏览 > 去掉“显示友好 HTTP 错误信息” 来看到内部错误。
可 惜的是,这个简单一点都不美,而且还很可恨。因为我们不知道到底背后发生了什么错误,如果此时能在系统日志或 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:
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" > |
7 | </ add ></ add ></ add ></ clear ></ httpmodules > |
通过 Reflector 查看 Microsoft.SharePoint.ApplicationRuntime.SPRequestModule 类源码,可以看到作为 HttpApplication.Error 的处理程序的 ErrorAppHandler 方法中确实对 UnauthorizedAccessException 异常进行了特殊处理:
01 | private void ErrorAppHandler( object oSender, EventArgs ea) |
03 | HttpApplication app = oSender as HttpApplication; |
06 | HttpContext context = app.Context; |
09 | Exception error = context.Error; |
12 | context.Items[ "HttpHandlerException" ] = "1" ; |
13 | while (error.InnerException != null ) |
15 | error = error.InnerException; |
17 | if (error is UnauthorizedAccessException) |
19 | SPUtilityInternal.Send403(context.Response); |
SPUtilityInternal.Send403 内部则是调用了 SendResponse(response, 0x193, "403 FORBIDDEN");
01 | internal static void SendResponse(HttpResponse response, int code, string strBody) |
03 | HttpContext current = HttpContext.Current; |
04 | object obj2 = current.Items[ "ResponseEnded" ]; |
05 | if ((obj2 == null ) || !(( bool ) obj2)) |
07 | current.Items[ "ResponseEnded" ] = true ; |
08 | response.StatusCode = code; |
12 | response.Write(strBody); |
解决这个问题
那么,我们是不是也可以自己写一个 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())); |
将编译好的的 LeoWorks.SharePoint.ErrorHandlerModule.dll 放入sharepoint站点所在的目录的 bin 文件夹,并在 web.config 配置此 module:
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" > |
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 按钮,此时我们就可以看到浏览器输出了调用堆栈。
01 | <%@ Page Language="C#" AutoEventWireup="true" %> |
05 | < script type = "text/C#" runat = "server" > |
07 | protected void btnThrow_Click(object sender, EventArgs e) |
09 | // An UnauthorizedAccessException will cause the SharePoint engine to response |
10 | // a simple HTTP 403 error, |
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) |
18 | ThrowExceptionChain(false); |
20 | else if (rblExType.SelectedIndex == 2) |
22 | ThrowExceptionChain(true); |
24 | else if (rblExType.SelectedIndex == 1) |
26 | throw new UnauthorizedAccessException("mo xu you"); |
30 | throw new ApplicationException("generic error!"); |
34 | void ThrowExceptionChain(bool throws403) |
40 | throw new ApplicationException("generic error"); |
44 | throw new UnauthorizedAccessException("mo xu you"); |
49 | throw new Exception("yuan wang!", ex); |
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 > |
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 > |
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 配置如下:
2 | < modules runallmanagedmodulesforallrequests = "true" > |
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" > |
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)