專題八的上篇大致討論了memberrole中的membership實現(xiàn),對于運用membership進行足夠,但是對于想更深入了解membership實現(xiàn)機理的朋友那是遠遠不夠的,這個專題我們更深入一下了解membership。
其實memberrole是一個非常好的資源包,借住reflector這個優(yōu)秀的工具,你可以對其進行代碼分析。它無論是在組建的構架、代碼的設計、數(shù)據庫表的建立、存儲過程的使用等都是非常優(yōu)秀的,你是程序員也好構架師也罷,其中可以學習的真的很多很多,我在整個分析的過程中也深深受益。
由于memberrole中的membership只實現(xiàn)了對sql server的操provider類,即sqlmembershipprovider類。因此我們從sqlmembershipprovider開始分析。provider模型在上篇已經做過介紹,sqlmembershipprovider類繼承了membershipprovider,并實現(xiàn)其所有的抽象方法。在分析之前先看兩個類:membershipuser與membershipusercollection。
membershipuser,先看看代碼:(代碼中省略的具體實現(xiàn),只有方法與屬性名稱)
public class membershipuser
{
// methods
protected membershipuser();
public membershipuser(membershipprovider provider, string name, object provideruserkey, string email, string passwordquestion, string comment, bool isapproved, bool islockedout, datetime creationdate, datetime lastlogindate, datetime lastactivitydate, datetime lastpasswordchangeddate, datetime lastlockoutdate);
public virtual bool changepassword(string oldpassword, string newpassword);
public virtual bool changepasswordquestionandanswer(string password, string newpasswordquestion, string newpasswordanswer);
public virtual string getpassword();
public virtual string getpassword(string passwordanswer);
public virtual string resetpassword();
public virtual string resetpassword(string passwordanswer);
public override string tostring();
public virtual bool unlockuser();
internal virtual void update();
private void updateself();
// properties
public virtual string comment { get; set; }
public virtual datetime creationdate { get; }
public virtual string email { get; set; }
public virtual bool isapproved { get; set; }
public virtual bool islockedout { get; }
public bool isonline { get; }
public virtual datetime lastactivitydate { get; set; }
public virtual datetime lastlockoutdate { get; }
public virtual datetime lastlogindate { get; set; }
public virtual datetime lastpasswordchangeddate { get; }
public virtual string passwordquestion { get; }
public virtual membershipprovider provider { get; }
public virtual object provideruserkey { get; }
public virtual string username { get; }
// fields
private string _comment;
private datetime _creationdate;
private string _email;
private bool _isapproved;
private bool _islockedout;
private datetime _lastactivitydate;
private datetime _lastlockoutdate;
private datetime _lastlogindate;
private datetime _lastpasswordchangeddate;
private string _passwordquestion;
private membershipprovider _provider;
private object _provideruserkey;
private string _username;
}
這是一個實體類,表示一個由membership創(chuàng)建的user,該類中有這個user的一些基本狀態(tài),如該user的username、email等,還有一些方法,如changepassword()、resetpassword()等(如果你是初學者,還在為建立一個對象需要什么屬性,包含什么方法發(fā)愁,那這就是你應該好好學的,這也是oop最基本的要求)。
membershipusercollection,這是一個membershipuser類的容器,用來存放membershipuser列表,記得上次廣州.net俱樂部聚會時,我的演講中有朋友在提出cs是否使用自定義類來存儲用戶列表,其實在這里可以看到cs中使用的就是自定義的類而不是dataset(我想在asp.net 2.0正式發(fā)布后這也不會改變),這樣做主要是因為考慮到性能與靈活性。
好了,回到sqlmembershipprovider類上來,我們具體分析一個有代表性質的方法:
public override membershipuser createuser(string username, string password, string email, string passwordquestion, string passwordanswer, bool isapproved, object provideruserkey, out membershipcreatestatus status)
{
string text3;
membershipuser user1;
if (!secutility.validateparameter(ref password, true, true, false, 0x80))
{
status = membershipcreatestatus.invalidpassword;
return null;
}
string text1 = base.generatesalt();
string text2 = base.encodepassword(password, (int) this._passwordformat, text1);
if (text2.length > 0x80)
{
status = membershipcreatestatus.invalidpassword;
return null;
}
if (passwordanswer != null)
{
passwordanswer = passwordanswer.trim();
}
if ((passwordanswer != null) && (passwordanswer.length > 0))
{
if (passwordanswer.length > 0x80)
{
status = membershipcreatestatus.invalidanswer;
return null;
}
text3 = base.encodepassword(passwordanswer.tolower(cultureinfo.invariantculture), (int) this._passwordformat, text1);
}
else
{
text3 = passwordanswer;
}
if (!secutility.validateparameter(ref text3, this.requiresquestionandanswer, this.requiresquestionandanswer, false, 0x80))
{
status = membershipcreatestatus.invalidanswer;
return null;
}
if (!secutility.validateparameter(ref username, true, true, true, 0x100))
{
status = membershipcreatestatus.invalidusername;
return null;
}
if (!secutility.validateparameter(ref email, this.requiresuniqueemail, this.requiresuniqueemail, false, 0x100))
{
status = membershipcreatestatus.invalidemail;
return null;
}
if (!secutility.validateparameter(ref passwordquestion, this.requiresquestionandanswer, this.requiresquestionandanswer, false, 0x100))
{
status = membershipcreatestatus.invalidquestion;
return null;
}
if ((provideruserkey != null) && !(provideruserkey is guid))
{
status = membershipcreatestatus.invalidprovideruserkey;
return null;
}
if (password.length < this.minrequiredpasswordlength)
{
status = membershipcreatestatus.invalidpassword;
return null;
}
int num1 = 0;
for (int num2 = 0; num2 < password.length; num2++)
{
if (!char.isletterordigit(password, num2))
{
num1++;
}
}
if (num1 < this.minrequirednonalphanumericcharacters)
{
status = membershipcreatestatus.invalidpassword;
return null;
}
if ((this.passwordstrengthregularexpression.length > 0) && !regex.ismatch(password, this.passwordstrengthregularexpression))
{
status = membershipcreatestatus.invalidpassword;
return null;
}
validatepasswordeventargs args1 = new validatepasswordeventargs(username, password, true);
this.onvalidatingpassword(args1);
if (args1.cancel)
{
status = membershipcreatestatus.invalidpassword;
return null;
}
try
{
sqlconnectionholder holder1 = null;
try
{
holder1 = sqlconnectionhelper.getconnection(this._sqlconnectionstring, true);
this.checkschemaversion(holder1.connection);
sqlcommand command1 = new sqlcommand("dbo.aspnet_membership_createuser", holder1.connection);
command1.commandtimeout = this.commandtimeout;
command1.commandtype = commandtype.storedprocedure;
command1.parameters.add(this.createinputparam("@applicationname", sqldbtype.nvarchar, this.applicationname));
command1.parameters.add(this.createinputparam("@username", sqldbtype.nvarchar, username));
command1.parameters.add(this.createinputparam("@password", sqldbtype.nvarchar, text2));
command1.parameters.add(this.createinputparam("@passwordsalt", sqldbtype.nvarchar, text1));
command1.parameters.add(this.createinputparam("@email", sqldbtype.nvarchar, email));
command1.parameters.add(this.createinputparam("@passwordquestion", sqldbtype.nvarchar, passwordquestion));
command1.parameters.add(this.createinputparam("@passwordanswer", sqldbtype.nvarchar, text3));
command1.parameters.add(this.createinputparam("@isapproved", sqldbtype.bit, isapproved));
command1.parameters.add(this.createinputparam("@uniqueemail", sqldbtype.int, this.requiresuniqueemail ? 1 : 0));
command1.parameters.add(this.createinputparam("@passwordformat", sqldbtype.int, (int) this.passwordformat));
command1.parameters.add(this.gettimezoneadjustmentparam());
sqlparameter parameter1 = this.createinputparam("@userid", sqldbtype.uniqueidentifier, provideruserkey);
parameter1.direction = parameterdirection.inputoutput;
command1.parameters.add(parameter1);
parameter1 = new sqlparameter("@returnvalue", sqldbtype.int);
parameter1.direction = parameterdirection.returnvalue;
command1.parameters.add(parameter1);
object obj1 = command1.executescalar();
datetime time1 = this.roundtoseconds(datetime.now);
if ((obj1 != null) && (obj1 is datetime))
{
time1 = (datetime) obj1;
}
int num3 = (parameter1.value != null) ? ((int) parameter1.value) : -1;
if ((num3 < 0) || (num3 > 11))
{
num3 = 11;
}
status = (membershipcreatestatus) num3;
if (num3 != 0)
{
return null;
}
provideruserkey = new guid(command1.parameters["@userid"].value.tostring());
return new membershipuser(this, username, provideruserkey, email, passwordquestion, null, isapproved, false, time1, time1, time1, time1, new datetime(0x6da, 1, 1));
}
finally
{
if (holder1 != null)
{
holder1.close();
holder1 = null;
}
}
}
catch
{
throw;
}
return user1;
}
該方法實現(xiàn)建立一個用戶的過程,建立后返回一個被建立的membershipuser對象,如果建立失敗membershipuser對象為null(其實我早期做過一些項目的時候喜歡在建立對象成功后返回一個id)。可以看到在這個方法中有很多的if語句,它們是為了檢驗數(shù)據是否合法,這是必須的嗎?其實不是,但對于構建一個強壯的底層代碼這是必須的,不然一點點的錯誤都有可能導致系統(tǒng)的癱瘓。其實做項目與做開發(fā)有的時候不太一樣,企業(yè)的有些項目開發(fā)很多時候只要能實現(xiàn)功能就可以了,而且開發(fā)過程也集中在一些現(xiàn)有的代碼或者組建的基礎上,個人的錯誤不會影響全局的運行,pm也不做過多要求。但如果是做產品,這個情況可能會有所改變,很多時候要求很嚴格,至少我是這樣。在做完對輸入參數(shù)的驗證后,createuser建立與數(shù)據庫的連接,這里是調用sqlconnectionhelper類下的getconnection方法進行的,為了照顧初學者閱讀,我這里這一下為什么需要把對數(shù)據庫連接與操作寫在sqlconnectionhelper類下,而不是直接采用sqlconnection提供的方法,其實這是一個設計模式的問題,membership的實現(xiàn)需要很多的方法與數(shù)據庫進行交換數(shù)據庫,如果每次方法都調用一次sqlconnection的方法建立數(shù)據庫連接,一來會造成大量的代碼冗余,而且一旦數(shù)據庫連接語句一旦改變,你就要去修改很多個方法,如果你把這個過程都包裝在一個類下面,連接數(shù)據庫就有統(tǒng)一的入口,一來容易維護,二來不會有太多的代碼冗余,再者如果需要查找錯誤也非常容易。這里membership采用的是存儲過程,我們可以看到使用的是dbo.aspnet_membership_createuser存儲過程,好了,打開你的數(shù)據庫,找到這個存儲過程:
create procedure dbo.aspnet_membership_createuser
@applicationname nvarchar(256),
@username nvarchar(256),
@password nvarchar(128),
@passwordsalt nvarchar(128),
@email nvarchar(256),
@passwordquestion nvarchar(256),
@passwordanswer nvarchar(128),
@isapproved bit,
@timezoneadjustment int,
@createdate datetime = null,
@uniqueemail int = 0,
@passwordformat int = 0,
@userid uniqueidentifier output
as
begin
declare @applicationid uniqueidentifier
select @applicationid = null
declare @newuserid uniqueidentifier
select @newuserid = null
declare @islockedout bit
set @islockedout = 0
declare @lastlockoutdate datetime
set @lastlockoutdate = convert( datetime, '17540101', 112 )
declare @failedpasswordattemptcount int
set @failedpasswordattemptcount = 0
declare @failedpasswordattemptwindowstart datetime
set @failedpasswordattemptwindowstart = convert( datetime, '17540101', 112 )
declare @failedpasswordanswerattemptcount int
set @failedpasswordanswerattemptcount = 0
declare @failedpasswordanswerattemptwindowstart datetime
set @failedpasswordanswerattemptwindowstart = convert( datetime, '17540101', 112 )
declare @newusercreated bit
declare @returnvalue int
set @returnvalue = 0
declare @errorcode int
set @errorcode = 0
declare @transtarted bit
set @transtarted = 0
if( @@trancount = 0 )
begin
begin transaction
set @transtarted = 1
end
else
set @transtarted = 0
exec dbo.aspnet_applications_createapplication @applicationname, @applicationid output
if( @@error <> 0 )
begin
set @errorcode = -1
goto cleanup
end
if (@createdate is null)
exec dbo.aspnet_getutcdate @timezoneadjustment, @createdate output
else
select @createdate = dateadd(n, [email protected], @createdate) -- switch to utc time
select @newuserid = userid from dbo.aspnet_users where lower(@username) = loweredusername and @applicationid = applicationid
if ( @newuserid is null )
begin
set @newuserid = @userid
exec @returnvalue = dbo.aspnet_users_createuser @applicationid, @username, 0, @createdate, @newuserid output
set @newusercreated = 1
end
else
begin
set @newusercreated = 0
if( @newuserid <> @userid and @userid is not null )
begin
set @errorcode = 6
goto cleanup
end
end
if( @@error <> 0 )
begin
set @errorcode = -1
goto cleanup
end
if( @returnvalue = -1 )
begin
set @errorcode = 10
goto cleanup
end
if ( exists ( select userid
from dbo.aspnet_membership
where @newuserid = userid ) )
begin
set @errorcode = 6
goto cleanup
end
set @userid = @newuserid
if (@uniqueemail = 1)
begin
if (exists (select *
from dbo.aspnet_membership m with ( updlock, holdlock )
where applicationid = @applicationid and loweredemail = lower(@email)))
begin
set @errorcode = 7
goto cleanup
end
end
insert into dbo.aspnet_membership
( applicationid,
userid,
password,
passwordsalt,
email,
loweredemail,
passwordquestion,
passwordanswer,
passwordformat,
isapproved,
islockedout,
createdate,
lastlogindate,
lastpasswordchangeddate,
lastlockoutdate,
failedpasswordattemptcount,
failedpasswordattemptwindowstart,
failedpasswordanswerattemptcount,
failedpasswordanswerattemptwindowstart )
values ( @applicationid,
@userid,
@password,
@passwordsalt,
@email,
lower(@email),
@passwordquestion,
@passwordanswer,
@passwordformat,
@isapproved,
@islockedout,
@createdate,
@createdate,
@createdate,
@lastlockoutdate,
@failedpasswordattemptcount,
@failedpasswordattemptwindowstart,
@failedpasswordanswerattemptcount,
@failedpasswordanswerattemptwindowstart )
if( @@error <> 0 )
begin
set @errorcode = -1
goto cleanup
end
if (@newusercreated = 0)
begin
update dbo.aspnet_users
set lastactivitydate = @createdate
where @userid = userid
if( @@error <> 0 )
begin
set @errorcode = -1
goto cleanup
end
end
select @createdate = dateadd( n, @timezoneadjustment, @createdate )
if( @transtarted = 1 )
begin
set @transtarted = 0
commit transaction
end
return 0
cleanup:
if( @transtarted = 1 )
begin
set @transtarted = 0
rollback transaction
end
return @errorcode
end
go
夠長的,不過沒有關系,分幾個部分看,首先是定義一些要發(fā)揮得參數(shù),然后初始化,接著exec dbo.aspnet_applications_createapplication,調用aspnet_applications_createapplication存儲過程,建立一個名字為@applicationname 的application,如果該application不存在的話.并且返回該application的id,這里的applicationname在web.config membership節(jié)點中設置過,即:dev。如果執(zhí)行以上過程有錯誤,通過sql的goto語句跳至cleanup部分,執(zhí)行rollback transaction,回滾這次操作。如果沒有錯誤存儲過程就接著向下執(zhí)行,exec dbo.aspnet_getutcdate @timezoneadjustment, @createdate output,這是獲得當前utc時間。再下來就判斷aspnet_users表中用戶的userid是否在數(shù)據庫中有該userid(userid是一個guid),如果沒有就在表aspnet_users中建立。這時在進行一次失分發(fā)生錯誤的判斷,執(zhí)行的方法與前一次一樣。再下來判斷aspnet_membership表中是否有該userid存在,如果沒有就根據@uniqueemail參數(shù)判斷是否允許email在數(shù)據庫中重復。最后才是把user的信息插入aspnet_membership表,再下來還有一些對錯誤的處理...
其實這個存儲過程并不復雜,但是非常繁瑣的,也可以看出設計者對數(shù)據庫檢驗的嚴格性的要求非常高。有了對存儲過程一定的了解后,我們接下來那些傳遞的參數(shù)也就明白有何用處了,最后關閉數(shù)據庫的連接,把返回的這些參數(shù)通過實例化一個membershipuser類傳遞過去,然后返回這個實例化的membershipuser,這樣該方法就完成了一次操作。
最后我們看看數(shù)據庫,membership直接關聯(lián)的有3個表

表很簡單,關系也很明了,我就不多說了,總要給我留點時間吧,也給你自己留一些分析的空間,我要是全都說完了那你做什么?呵呵。
如果你了解cs系統(tǒng),你肯定會提出這樣一個疑問:用戶信息不只表membership中這一點呀,保存用戶個性化設置的如選用什么語言、什么皮膚等等信息的數(shù)據都在哪里?期待吧,那是后面的profile專題需要敘述的問題。
新聞熱點
疑難解答