一、CommunityServer的身份鉴别
任何交互系统,都要考虑身份鉴别方式,CS作为开源的社区系统项目,必然要涉及到同其他现有或者扩展系统的兼容性,那么就必须研究CS的身份验证体系细节,也总结出如何扩展此系统。
要处理身份验证,入口应该是系统的登录部分。CS的web工程里有一个login.aspx文件,是处理登录的。该页面只有一个CS:Login控件,这是个继承自
SecureTemplatedWebControl 的控件类,大致我们可以看到登录细节在登录按钮的click事件处理:
public void LoginButton_Click (Object sender, EventArgs e) {
User userToLogin = new User();
// Save in cookie auto login user's preference
// only if it wasn't previously set or the cookie value differs from
// login's autologin checkbox status.
//
AutoLoginCookie alCookie = new AutoLoginCookie(); //设定如果控件选择了允许自动登录的检查框那么就进行处理确保存在一个记录该设置的Cookie
if (!alCookie.HasValue ||
(alCookie.HasValue && (alCookie.GetValue() != autoLogin.Checked)))
{
alCookie.Write( autoLogin.Checked );
}
if (!Page.IsValid)
return;
userToLogin.Username = username.Text;
userToLogin.Password = password.Text;
LoginUserStatus loginStatus = Users.ValidUser(userToLogin); //验证用户的登陆状态 ProcessLoginRequest(userToLogin, loginStatus, autoLogin.Checked);//检查登录效果,根据登录状态来进行后续处理
}
Users 看来是一个关于User对象操作的逻辑类,通过静态方法提供对于User类型的用户API。
public
static LoginUserStatus ValidUser(User user)
{
CSContext context = CSContext.Current; //取得当前环境对象,单例模式?
if(user.Password ==null)
{
user.Password ="";
}
if(!MemberRoleProfileProvider.Instance().Members.ValidateUser(user.Username,user.Password)) //通过provider方式获得关于会员资格验证的组件并进行实例化,调用其中的Members.ValidateUser函数进行用户名、口令的值对判断,结果当前username/password验证失败
{
EventLogEntry entry = new EventLogEntry();
entry.Category = "Security";
entry.EventID = 600;
entry.SettingsID = context.SettingsID;
entry.EventType = EventType.Warning;
entry.Message = "Failed login for " + user.Username;
EventLogs.Write(entry); //书写事件
return LoginUserStatus.InvalidCredentials; //返回登录错误的代码
}
//如果运行到这里,表示用户名、密码验证通过!
User userLookup = Users.FindUserByUsername( user.Username );
if (userLookup == null) //没找到用户?
return LoginUserStatus.InvalidCredentials; //返回告诉当前失败原因
if(!userLookup.Member.IsApproved) //membership没有审核通过?
return LoginUserStatus.AccountPending; //返回账号阻塞状态
//下面来检查帐号的登录状态,因为即使通过身份验证,也许帐号被阻赛,需要在检查这类问题是否存在,系统应当不允许重复的username
if (!context.SiteSettings.EnableBannedUsersToLogin &&
userLookup.IsBanned &&
DateTime.Now <= userLookup.BannedUntil)
{//3种情况:是否允许用户锁定仍可登陆 用户是否锁定 锁定时间是否已经过去了
return LoginUserStatus.AccountBanned; //账号锁定?
}
// LN 5/21/04: Update user to DB if ban expired
else if (userLookup.IsBanned && DateTime.Now > userLookup.BannedUntil)
{ //锁定期更新
userLookup.AccountStatus = UserAccountStatus.Approved;
userLookup.BannedUntil = DateTime.Now;
Users.UpdateUser(userLookup);
}
if (userLookup.AccountStatus == UserAccountStatus.ApprovalPending)
{ // 未被审核通过的帐号 或者等待审核的状态的帐号
return LoginUserStatus.AccountPending;
}
if (userLookup.AccountStatus == UserAccountStatus.Disapproved)
{ // 冻结的帐号
return LoginUserStatus.AccountDisapproved;
}
//执行到此,该帐号正常
context.User = userLookup; //设定当前用户到context中
if(context.IsWebRequest) RemoveAnonymousTracking(context.Context,context.SiteSettings.AnonymousCookieName);//去掉对匿名状态的跟踪 当前是明确的登录用户
//allow others to take action here
CSEvents.UserValidated(userLookup); //纪录登陆状态 成功
//附加的diggsvc身份插入代码
//此处可以进行正常登录的增补行为
return LoginUserStatus.Success;
}
MemberRoleProfileProvider 也是一个重要的provider,登记在 communityServer.config中,
<add
name = "MemberRolProfileProvider"¾
type = "CommunityServer.MemberRole.CSMemberRoleProfileProvider, CommunityServer.MemberRole"
/>
那么我们查找具体的CommunityServer.MemberRole.CSMemberRoleProfileProvider在 ,
Telligent Systems 的非开源部分,不过通过
Reflector 工具查看,似乎只是封装了微软的membership组件而已,并且最终通过MS的membership类的接口提供验证判断(返回boolean值)。
如果返回通过帐号后,则进入
ProcessLoginRequest 函数处理登录成功后的工作。
protected
void ProcessLoginRequest(User userToLogin, LoginUserStatus loginStatus, bool autoLoginUser)
{
bool enableBannedUsersToLogin = csContext.SiteSettings.EnableBannedUsersToLogin;
string redirectUrl = null;
//如果当前用户帐号OK 或者虽然是被禁止的帐号,但是当前设置允许被禁止帐号登录
if (loginStatus == LoginUserStatus.Success || (enableBannedUsersToLogin && loginStatus == LoginUserStatus.AccountBanned))
{
//如果站点设置不允许登录 而且当前帐号又不是管理员,那么当然要抛出例外错误
if (!csContext.SiteSettings.AllowLogin && !userToLogin.IsAdministrator)
{
throw new CSException(CSExceptionType.UserLoginDisabled);
}
//正常登录状态,那么就要设定登录cookie
HttpCookie formsAuthCookie;
//获得身份验证cookie
formsAuthCookie = FormsAuthentication.GetAuthCookie(userToLogin.Username, autoLoginUser);
// FormsAuthentication是系统类。在用户通过身份验证后,Forms 身份验证即会在 Cookie 或 URL 中维护一个身份验证票证,这样已通过身份验证的用户就无需在每次请求时都提供凭据了。
formsAuthCookie = AlterAuthCookie(formsAuthCookie);// AlterAuthCookie未实现
UserCookie userCookie = csContext.User.GetUserCookie();//获得一个UserCookie实例
userCookie.WriteCookie(formsAuthCookie, 30, autoLoginUser); //然后写入刚才的票据Cookie
//处理推荐注册的善后工作
if (invitation != null)
UserInvitations.Accept(invitation, csContext.User);
// 如果含有ReturnUrl那么设定注册后的返回Url
if (!Globals.IsNullorEmpty(csContext.ReturnUrl))
redirectUrl = csContext.ReturnUrl;
// 确定最后转入Url
if ((redirectUrl == null) && (ReferralLink != null) && (ReferralLink.Trim() != string.Empty))
redirectUrl = ReferralLink;
if (Globals.IsNullorEmpty(redirectUrl)
|| (redirectUrl.IndexOf("MessageID") != -1)
|| (redirectUrl.IndexOf(Globals.GetSiteUrls().Logout) != -1)
|| (redirectUrl.IndexOf("ChangePassword") != -1)
|| (redirectUrl.IndexOf("EmailForgottenPassword") != -1))
redirectUrl = Globals.GetSiteUrls().Home;
if (invitation != null)
redirectUrl = Globals.GetSiteUrls().Message(CSExceptionType.UserInvitationAccepted);
LeaveSecureConnection(redirectUrl);//安全转入到最后确定的Url
}
else if (loginStatus == LoginUserStatus.InvalidCredentials)
{ // 如果身份验证失败 LeaveSecureConnection(Globals.GetSiteUrls().Message(CSExceptionType.UserInvalidCredentials)); }
else if (loginStatus == LoginUserStatus.AccountPending)
{ // 如果帐号未被审核 LeaveSecureConnection(Globals.GetSiteUrls().Message(CSExceptionType.UserAccountPending));
}
else if (loginStatus == LoginUserStatus.AccountDisapproved)
{ // 帐号被枪毙 LeaveSecureConnection(Globals.GetSiteUrls().Message(CSExceptionType.UserAccountDisapproved));
}
else if (loginStatus == LoginUserStatus.UnknownError)
{//未知错误
throw new CSException(CSExceptionType.UserUnknownLoginError);
}
else if (!enableBannedUsersToLogin && loginStatus == LoginUserStatus.AccountBanned)
{//帐号阻塞 LeaveSecureConnection(Globals.GetSiteUrls().Message(CSExceptionType.UserAccountBanned));
}
}
以上是登录时候的验证,可以看到验证通过后会放置一个安全存根到Cookie,随后的请求应当会携带这些Cookie,并经过获得身份鉴别。CS的身份鉴别还是自己进行了处理,通过web.config中的设定,挂接了相应的HttpModual:
<httpModules>
<addname="CommunityServer"type="CommunityServer.CSHttpModule, CommunityServer.Components"/>
<addname="CSProfile"type="Microsoft.ScalableHosting.Profile.ProfileModule, MemberRole, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b7c773fb104e7562"/>
<addname="CSRoleManager"type="Microsoft.ScalableHosting.Security.RoleManagerModule, MemberRole, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b7c773fb104e7562"/>
</httpModules>
这几个
httpModule都有同当前身份验证有关系,我们逐个分析。
CommunityServer.CSHttpModule 在Init方法中挂接了多个事件:
public void Init(HttpApplication application)
{
// Wire-up application events
//
application.BeginRequest += new EventHandler(this.Application_BeginRequest);
application.BeginRequest +=new EventHandler(cs_beginrequest);
application.AuthenticateRequest += new EventHandler(Application_AuthenticateRequest);
application.Error += new EventHandler(this.Application_OnError);
application.AuthorizeRequest += new EventHandler(this.Application_AuthorizeRequest);
application.EndRequest += new EventHandler(this.Application_EndRequest);
}
与身份相关的全局事件是:
application.AuthenticateRequest
application.AuthorizeRequest
application.EndRequest
来看看其中代码:
事件发出信号表示配置的身份验证机制已对当前请求进行了身份验证。预订 AuthenticateRequest 事件可确保在处理附加的模块或事件处理程序之前对请求进行身份验证。不过,似乎对于开源版本以下涉及到的各个模块都没有提供,我想那应该是收费版本才提供的DLL吧。希望有付费用户能够提供相关补充资料。
private
void Application_AuthenticateRequest(Object source, EventArgs e)
{//鉴别用户身份的事件 Authenticate 利用外挂模块的方式,通过外挂接口来进行身份验证
HttpContext context = HttpContext.Current;
Provider p = null;
ExtensionModule module = null;
// If the installer is making the request terminate early
if (CSConfiguration.GetConfig().AppLocation.CurrentApplicationType == ApplicationType.Installer) {//如果当前事件是Installer application事件,那么退出
return;
}
// Only continue if we have a valid context
//还要去确保context存在
if (context == null)
return;
authenticationType = String.Empty; //首先初始化身份验证类型String.Empty
if (context.User == null)
{//上段的注释告诉我们 如果User为空,并不一定意味着我们没有进行身份验证,可能使通过定制的模块验证方式处理了验证,此时需要看看 customidentity 类别的验证是否已经实施
p = (Provider) CSConfiguration.GetConfig().Extensions["CustomAuthentication"];
module = ExtensionModule.Instance(p);
if(module != null) //如果实例化验证provider模块成功
authenticationType = "customidentity";
else
return;
}
else
{
authenticationType = context.User.Identity.GetType().Name.ToLower();
}
//执行这段try catch之前已经确定了 authenticationType
try
{//来进行相应的 ProcessRequest
switch(authenticationType)
{
case "passportidentity":
p = (Provider) CSConfiguration.GetConfig().Extensions["PassportAuthentication"];
module = ExtensionModule.Instance(p);
if(module != null)
module.ProcessRequest();
else
goto default;
break;
// Windows
case "windowsidentity":
p = (Provider) CSConfiguration.GetConfig().Extensions["WindowsAuthentication"];
module = ExtensionModule.Instance(p);
if(module != null)
module.ProcessRequest();
else
goto default;
break;
case "formsidentity": //目前大多数是这种验证,此时调用 CommunityServer.config中 extensionModules 部分的模块验证
p = (Provider) CSConfiguration.GetConfig().Extensions["FormsAuthentication"];
module = ExtensionModule.Instance(p);
if(module != null)
module.ProcessRequest(); //此处进行了身份的验证处理,应该包括了cookie的设定和加密揭秘
else
goto default;
break;
// Custom
case "customidentity":
p = (Provider) CSConfiguration.GetConfig().Extensions["CustomAuthentication"];
module = ExtensionModule.Instance(p);
if(module != null)
module.ProcessRequest();
else
goto default;
break;
default:
CSContext.Current.UserName = context.User.Identity.Name;
break;
}
}
catch( Exception ex )
{
CSException forumEx = new CSException( CSExceptionType.UnknownError, "Error in AuthenticateRequest", ex );
forumEx.Log();
throw forumEx;
}
}
以上都是通过配置,然后利用provider模式获得具体的功能组件。不过,当前这块都是属于
Telligent Systems的定制DLL提供,并且似乎没有开源,要分析只有求助于
Reflector工具了。
我得到的CS版本中,存在一个关于验证的节:
该事件表示 ASP.NET 已对当前请求进行了授权。预订 AuthorizeRequest 事件可确保在处理附加的模块或事件处理程序之前对请求进行身份验证和授权。
private
void Application_AuthorizeRequest (Object source, EventArgs e) {
if (CSConfiguration.GetConfig().AppLocation.CurrentApplicationType == ApplicationType.Installer)
{ //如果是installer则不进行验证
return;
}
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
CSContext csContext = CSContext.Current; //获得CSContext,当前执行上下文的封装对象
CSEvents.UserKnown(csContext.User);
ValidateApplicationStatus(csContext); //验证当前执行上下文 如果上下文无法建立,表名身份验证出问题了,会进入到不同的错误处理。暂不深入分析,只是知道是进行上下文验证即可。
if (context.Request.IsAuthenticated) {//如果通过了asp.net框架的验证
string username = context.User.Identity.Name; //
if (username != null) {
string[] roles = CommunityServer.Components.Roles.GetUserRoleNames(username); //设定当前用户的所属组别
if (roles != null && roles.Length > 0) {
csContext.RolesCacheKey = string.Join(",",roles);
}
}
}
}
很多时候,需要验证当前用户的分组,那么在通过验证,取得当前用户的身份情况下,需要通过CommunityServer.Components.Roles获得当前用户的分组。
结束验证的时候,查看当前执行上下文的状态:
private
void Application_EndRequest (Object source, EventArgs e)
{
HttpContext context = HttpContext.Current;
// HTTP错误401.1-未经授权:访问由于凭据无效被拒绝
if (authenticationType == "customidentity" && context.Response.StatusCode == 401 && context.Request.QueryString["ReturnUrl"] == null)
{ //计算得到ReturnUrl
string redirectUrl = String.Empty;
redirectUrl = SiteUrls.FormatUrlWithParameters(SiteUrls.Instance().UrlData.Paths["login_clean"],
"ReturnUrl=" + Globals.UrlEncode(context.Request.Url.ToString()));
context.Response.Redirect(redirectUrl); //返回到验证失败的前一个页面
}
}
如果当前访问无效且是定制验证模块(意味着当前验证失败错误无法知道什么具体原因),那么就要进行Response.Redirect。
如果要修改身份验的逻辑(比如要修改身份验证等逻辑),通常我们要在MemberRoleProfileProvider.Instance().Members.ValidateUser中重写原来的验证方法或者在此处加入增补验证的逻辑。譬如,很多原来的论坛升级,需要将原来的用户名和密码进行迁移,此时就需要针对原来的密码加密验证方法进行重写,检验密码是否通过。
至于通过身份后,由于很多信息在
Cookie
并经过加密,由
Telligent Systems
进行处理,所以无法阐述细节,但是其实从体系上,大家可以置换相应模块
(provider
模式
)
,按照需求定义(抽象类派生或者接口实现)来实现相关功能。其中会涉及到
modual
编程,在合适的事件上进行身份还原和获取工作。