注:这篇文章主要给新手看的,老手们可能会觉得没啥营养,就请绕过吧。
“认证”与“授权”是几乎所有系统中都会涉及的概念,通俗点讲:
认证(authentication) 就是 "判断用户有没有登录?",好比windows系统,没登录就无法使用(不管你是用Administrator或Guest用户,总之要先正确登录后,才能进入系统).
授权(authorization) 就是"用户登录后的身份/角色识别",好比"管理员用户"登录windows后,能安装软件、修改windows设置等所有操作,而Guest用户登录后,只有做有限的操作(比如安装软件就被禁止了).
.net中与"认证"对应的是IIdentity接口,而与"授权"对应的则是IPrincipal接口,这二个接口的定义均在命名空间System.Security.Principal中:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
using
System;
using
System.Runtime.InteropServices;
namespace
System.Security.Principal
{
[ComVisible(
true
)]
public
interface
IIdentity
{
string
AuthenticationType {
get
; }
bool
IsAuthenticated {
get
; }
string
Name {
get
; }
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
using
System;
using
System.Runtime.InteropServices;
namespace
System.Security.Principal
{
[ComVisible(
true
)]
public
interface
IPrincipal
{
IIdentity Identity {
get
; }
bool
IsInRole(
string
role);
}
}
|
应该注意到:IPrincipal接口中包含着一个只读的IIdentity,这也跟最开始提到的概念一致:识别身份的前提是先登录,只有登录成功后能进一步确认身份。
用Membership/Role做过asp.net开发的朋友们,看到这二个接口的定义,应该会觉得很眼熟,想想我们在Asp.Net页面中是如何判断用户是否登录以及角色的?
1
2
3
4
5
6
7
8
9
10
11
12
|
protected
void
Page_Load(
object
sender, EventArgs e)
{
HttpContext ctx = HttpContext.Current;
if
(ctx.User.Identity.IsAuthenticated && ctx.User.IsInRole(
"管理员"
))
{
//管理员该做的事,就写在这里
}
else
{
//Hi,您不是管理员,别胡来!
}
}
|
这段代码再熟悉不过了,没错!membership/role的原理就是基于这二个接口的,如果再对HttpContext.Current.User刨根问底,能发现下面的定义:
即:HttpContext.Current.User本身就是一个IPrincipal接口的实例。有了上面的预备知识,可以直奔主题了,先来一个Console控制台程序测试一下用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
using
System;
using
System.Security.Principal;
using
System.Threading;
namespace
ConsoleTest
{
class
Program
{
static
void
Main(
string
[] args)
{
GenericIdentity _identity =
new
GenericIdentity(
"chr"
);
GenericPrincipal _principal =
new
GenericPrincipal(_identity,
new
string
[] {
"管理员"
,
"网站会员"
});
Thread.CurrentPrincipal = _principal;
//并非必需,但在winform程序中有很用(后面会提到)
string
loginName = _principal.Identity.Name;
bool
isLogin = _principal.Identity.IsAuthenticated;
bool
isAdmin = _principal.IsInRole(
"管理员"
);
bool
isWebUser = _principal.IsInRole(
"网站会员"
);
Console.WriteLine(
"当前用户: {0}"
, loginName);
Console.WriteLine(
"是否已经登录? {0}"
, isLogin);
Console.WriteLine(
"是否管理员? {0}"
, isAdmin);
Console.WriteLine(
"是否网站会员? {0}"
, isWebUser);
Console.Read();
}
}
}
|
输出如下:
当前用户: chr
是否已经登录? True
是否管理员? True
是否网站会员? True
一切正常,没什么大不了,但Console默认只是一个单线程的程序,也没有丰富的GUI界面,所以...这个只不过是热身,看下接口定义的几个方法是否管用而已。
这二个接口同样也能用在Winform程序中,下面将创建一个WinForm应用,里面有二个窗口:Form1以及Form2,可以把Form1当成登录界面,而Form2则是程序主窗口,在很多管理软件中,主窗口都要求登录以后才能访问,我们就来模拟一下:
Form1的界面:
Form2更简单:(就一个只读的TextBox)
我想做的事情:在Form1上登录后,看看在Form2中,能否判断出用户已经登录,以及识别出身份。
Form1 中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
using
System;
using
System.Security.Principal;
using
System.Threading;
using
System.Windows.Forms;
namespace
WinformTest
{
public
partial
class
Form1 : Form
{
public
Form1()
{
InitializeComponent();
}
private
void
btnLogin_Click(
object
sender, EventArgs e)
{
if
(txtUserName.Text.Trim() ==
""
) {
MessageBox.Show(
"请输入用户名!"
);
txtUserName.Focus();
return
;
}
IIdentity _identity =
new
GenericIdentity(txtUserName.Text.Trim());
IPrincipal _principal =
new
GenericPrincipal(_identity,
new
string
[] {
"管理员"
});
Thread.CurrentPrincipal = _principal;
//将其附加到当前线程的CurrentPrincipal
MessageBox.Show(
"登录成功!"
);
}
private
void
btnShow_Click(
object
sender, EventArgs e)
{
(
new
Form2()).ShowDialog();
}
private
void
btnLogOut_Click(
object
sender, EventArgs e)
{
Thread.CurrentPrincipal =
null
;
MessageBox.Show(
"已经退出!"
);
}
}
}
|
Form2中的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
using
System;
using
System.Security.Principal;
using
System.Threading;
using
System.Windows.Forms;
namespace
WinformTest
{
public
partial
class
Form2 : Form
{
public
Form2()
{
InitializeComponent();
}
private
void
Form2_Load(
object
sender, EventArgs e)
{
IPrincipal _principal = Thread.CurrentPrincipal;
if
(_principal.Identity.IsAuthenticated)
{
this
.textBox1.Text =
"您已经登录,当前用户:"
+ _principal.Identity.Name;
this
.textBox1.Text += Environment.NewLine +
"当前角色:"
+ (_principal.IsInRole(
"管理员"
) ?
"管理员"
:
"非管理员"
);
}
else
{
this
.textBox1.Text =
"您还没有登录"
;
}
}
}
}
|
测试一下:如果在未登录的情况下,直接点击"Show窗体2",结果如下
如果输入用户名,并点击"登录"后,再点击"Show窗体2",结果如下:
很理想!Form2中直接就能判断用户是否登录,以及当前登录用户的角色。这里有一个关键的细节:
1
|
Thread.CurrentPrincipal = _principal;
//将其附加到当前线程的CurrentPrincipal
|
在Form1中,将登录后的_principal附加到当前线程的CurrentPrincipal,我们知道:每个程序不管它是不是多线程,总归是有一个默认的主线程的。所以只要把主线程的CurrentPrincipal与登录后的_principal关联起来后,其它任何窗体,都可以直接用它来做判断,如果判断通过,则可以这样或那样(包括创建多线程进行自己的处理),如果判断不通过,则可以拒绝继续操作。
Winform的问题解决了,再来考虑一下Webform,当然,你可以直接使用从Asp.Net2.0就支持的membership/role机制,但membership/role默认只支持sqlserver数据库(通过membership provider for oracle也可以支持oracle,但总有一些数据库不被支持,比如access、mysql、sqlite、db2等),假如你不想把用户名/密码这类信息保存在sqlserver中(甚至不想保存在数据库中,比如:xml),这时候就得开动脑筋了。
其实...就算不用membership/role,上面提到的这二个接口仍然是可以使用的,但有一个问题:winform中,IPrincipal接口的实例可以一直存储在内存中(直到程序退出),所以其它窗口就能继续访问它,以便做进一步的判断,但是在webform中,页面本身是无状态的,一旦服务器输出html到客户端浏览器后,客户端的页面就与服务器再无瓜葛了(你甚至可以离线浏览,前提是不刷新),那么最后的认证信息保存在什么地方呢?
答案就是客户端的浏览器Cookie!所以在WebForm中的做法稍有不同:
创建一个webApplication,里面新建4个页面:login.aspx,logout.aspx,default.aspx,gotoUrl.aspx,这四个页面的作用如下:
login.aspx : 登录页面
logout.aspx: 用来处理用户注销 (非必需,但建议把注销逻辑放在这里,以便任何需要注销的地方重复利用)
default.aspx: 登录完成后的显示页面
gotoUrl.aspx : 登录完成后,用来辅助做页面跳转的页面(非必需,但建议加上)
login.aspx代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="LoginTest.Login" %>
<!
DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<
head
runat="server">
<
title
></
title
>
</
head
>
<
body
>
<
form
id="form1" runat="server">
<
table
>
<
tr
>
<
td
>用户名:</
td
>
<
td
>
<
asp:TextBox
ID="txtUserName" runat="server" style="width:200px"></
asp:TextBox
></
td
>
</
tr
>
<
tr
>
<
td
>密 码:</
td
>
<
td
>
<
asp:TextBox
ID="txtPassword" runat="server" TextMode="Password" style="width:200px"></
asp:TextBox
>
</
td
>
</
tr
>
<
tr
>
<
td
></
td
>
<
td
>
<
asp:Button
ID="Button1" runat="server" Text="登 录" onclick="Button1_Click" />
</
td
>
</
tr
>
</
table
>
</
form
>
</
body
>
</
html
>
|
后置代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
using
System;
using
System.Web;
using
System.Web.Security;
namespace
LoginTest
{
public
partial
class
Login : System.Web.UI.Page
{
protected
void
Page_Load(
object
sender, EventArgs e)
{
}
protected
void
Button1_Click(
object
sender, EventArgs e)
{
string
user =
this
.txtUserName.Text;
//读取用户名
string
password =
this
.txtPassword.Text;
//读取密码
if
(ValidateUser(user, password) ==
true
)
//ValidateUser方法用来验证用户合法性的
{
//建立表单验证票据
FormsAuthenticationTicket Ticket =
new
FormsAuthenticationTicket(1, user, DateTime.Now, DateTime.Now.AddMinutes(30),
true
,
"管理员,会员"
,
"/"
);
//使用webcongfi中定义的方式,加密序列化票据为字符串
string
HashTicket = FormsAuthentication.Encrypt(Ticket);
//将加密后的票据转化成cookie
HttpCookie UserCookie =
new
HttpCookie(FormsAuthentication.FormsCookieName, HashTicket);
//添加到客户端cookie
Context.Response.Cookies.Add(UserCookie);
//登录成功后重定向
Response.Redirect(
"GotoUrl.aspx?returnUrl="
+ Server.UrlEncode(
"Default.aspx"
));
}
else
{
//登录失败后的处理
}
}
/// <summary>
/// 验证用户名/密码是否正确
/// </summary>
/// <param name="userName"></param>
/// <param name="pwd"></param>
/// <returns></returns>
private
bool
ValidateUser(
string
userName,
string
pwd) {
return
true
;
//当然实际开发中,您可以到数据库里查询校验,这里只是示例而已
}
}
}
|
GotoUrl.aspx:这个页面只是单纯的辅助跳转而已,所以aspx页面本身不用加什么代码,只需要在后置cs代码里简单处理一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
using
System;
namespace
LoginTest
{
public
partial
class
GotoUrl : System.Web.UI.Page
{
protected
void
Page_Load(
object
sender, EventArgs e)
{
string
_returnUrl = Request[
"returnUrl"
];
if
(
string
.IsNullOrEmpty(_returnUrl))
{
_returnUrl =
"~/default.aspx"
;
}
Response.Redirect(_returnUrl);
}
}
}
|
接下来应该是Default.aspx了,这里只是演示,所以没有后置代码,判断的逻辑全写在default.aspx本身:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LoginTest.Default" %>
<!
DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<
head
runat="server">
<
title
></
title
>
</
head
>
<
body
>
<
form
id="form1" runat="server">
<
div
>
<% if (User.Identity.IsAuthenticated)
{
Response.Write("<
span
style='color:red'>" + User.Identity.Name + "</
span
>已登录!");
if (User.IsInRole("管理员"))
{
Response.Write(" 当前用户角色:管理员");
}
if (User.IsInRole("会员"))
{
Response.Write(",会员。");
}
Response.Write(" <
a
href='logout.aspx'>安全退出</
a
>");
}
else
{
Response.Write("请先<
a
href='login.aspx'>登录</
a
>");
}
%>
</
div
>
</
form
>
</
body
>
</
html
>
|
最后一个是注销页面logout.aspx,类似的,这个页面本身只负责注销cookie票据,所以界面上没东西,只有后置代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
using
System;
using
System.Web.Security;
namespace
LoginTest
{
public
partial
class
Logout : System.Web.UI.Page
{
protected
void
Page_Load(
object
sender, EventArgs e)
{
FormsAuthentication.SignOut();
Response.Redirect(
"default.aspx"
);
}
}
}
|
如果您已经等不急的按下了F5想看下最终的结果,可能会令人失望:
咱还没登录呢,甚至连用户名,密码都没输入,咋会显示已登录?是不是想起了小沈阳的那句经典台词:为~什么呢?
这就是webform与winform不同的地方,asp.net默认的表单认证方式是Windows,所以程序一运行,asp.net就把windows当前的登录用户视为已经登录了,因此我们得改变asp.net的默认“傻帽”行为,修改web.config成下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<?
xml
version="1.0"?>
<
configuration
>
<
system.web
>
<
compilation
debug="true" targetFramework="4.0" />
<
authentication
mode="Forms">
<
forms
name=".ASPXAUTH"
loginUrl="login.aspx"
timeout="30"
path="/"
requireSSL="false"
domain="">
</
forms
>
</
authentication
>
</
system.web
>
</
configuration
>
|
哦,忘了告诉大家,我用的是asp.net 4.0,所以web.config显示十分简洁清爽。
ok,再来跑一下:
这回对了,点击“登录",转到login.aspx,然后在用户名里输入点啥(比如:"菩提树下的杨过"),然后会得到下面的结果:
认证已经成功了!但是好象还有点问题:并没有识别出身份!(即login.aspx.cs中代码指定的"管理员,会员"角色)
静下心来想想问题出在哪里?
在winform中,我们用
1
2
|
IPrincipal _principal =
new
GenericPrincipal(_identity,
new
string
[] {
"管理员"
});
Thread.CurrentPrincipal = _principal;
//将其附加到当前线程的CurrentPrincipal
|
给_principal授权为"管理员"(当然还能给它更多的角色),然后将其赋值为线程的CurrentPrincipal,所以就ok了,但是webform中并没有Thread.CurrentPrincipal,而且http本身又是无状态的,下一次http请求,根本无法记得上次请求时的状态(就好象每次http请求都是重新投胎一样,前世忘记得一干二净),幸好:微软为asp.net搞出一个上下文Context的概念,一个webApplication中,虽然http协议本身是无状态的,但是每个aspx页面被请求时,总会附带一个HttpContext上下文,可以用它来找回一些前世的记忆,而且文章最开头提到了 HttpContext.Current.User本身就是IPrincipal,这不就是Thread.CurrentPrincipal的变种么?
顺便再回忆一下Asp.Net的页面生命周期,每个AspX页面在请求认证时,都会触发Application_AuthenticateRequest事件,而这个事件是定义在Global.ascx中的,所以可以从这个入手:
新建一个Global.ascx,打开后置代码,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
using
System;
using
System.Security.Principal;
using
System.Web;
using
System.Web.Security;
namespace
LoginTest
{
public
class
Global : System.Web.HttpApplication
{
protected
void
Application_Start(
object
sender, EventArgs e)
{
}
protected
void
Session_Start(
object
sender, EventArgs e)
{
}
protected
void
Application_BeginRequest(
object
sender, EventArgs e)
{
}
/// <summary>
/// 每个aspx页面要求认证时被触发
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected
void
Application_AuthenticateRequest(
object
sender, EventArgs e)
{
HttpContext _ctx = HttpContext.Current;
if
(_ctx.User !=
null
)
{
if
(_ctx.User.Identity.IsAuthenticated ==
true
)
//认证成功的用户,才进行授权处理
{
FormsIdentity _Identity = (FormsIdentity)_ctx.User.Identity;
string
[] Roles = _Identity.Ticket.UserData.Split(
','
);
//将角色字符串,即login.aspx.cs中的“管理员,会员”,变成数组
_ctx.User =
new
GenericPrincipal(_Identity, Roles);
//将带有角色的信息,重新生成一个GenericPrincipal赋值给User,相当于winform中的Thread.CurrentPrincipal = _principal
}
}
}
protected
void
Application_Error(
object
sender, EventArgs e)
{
}
protected
void
Session_End(
object
sender, EventArgs e)
{
}
protected
void
Application_End(
object
sender, EventArgs e)
{
}
}
}
|
再测试一下,结果总算正常了:
最后再帮.Net做点广告:.Net是一个平台,其中的很多技术是全平台通用的(不管是winform还是webform),强烈建议大家尽量向微软自带的标准模型靠拢,这样在多种不同类型的应用整合时,将非常方便,而且兼容性好,容易升级。
经常看见有人winform中登录用一种做法(比如设置一个全局的静态变量,判断用户是否已经登录),然后webform中又动不少脑筋想一种做法(比如自己建用户表,搞加密算法,然后用session做判断),假如以后这二种应用要整合起来,估计要费不少劲(当然,也有设计得很好,一开始就考虑到日后的扩展的,但是这种毕竟少数,而且相对而言,对程序员的要求比较高),但是如果大家都用文中所提的标准模型(IIdentity,IPrincipal),要整合这二种应用是非常方便的。