今日偶然阅读了Understanding Single Sign-On in ASP.NET 2.0 这篇关于单点登录的文章,仔细阅读,发现还是有些不尽完整之处。
文中对于单点登录的介绍,忽略了一个重要问题——就是站点的域的问题。在深入讨论这个问题之前,先简单介绍一下网站域的概念,举个例子,假如我们有三个站点:
A: blog.joycode.com; B: beta.joycode.com; C: www.openlab.net.cn 。那么A站点的域是"blog.joycode.com",它的主域是"joycode.com";B站点的域是"beta.joycode.com",它的主域是"joycode.com";C站点的域是"www.openlab.net.cn" ,它的主域是"openlab.net.cn"。对于A和B来说,他们是不同的网站域,但是主域是相同的,都是"joycode.com",对于A和C来说,不管是域还是主域,都不相同。一般我们所说的单点登录,都是指A,B,C这三类站点可以在一点登录,实现所有的站点都不需要再次登录,甚至于不仅限于网站之间,也可能是从其他客户端到网站。一般比较大的系统都需要这样的单点登录系统,例如著名的微软的Passport,还有飞信的SSO。
回过头来看这篇单点登录文章,按照这种方案,仅能实现同一域下各虚拟目录的单点登录,离真正的单点登录还差得远,当然,文章的思路,对于同一域下的虚拟目录,或者同一主域的不同站点,还有有意义的。
前面说到了文章中的方案还只能实现同一域下的不同虚拟目录的单点登录,还不能实现同一主域的单点登录,那么怎么样才能在这个方案基础上实现同一主域的不同站点的单点登录呢?
文章的核心部分在于让每个站点的MachineKey保持一致,采用Form验证,这样可以保证每台服务器Cookie加密解密的结果是一致的。Form验证将登录后的授权凭证加密后保存在Cookie中,由于同一域下面的虚拟目录,Cookie是可以共享的,因此可以同一域内直接实现单点登录,而对于不同域,Cookie是不能直接共享的,所以对于不同域而同一主域的情况,我们还需要将Cookie的domain设为主域。那么还以前文的A、B站点为例,要实现单点,我们只要在web.config中,配置authentication \forms节点下domain值为主域,如下:
<authentication mode="Forms"> <forms loginUrl="login.aspx" name=".ASPXAUTH" domain="joycode.com"/> </authentication>
即可实现同一主域不同子域站点之间共享登录了。
而对于不同主域的站点,实现方案相对就复杂多了。
如果你的Asp.Net程序执行时碰到这种错误:“验证视图状态 MAC 失败。如果此应用程序由网络场或群集承载,请确保 <machineKey> 配置指定了相同的 validationKey 和验证算法。不能在群集中使用 AutoGenerate。”那么说明你没有让你的应用程序使用统一的machineKey,那么machineKey的作用是什么呢?按照MSDN的标准说法:“对密钥进行配置,以便将其用于对 Forms 身份验证 Cookie 数据和视图状态数据进行加密和解密,并将其用于对进程外会话状态标识进行验证。”也就是说Asp.Net的很多加密,都是依赖于machineKey里面的值,例如Forms 身份验证 Cookie、ViewState的加密。默认情况下,Asp.Net的配置是自己动态生成,如果单台服务器当然没问题,但是如果多台服务器负载均衡,machineKey还采用动态生成的方式,每台服务器上的machinekey值不一致,就导致加密出来的结果也不一致,不能共享验证和ViewState,所以对于多台服务器负载均衡的情况,一定要在每台站点配置相同的machineKey。
machineKey生成的算法:
validationKey = CreateKey(20);
decryptionKey = CreateKey(24);
protected string CreateKey(int len)
{
byte[] bytes = new byte[len];
new RNGCryptoServiceProvider().GetBytes(bytes);
StringBuilder sb = new StringBuilder();
for(int i = 0; i < bytes.Length; i++)
{
sb.Append(string.Format("{0:X2}",bytes[i]));
}
return sb.ToString();
}
附参考的matchineKey配置:
<?xml version="1.0"?>
<configuration>
<system.web>
<machineKey validationKey="3FF1E929BC0534950B0920A7B59FA698BD02DFE8" decryptionKey="280450BB36319B474C996B506A95AEDF9B51211B1D2B7A77" decryption="3DES" validation="SHA1"/>
</system.web>
</configuration>
模版页中难免要引用CSS、脚本、图片等,这些文件的路径如果简单的使用相对路径,那么如果引用模版的目录一发生变化,这些路径就会出错;如果使用绝对路径,又不够灵活,如果应用程序目录发生变化,可能会导致要大量修改。asp.net支持一种相对于应用程序的路径,以波浪线开头的,形如"~/",使用它即可解决,例如:
<link rel="stylesheet" media="screen" type="text/css" href="<%=ResolveClientUrl("~/css/global.css") %>" />
当然如果你觉得每个路径都要写成动态的不爽,而又正好有页面基类的话,倒是可以换一种方式:
所有的路径直接书写为相对于应用程序目录的路径,形如:
<link rel="stylesheet" media="screen" type="text/css" href="~/css/global.css" />
当然默认HTML是不支持的这样的路径方式的,这时候就要借助PageBase了,代码如下(好像是从DNN的代码里面抠出来的):
| public abstract class PageBase : Page |
| { |
| protected override void Render(HtmlTextWriter writer) |
| { |
| StringWriter stringWriter = new StringWriter(); |
| HtmlTextWriter htmlWriter = new HtmlTextWriter(stringWriter); |
| base.Render(htmlWriter); |
| string html = stringWriter.ToString(); |
|
|
| #region 转换相对路径 |
| MatchCollection collection = Regex.Matches(html, "<(a|link|img|script|input|form).[^>]*(href|src|action)=(\\\"|'|)(.[^\\\"']*)(\\\"|'|)[^>]*>", RegexOptions.IgnoreCase); |
|
| foreach (Match match in collection) |
| { |
| if (match.Groups[match.Groups.Count - 2].Value.IndexOf("~") != -1) |
| { |
| string url = this.Page.ResolveUrl(match.Groups[match.Groups.Count - 2].Value); |
| html = html.Replace(match.Groups[match.Groups.Count - 2].Value, url); |
| } |
| } |
| #endregion |
| writer.Write(html); |
|
| } |
|
| } |
首先请看这两篇Blog
http://weblogs.asp.net/lkempe/archive/2006/08/27/C_2300_-implementation-of-newMediaObject-for-the-MetaWeblog-API.aspx
http://www.cnblogs.com/Luna/archive/2006/09/08/498799.html
基本原理已经讲明白了,因为例子中代码没有和DotText的相册整合,所以需要自己动手做一点改动,使LiveWriter的照片能直接上传到DotText的相册中,方便管理,也没有多少技术含量,直接贴上代码,省得大家做重复劳动。
MetaWeblogAPI.cs 中增加:
----------------------------------------------------
public struct MediaObjectUrl
{
public string url;
}
public struct MediaObject
{
public string name;
public string type;
public byte[] bits;
}
?
public interface IMetaWeblog
{
//新增接口
[XmlRpcMethod("metaWeblog.newMediaObject",
Description="Add a media object to a post using the "
+ "metaWeblog API. Returns media url as a string.")]
MediaObjectUrl newMediaObject(
string blogid,
string username,
string password,
MediaObject mediaObject);
}
----------------------------------------------------
MetaWeblog.cs中新增
----------------------------------------------------
///
/// Post a media object.
///
/// The blogid.
/// The username.
/// The password.
/// The media object.
/// MediaObjectUrl defining the url of the media
public MediaObjectUrl newMediaObject(string blogid, string username,
string password, MediaObject mediaObject)
{
BlogConfig config = Config.CurrentBlog();
if(ValidateUser(username,password,config.AllowServiceAccess))
{
mediaObject.name = mediaObject.name.Replace("/", "_");
int categoryId = GetMetaWeblogImageCategoryId();
Dottext.Framework.Components.Image image = new Dottext.Framework.Components.Image();
image.CategoryID = categoryId;
image.Title = mediaObject.name;
image.IsActive = true;
image.File = mediaObject.name;
image.LocalFilePath = string.Format("{0}\\{1}\\",config.ImageDirectory,categoryId);
int imageID = Images.InsertImage(image, mediaObject.bits);
string imagePath = config.ImagePath;
MediaObjectUrl mediaObjectUrl = new MediaObjectUrl();
mediaObjectUrl.url = String.Format("{0}{1}", string.Format("{0}{1}/", imagePath,categoryId),
image.OriginalFile);
return mediaObjectUrl;
}
throw new XmlRpcFaultException(0,"User does not exist");
}
private int GetMetaWeblogImageCategoryId()
{
string name = "Meta Weblog Images"
LinkCategoryCollection categories = Links.GetCategories(CategoryType.ImageCollection, false);
foreach(LinkCategory lc in categories)
{
if (lc.Title == name)
{
return lc.CategoryID;
}
}
LinkCategory category = new LinkCategory();
category.CategoryType = CategoryType.ImageCollection;
category.Title = name;
category.IsActive = true;
category.Description = "MetaWeblogAIP专用,请勿删除和修改此分类!"
return Links.CreateLinkCategory(category);
}
如果您想在页面中显示出来当前asp.net程序占用了多少内存,那么可以使用:
double memoryUsage = (((double)System.Diagnostics.Process.GetCurrentProcess().WorkingSet64) / 1024) / 1024;
来计算,单位是MB。
可能我们Membership的用户数据库是从其它系统导入的,例如动网论坛,那么用户系统用的加密方式和membership不一样的,也就是使用Membership就无法登录了,我曾试通通过继承Membership重写其中方法来实现,但是发现很多方法都无法override,这点Membership设计太失败了!
还好微软发布了Provider实现的源码《asp.net2.0自带的Provider源码下载》,这样我就可以通过修改Provider来实现了,SQLMembershipProvider中有一个CheckPassword方法,在里面,增加了对其他用户密码加密的支持,然后在web.config中,指定membership的provider为您修改后的Provider
倒不是最近写asp了,而是最近在写asp.net时想到的。以前在文章《Asp无组件上传带进度条(续)》中,提供了asp无组件上传带进度条的解决方案,但是当时都是内存操作,如果文件大很占内存,因为vbs的语法对IO操作的功能有限,无法对分块的内容进行拼合,所以只能在内存操作。
除了IO和内存可以将分块拼合,还有第三种方式可以——数据库,ado支持AppendChunk方法,Sql也支持UPDATETEXT。
大概思路是这样的:分块上传,对当前分块进行解析,解析后,将分块中解析出来的文件的内容分块都追加相应的数据库记录中,这样就可以保证内存占用非常小了,因为数据都转移到数据库中了,最后传完了,从数据库中取出来就可以了。
虽然我没写,但是这种方案是肯定可行的。不过因为我不再写asp代码了,所以找我要代码是没有的,如果有哪位达人写出来了,不妨共享一下,谢谢!
附:我以前写的asp无组件上传源码 http://webuc.net/myproject/upload/demo.htm
我一直觉得Asp.Net2.0的Membership不够用,例如:不能自定义密码类型,不能获取角色Id,不能方便的检测用户名或Email是否存在……,总之还是不能完全满足自己的要求,那么就自己扩展吧,但是总是不那么爽,重新实现一套Provider吧,太麻烦了。
今天偶然读到这个blog:《Source Code for the Built-in ASP.NET 2.0 Providers Now Available for Download》,发现可以下载其Provicer源码,这样可以在其基础上做我们自己的Provider了,省事多了!
下载:http://download.microsoft.com/download/a/b/3/ab3c284b-dc9a-473d-b7e3-33bacfcc8e98/ProviderToolkitSamples.msi
我以前一直知道有一个 http://www.asp.net 网站,不知道还有一个http://www.windowsforms.net/,专门面向.Net Framework Windows Forms的,有不少不错的.Net源码,例如有一个.Net2.0写的Windows Forms RSS Portal 挺不错的。
我不废话,只发1个SQL:
select threadid from
(
select threadid, ROW_NUMBER() OVER (order by stickydate) as Pos from cs_threads
) as T
where T.Pos > 100000 and T.Pos < 100030
CSDN因为访问量巨大,所以采用了文件缓存机制,即将帖子内容生成XML文件,再在客户端有XSLT解析,确实可以很大的减轻数据库压力。于是我也想了想,如果采用这种文件缓存的方式来增强论坛、Blog的性能,减轻数据库压力,要注意哪些问题呢?于是仔细想了想一些可能的问题和如何来解决:
在这之前我先对本文的术语作一个简单解释:第一篇帖子和它的所有回复都称之为一个主题(Thread),第一篇帖子和它后面的每篇回复都叫做帖子(Post)。另外一个前提是本文中,类似于CSDN的情况:即XML文件缓存是辅助的,在数据库中同时存有一份数据,访问时先访问数据库获取关键信息,然后在显示帖子内容时读取XML文件。
缓存的单位:CSDN的缓存是以一个Thread为单位,将Thread下所有的Posts都生成一个XML,这样一个好处就是生成的文件比较少,缺点就是文件更新较频繁(每有回帖就必须更新一下文件),不方便支持帖子修改/删除功能,客户端下载时需要整个XML下载完才能显示出HTML。我个人倾向于以Post为单位,每个Post生成一个XML,这样做的缺点是生成的文件较多,优点是调用更加灵活,不用频繁更新XML,在客户端解析时可以支持单个Post加载完就显示。
缓存的时机:生成缓存文件主要有两种时机——帖子创建时即生成XML、第一次访问帖子时生成XML。我个人倾向于第二种方案:有利于多台服务器负载均衡,如果缓存文件丢失,可以方便再次生成。CSDN好像是采用的在有新帖子创建时生成/修改XML,不确认。
支持帖子修改:CSDN是不支持帖子修改和删除的(版主应该可以吧),具体原因我不清楚,如果他们的缓存文件时在帖子创建时生成和更新,那么可能是不想频繁更新文件,如果是第一次访问时,那么需要有一个时间戳来记录帖子是不是修改了,如果修改了那么需要更新XML。基于前面两个问题我选择的方案,在第一次访问时生成缓存的XML文件,并且以Post为单位生成XML文件,那么当帖子修改后,我在下次访问时需要更新这个XML文件,而不是访问旧的XML文件。
那么怎么样才能在每次访问时知道帖子更新了呢?如果是帖子修改时就直接更新XML文件当然简单,但是现在不是这样,那么是不是每次先服务器端读取一遍XML,然后对比一下XML中的帖子最后更新时间和数据库中的帖子最后更新时间是不是一致的,如果不是就表示需要更新XML了,这样当然可行,但是效率太低了,不过这倒是给了我一点启发:为什么不直接以帖子最后更新时间(精确到秒)命名,这样在找缓存文件时,如果帖子更新了,而缓存文件没有更新,那么就会找不到缓存文件,再次生成就好了。这样有个问题是会导致过期的文件还存在,倒不见得是坏事情——相当于对每个帖子都有个历史存档了。
权限问题:如果XML路径是很有规律的,并且可以被直接访问到,那么就没有什么权限和隐私可言了,一种比较直接有效的方法,就是通过HttpHandler来隐藏缓存的XML文件的真实路径,并且判断用户是否有权限看到。如果你担心XML文件路径可能被猜出来,那么可以将缓存文件禁止直接外部访问,或者对上面提到的XML文件命名方案进行改进:对于Post增加一个属性——缓存文件名,第一次创建时生成一个随机数作为文件名,如果后来帖子有修改,同时更新这个属性。
文件保存路径:因为每个Post一个文件这种方案会让文件很多,如果一个目录下文件太多,检索效率会非常低的,那么应该对文件进行分区,分成四级目录:第一级将帖子文件按照所属板块(Forum)分类,例如按照板块的ID生成目录;第二级按照Thread的日期来分类,这个可以根据帖子量来决定日期划分,如果帖子量巨大甚至可以每天一个目录;第三级按照Thread来分类,例如按照Thread.ThreadId来生成目录,第四级按照Post来分类,因为每个Post每更新一次就会生成一个新的XML,参考命名方式:
string cacheFilePath = string.Format("Forum{0}\\{1}\\{2}\\Thread{3}\\Post{4}\\{5}.xml",?post.ForumId, post.ThreadDate.ToString("yyyy"), post.ThreadDate.ToString("MM"), post.ThreadId, post.PostId, post.CacheFileName);
换皮肤问题:XSLT本身就可以很方便的实现换肤功能——每种皮肤一个XSLT文件即可,不过这样的一个问题就是需要对XSLT比较熟悉。如果要在传统的论坛换皮肤基础上实现这个功能,可以考虑一个折中的方案:将每个缓存后的Post的XML作为XML数据岛嵌入在帖子显示的位置,本身IE对XML数据岛就支持非常好,即使是FireFox之类不支持XML数据岛的浏览器(貌似是不支持的),也可以结合XSLT来做,这个XSLT只要解析显示帖子内容这部分就好了,相对难度小很多。
实际上这也是我对文件缓存的一些不成熟想法。同时推荐一篇相关文章:Disk-based Output Caching Module
费了好一袋烟工夫把CommunityServer升级到了Asp.Net2.0平台,一点心得:
vs2005可以很方便的帮我们把vs2003开发的asp.net1.1版本项目升级到vs2005开发的asp.net2.0版本,从vs2005里面打开vs2003的解决方案或者项目文件,会有向导帮我们自己完成升级工作。一部分asp.net1.1的项目做完这个工作就足够了。
不过更多的时候不会这么顺利,还要注意一些问题:
- vs2003开发的asp.net1.1程序,不会检查aspx、aspcx等文件中的语法错误,而vs2005会检查项目中所有的aspx、aspcx等文件中的语法,所以如果有语法错误,会导致编译无法通过。
- vs2003中,如果用的是默认的代码绑定方式,那么在aspx文件(以aspx文件为例,ascx文件也有这个问题)中申明的服务器端控件,会在aspx文件对应的aspx.cs文件中,生成一个对应的申明,例如aspx中有一个TextBox,ID是MyTextBox,那么在aspx.cs中,会申明一个"protected TextBox MyTextBox;",而在vs2005中,这个申明是多余的,所以升级后要去除这些多余的申明。
- 如果有程序采用了asp.net1.1下的Membership——使用MemberRole.dll,要升级到asp.net2.0下的Membership,需要做如下工作:
- 删除所有项目中对"MemberRole.dll"的引用,添加"System.Configration"的引用
- 改变命名空间ScalableHosting.Profile -> System.Web.Profile;ScalableHosting.Security -> System.Web.Security; 同时添加using System.Configuration;
- 移除所有MemberRole.dll相关的Membership配置,参照以前的Membership配置,增加asp.net2.0支持的Membership配置,更新Membership的存储过程。
- CCS1.1 for asp.net2.0的下载:http://www.communityserver.cn/builds
昨天在博客园上看到维生素C.NET(范维肖)的blog:《如何为live.com编写并添加自己的Gadget 》,在Xbox360的诱惑下,忍不住想写一个Gadget去碰碰运气,想来想去,就写一个有中国特色的Gadget——农历日历,网上有现成的“经典万年历”js代码,还是费了好一袋烟工夫,参考SampleGadgets的Hello world,结合“经典万年历”的核心脚本(作者已经无法考证,在次对作者表示感谢),完成了我的第一个Gadget——Chinese Date Time,有兴趣的朋友可以加到自己的live.com中,兼容IE和FF(发现在FF中效率要高),至少可以让大家在live.com中可以方便的查一下什么时候过春节啦:)
在 http://microsoftgadgets.com 网站上的位置:Chinese Date Time,Source也可以直接下载到。


