前言
.net framework不再进行演进,.net的form认证也被各种OAuth/OpenId认证流程取代。但是旧系统很多可能还是会使用这一种方式。
介绍
.net框架在处理请求的每个阶段,都会触发各种事件,每个事件又有对应的处理模块。涉及授权的是这两个模块:
FormsAuthenticationModule
:从用户ticket
种获取用户信息,设置到HttpContext中。而ticket
默认是在Cookie
中的。UrlAuthorizationModule
:检查指定的地址是否获得授权,检查规则一般是在配置文件中指定的。如果未登录/授权,会返回HTTP 401 Unauthorized
。
当未获得授权时,FormsAuthenticationModule
会捕获401错误,将用户引导到登录页登录。
在登录页,一般通过校验用户名密码,生成用ticket
,存入cookie中,接下来就会跳转到之前未认证的页面。
来自官方的流程图:
基本使用(Cookie设置,IPrincipal和IIDentity)
使能
通过在web.config文件配置
<configuration>
<system.web>
... Unrelated configuration settings and comments removed for brevity ...
<!--
The <authentication> section enables configuration
of the security authentication mode used by
ASP.NET to identify an incoming user.
-->
<authentication mode="Forms" />
</system.web>
</configuration>
登录页
用户名密码的校验可以有各种方法,比如数据库、三方系统,这里不做介绍。
如果校验成功,就要让用户登录
,而所谓登录就是为该用户生成票据ticket
,在下次访问系统时,将ticket
传入系统,这样FormsAuthenticationModule
就会将用户信息识别并存到这个请求相应的上下文HttpContext
中,供后续模块使用。
System.Web.Security
命名控件提供了三个基本方法,帮助登录。
GetAuthCookie(username, persistCookie)
由用户名创建一个ticket
,并由该ticket
创建一个Cookie,persistCookie
控制的cookie是否持久有效。注意,此时生成的Cookie需要手动加入到响应中,才会生效SetAuthCookie(username, persistCookie)
除了GetAuthCookie
的功能,该函数直接会将生成的Cookie添加到请求响应的Cookie中。RedirectFromLoginPage(username, persistCookie)
,除了上一步SetAuthCookie
的功能外,该函数还会直接跳转到returnUrl
参数标识的页面
使用
Request
对象中的IsAuthenticated
属性可以用来判断用户是否登录。
当前登录用户的信息,可以通过HttpContext.Current.User
获取,这个值是FormsAuthenticationModule
设置的。
HttpContext.Current.User
实际是GenericPrincipal
类,而GenericPrincipal
扩展了IPrincipal
接口。principal
有主角的意思(注意不是principle
)。这个接口代表了用户信息。而IPrincipal
接口又包含两个成员
-
IsInRole(roleName)
判断当前用户是否属于某个组 -
Identity
该类扩展IIdentity
接口,代表了用户身份。它又包含几个主要成员
- AuthenticationType 认证方式,表明身分来源,对于form认证来说,会被设置成Forms
- IsAuthenticated 是否已认证
- Name 用户名,来自创建ticket
时的参数
- 而Form认证时,Identity类又被扩展为FormsIdentity
,从而包含了一个额外的成员Ticket
,可以获取原始的Ticket
信息这里可以对比`.net core`中的`HttpContext.Current.User`属性,同样实现了`IPrincipal`,但是实现类是`ClaimPrincipal`,.net core切换为了基于`Claim`声明类型的认证方式。而不是像这里Form认证中的基于`Role`角色的认证方式。
配置
认证基本配置
在web.config中的<form></form>
节点可以通过属性对认证进行扩展配置:
<authentication mode="Forms">
<forms
propertyName1="value1"
propertyName2="value2"
...
propertyNameN="valueN"
/>
</authentication>
支持的属性:
- cookieless:对于不支持cookie的情况,ticket可以直接编码在url参数中
- domain:ticket适用的路径,比如可以仅在网站的
http://url/subdomain
这一个路径下才发送ticket - loginUrl:修改
FormsAuthenticationModule
跳转的登录页面地址 - name: 存储的cookie的名称,默认是
.ASPXAUTH
- protection:控制ticket是否加密、校验,默认
ALL
代表即加密,也增加签名校验,这是最安全、推荐的方式 - slidingExpiration:ticket会过期,配置这个参数,用户每次登录会刷新过期时间。不过对于cookie来说,cookie默认的机制是过期时间过半才会刷新。
- timeout:过期时间,默认30分钟。在默认通过cookie存储的情况下,这个值即代表cookie的过期时间,也代表ticket的过期时间
- 。。。其他一些参数,不一一列举
安全考虑
在<machineKey>
节点下,可以配置ticket安全key参数。
分为加解密和校验两部分内容
- decryption和decryptionKey:加解密的算法和密钥
- validation和validationKey:校验的算法和密钥
密钥默认都是自动生成,通过算法保证唯一。但是对于集群部署,或者单点登录,ticket要在其他系统使用的情况,就需要在这些系统中,手动配置相同的密钥了。
扩展使用
在ticket中增加自定义数据
之前没有具体提到ticket
的内容,ticket
实际是FormsAuthenticationTicket
类,除了包含有效期、用户名信息以外,还有一个UserData
字段,可以在创建时存储一些用户自定义的信息(string类型)。比如有一些数据存在这里,可以不用每次都在数据库重新获取一遍。
但是设置UserData
的方式只能通过构造函数,可能是因为ticket
创建时,就是加密和签名好的了,不允许补充数据。而携带UserCode
的构造函数,又需要很多注入版本号、过期时间等,所以通常的做法是,先创建一个不带UserData
的ticket,然后取出这个ticket
的变量,连同我们所需的UserData
,创建一个新的Ticket:
// Query the user store to get this user's User Data
string userDataString = "My Data";
// Create the cookie that contains the forms authentication ticket
HttpCookie authCookie = FormsAuthentication.GetAuthCookie(UserName.Text, RememberMe.Checked);
// Get the FormsAuthenticationTicket out of the encrypted cookie
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
// Create a new FormsAuthenticationTicket that includes our custom User Data
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, userDataString);
// Update the authCookie's Value to use the encrypted version of newTicket
authCookie.Value = FormsAuthentication.Encrypt(newTicket);
// Manually add the authCookie to the Cookies collection
Response.Cookies.Add(authCookie);
获取UserData
// Get User Data from FormsAuthenticationTicket
FormsIdentity ident = User.Identity as FormsIdentity;
if (ident != null)
{
FormsAuthenticationTicket ticket = ident.Ticket;
// This as out UserData!
string userDataString = ticket.UserData;
}
自定义Principal
Form认证默认使用GenericPrincipal
,如果想增加自定义属性,我们可以通过UserData
属性添加,或者也可以替换GenericPrincipal
而使用自定义的类。
自定义的类CustomPrincipal
也实现IPrincipal
,可以添加自定义的属性,其中的IDentity属性,也可以用自定义的CustomIDentity
替换,只要实现了IIdentity
接口
那么在何时替换GenericPrincipal
呢?
GenericPrincipal
是FormsAuthenticationModule
模块在AuthenticateRequest
时间阶段被执行的,而在这个事件之后,asp.net会触发PostAuthenticateRequest
事件,于是在这里,可以手动设置所需要的CustomPrincipal
类,替换原有的GenericPrincipal
。
而注册监听该事件,可以在Global.ascx
中:
void Application_OnPostAuthenticateRequest(object sender, EventArgs e)
{
// Get a reference to the current User
IPrincipal usr = HttpContext.Current.User;
// If we are dealing with an authenticated forms authentication request
if (usr.Identity.IsAuthenticated && usr.Identity.AuthenticationType == "Forms")
{
FormsIdentity fIdent = usr.Identity as FormsIdentity;
// Create a CustomIdentity based on the FormsAuthenticationTicket
CustomIdentity ci = new CustomIdentity(fIdent.Ticket);
// Create the CustomPrincipal
CustomPrincipal p = new CustomPrincipal(ci);
// Attach the CustomPrincipal to HttpContext.User and Thread.CurrentPrincipal
HttpContext.Current.User = p;
Thread.CurrentPrincipal = p;
}
}
这里需要同时设置HttpContext.Current.User
和Thread.CurrentPrincipal
,这两个值在处理流程中都有可能被用到,需要保持一致。
这样设置之后,可以像之前获取GenericPrincipal
一样,获取CustomPrincipal
:
CustomIdentity ident = User.Identity as CustomIdentity;