VS2005里面新建Web站点时可以从模板创建,默认有一个“Personal Web Site Starter Kit”的模板,是一个人网站程序,根据这个模板可以很容易扩展出自己的个人站点。
其实还有一个模板程序叫“Club Site Starter Kit”,VS2005RTM默认安装时没有带,不过可以从MSDN上下载到:
| |
|
| A starting point for creating a web site for your club or organization. Includes a news posting, calendaring, member directory, and photo album systems. |
| | See more about the Club Site Starter Kit: Try it Live! Download Starter Kit (version 50215.45.01, released 4/14/2005) View Whitepaper (coming soon) Generic Scripts for SQL Server 2000 and MSDE Event Calendar Control View Discussion Forum
|
News
- Create announcements and news articles
- Include a photo, or link to a photo album
Event calendaring
- Create events for your club
- View as a list or calendar
- Download events to Outlook or other calendaring application
Event locations
- Pages for each location for club events occur at
- Use for directions and facility information
Photo Albums
- Create albums and share the photos from your club activities
- Use the photos in news articles and calendar entries
Membership list
- View a contact list for the club members
Technologies and Design Approaches Demonstrated:
- Databound events calendar
- Customized web controls
- User controls
- Data binding
|
演示地址为:http://beta.asp.net/vwd/live/club/default.aspx。
下载地址为:http://asp.net/tools/redir.aspx?path=clubsite
下载Club.vsi并安装,这样在新建Web站点时就可以选择模板“Club Site Starter Kit”,使用这个模板会默认根据这个模板自动创建一个完整的Club站点程序,您可以在这个基础上进行扩展。
不过要注意一个问题就是这个模板的数据库是在Beta版本时创建的,在RTM下根本就不能正常使用。解决方案就是删除App_Data\clubsite.mdf这个数据库文件,然后重新创建,并下载执行SQL文件(如果要在这个数据库上执行SQL,好像要安装Sql2005):http://www.microsoft.com/downloads/details.aspx?FamilyId=0DD83A11-6980-4951-A192-DA6EACC6A19E&displaylang=en 需要注意的是这个SQL文件创建的数据库据不兼容中文,需要作一下改动,也就是执行SQL前要把SQL中的所有“ COLLATE SQL_Latin1_General_CP1_CI_AS NULL”全部删除。
如果您嫌自己重新创建麻烦,那么也可以在安装完Club.vsi后,下载http://www.CommunityServer.cn/downloads/ClubSiteCS.zip 并覆盖Visual Studio 2005\Templates\ProjectTemplates\Visual Web Developer\CSharp目录下的同名文件。这样以后创建时就不用担心了。
Personal Web Site Starter Kit 和 Club Site Starter Kit 是两个入门级的示例程序,通过学习和研究这两个模板的代码,可以让我们很快的了解学习和使用asp.net2.0。
肯定有这样子的需求:我们希望能用快捷键代替鼠标点击做一些事情,例如一个典型的应用就是论坛上常用的Ctrl + Enter 快捷发帖子。就以Ctrl+Enter快捷发帖子为例,实质上呢,就是通过js脚本,捕获系统的onkeyup事件,判断event.ctrlKey是否为true并且event.keyCode为13,如果满足这个条件,那么就调用按钮对象的click()方法,等同于用鼠标去点击按钮。写个简单的示例代码:
上面的脚本可以方便的给指定的按钮加上快捷键。如果现在我们希望应用到我们的服务器端控件当中去,例如Button、Linkbutton、Hyperlink等,因为控件的ID各不相同,而且所对应的快捷键也各不相同,那么我们就需要写一个控件来给他们添加快捷键了。
想想这个控件需要哪些属性?
TargetControlID:string:既然是给其他控件绑定的,那么所要绑定的目标控件ID是少不了了,根据这个控件ID,我们才能确定一个控件,才能知道它输出时的客户端ID,才能根据客户端ID来在脚本里面确定这个对象。
CtrlKey:bool:判断是否用到Ctrl组合键
ShiftKey:bool:判断是否用到Shift组合键
AltKey:bool:判断是否用到Alt组合键
KeyCode:int:和DHTML里面的event.keyCode对应的,例如Enter的keyCode是13。(注:其实这个不是很友好,因为用的时候还要找一下键盘各个按键和keyCode的对应关系,如果结合一个快捷键设置的控件就比较完美了)
Text:string:可能顺便需要一点文字说明什么的
相对来说,这是一个比较简单的用户自定义控件应用,创建一个名字为HotKey的类,继承自System.Web.UI.Controls。根据目标控件ID查找控件对象:this.cachedTargetControl = this.NamingContainer.FindControl(this.TargetControlID);
根据上面属性,我们就可以生成相应的客户端脚本了,然后在重写控件的OnPreRender事件中使用RegisterClientScriptBlock方法输出脚本。
代码相对比较简单:
最后,调用的时候就比较方便了:
和普通的自定义用户控件一样:
<asp:Button Runat="server" id="PostButton" CssClass="txt3"></asp:Button>
<cs:Hotkey runat="server" TargetControlID="PostButton" KeyCode="13" CtrlKey="True" Text="(Ctrl + Enter)"/>
完整的应用请参考CCS1.1(http://www.communityserver.cn/ )的代码。