如何在实现webwebapi form认证中添加认证和配置角色

/iykqqlpugocfnqe/item/e132329bdea22acbb6253105 &ASP.NET中处理请求的流程图
/yao/archive//434783.html
/fish-li/archive//2450571.html#_label3
这篇主要说说实现和逻辑流程.
所谓认证和授权是指对一个服务器资源的访问限制管理。
比如有一些文件是不公开的,只有管理人员能访问的到,这要求他们必须先"登入"。这就是认证
那么授权是在认证之后发生的事情,管理人员也有分等级,好比公司有些机密文件只有上层可以看。这就是授权.
认证和授权微软已经为我们做了很多封装,form 认证就是其中一种。
不过,这里我们先想想,在原始年代,我们要如何去实现呢?
我们都知道web所有资源都是通过http请求来访问的。显然第一步是拦截所有不公开的资源.&
第二步就是检查他们是否"登入".&
所谓的"登入"其实就是通过一个 cookie 来完成的。
如果请求没有附带指定的 cookie 那么就表示没有登入,就该阻止访问 (并跳转到登入界面).
在登入页面确认用户密码后,给予cookie,就表示登入了啦 .
通过了认证,我们必须查看这个用户的身份(或者说角色), 比如是经理,主管,还是普通员工。
进一步的验证用户是否有足够的权限(授权)来访问这个资源。
好了,其实不太难。大至少就是这样了。
以上步骤涉及到2个重要的点 :&
1. 如果拦截特定的资源请求 ?&
2. cookie 的安全性&
下面我来个一个微软封装好的例子, 一般上普通项目够用了。&
1.在web config 加上一个 authentication&
&system.web&
&authentication mode="Forms"&
&forms loginUrl="~/login/Default.aspx" timeout="2880" defaultUrl="~/" /&
&/authentication&
&/system.web&
这里用 mode 是 forms (我也只会这个)
loginUrl 是登入页面的路径 , timeout 是说cookie 的有效时间 , defaultUrl 我不清楚
2. 做一个登入页面,这里只是随便做。你明白就可以了
protected void Page_Load(object sender, EventArgs e)
protected void Button1_Click(object sender, EventArgs e) //登入
//set一个cookie , name and 是否要持久cookie,false的话会base on web config 的timeout
FormsAuthentication.SetAuthCookie("keatkeat", false);
protected void Button2_Click(object sender, EventArgs e) //注销
FormsAuthentication.SignOut();
3. 设定哪些文件路径需要拦截认证
&configuration&
&location path="securityFolder"&
&system.web&
&authorization&
&deny users="?"/&
&/authorization&
&/system.web&
&/location&
&/configuration&
path 指定路径,其下的所有folders files 都被限制了.
authorization 内的元素 有多种配搭模式&
&deny users="?"& &基本上由 3 的东东做出来,
1. deny | allow (禁止 或者 允许)
2.users | roles | verbs ( users 用户 , roles 角色比较特别,后面我会教你如何设置一个或多个角色在一个user身上,verbs 就是http method ,GET POST 等)
3. ? | * &( ? 代表匿名 , * 代表所有的)
所以上面这一句的解释是 &-禁止匿名用户- (没登入就无法访问)
任何访问都是 users="*" &, 登入后就不再是 users="?" &
完成以上的步骤基本上就可以做到一个简单的认证授权机制了(不需要分角色的话)
它验证的次序是这样的,如果pass了就不会继续验证了,所以一般上都是先写,deny 才写 allow
那么如果我们要高级一点的呢?&
&location path="securityFolder"&
&system.web&
&authorization&
&allow roles="Admin,Boss"/&
&deny users="*"/&
&/authorization&
&/system.web&
&/location&
允许角色为Admin或者Boss , 禁止所有用户
现在我们必须把用户的角色添加进用户里 (因为从上面开来,我们只给了个Name给用户)
class AuthenticateHttpModule : IHttpModule
public void Dispose() { }
public void Init(HttpApplication context)
context.AuthenticateRequest += new EventHandler(AuthenticateRequest);
private void AuthenticateRequest(object sender, EventArgs e)
HttpApplication app = (HttpApplication)
HttpContext ctx = app.C //获取本次Http请求的HttpContext对象
if (ctx.User != null)
if (ctx.Request.IsAuthenticated == true) //验证过的一般用户才能进行角色验证
string name = ctx.User.Identity.N
FormsIdentity fi = (System.Web.Security.FormsIdentity)ctx.User.I
//FormsAuthenticationTicket ticket = fi.T //取得身份验证票
//string userData = ticket.UserD//从UserData中恢复role信息
string[] roles = "Admin,zz".Split(','); //将角色数据转成字符串数组,得到相关的角色信息
ctx.User = new GenericPrincipal(fi, roles); //这样当前用户就拥有角色信息了
这里我们要写一个 HttpModule 来完成 (记得web config 也要添加哦)
我们用 new GenericPrincipal 来添加角色进用户里,这样就可以了。&
注 : 我们这个模块是跑在微软后面的,所以我们完全不需要从cookie里面获取任何东西,直接用 context.User 就好了。
以上大概就是全部的过程了。
这里给一个自定义的例子 :&
public class AdministratorIdentity : IIdentity
public string AuthenticationType { get; set; }
public string Name { get; set; }
public bool IsAuthenticated { get; set; }
public class Administrator : IPrincipal
public IIdentity Identity { get; set; }
public string name { get; set; }
//可以任意定义属性
public bool IsInRole(string role)
if (role == "Admin") //个种你想的到的验证手法都可以
return true;
return false;
if (ctx.Request.IsAuthenticated == true) //验证过的一般用户才能进行角色验证
string name = ctx.User.Identity.N
string type = ctx.User.Identity.AuthenticationT
ctx.User = new Administrator
name = "keatkeat",
Identity = new AdministratorIdentity {
AuthenticationType = ctx.User.Identity.AuthenticationType,
Name = "z",
IsAuthenticated = true
//原版添加 roles 的方式
//FormsIdentity fi = (System.Web.Security.FormsIdentity)ctx.User.I
////FormsAuthenticationTicket ticket = fi.T //取得身份验证票
////string userData = ticket.UserD//从UserData中恢复role信息
//string[] roles = "Admin".Split(','); //将角色数据转成字符串数组,得到相关的角色信息
//ctx.User = new GenericPrincipal(fi, roles); //这样当前用户就拥有角色信息了
这样到哪里只要 Ctx.User as Administrator 就可以容易的使用啦 ^^
这里也提一提使用 cookie 加密的安全性问题
第一,如果有人可以从你的电脑上获取到你的cookie , 那么他就等于拥有了你所有权限了。
第二,如果他没有入侵你的电脑,他是否可以自己创建一个加密的cookie来模拟你呢?
答案是不行,因为创建cookie时,加密是配合服务器的私钥的。(好像叫对称加密)
所以呢,基本上算是安全的。 参考 :&http://blog.csdn.net/fancyf/article/details/348202
要自定义服务器的私钥的话可以这样写 :&
&configuration&
&system.web&
&machineKey
validationKey="xxxxxxxxxxxxxxxxxxxxxxxx"
decryptionKey="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
validation="SHA1"
decryption="AES" /&
&/system.web&
&/configuration&
好像也可以指定一个程序来输出 ,&
machineKey 可以通过这个网站创建&/utility/Machine-Key-Generator.aspx#
下面我另外谈谈我的一些开发经验。
现今我们做的大部分是单页面应用,只有一个登入页面和一个主页面,其它的页面都是虚拟的。
如果是自己做 url rewrite 的话,要注意的是,请在&ResolveRequestCache(认证授权模块之后) 时才做.
以上的部分,如果你想自己实现也是完全可以的,cookie 的加密可以用微软的加密方法,你也可以继承 IPrincipal 来实现 自己的 User
也可以注册 HttpModule 拦截&AuthorizeRequest 比对路径,去sql 拿用户职位等等来做授权验证。&
还有如果你用的是 WebAPI 的话,建议要把这2者分开。以上说的拦截是针对页面资源的访问。
WebAPI 内部也有拦截认证和授权的机制。所以针对 WebAPI 的资源还是用用 WebAPI 本身的机制来管理比较妥当.
WebAPI 是支持self host,但是如果我们是使用IIS又贪方便的话,我们也可以直接用上面的form认证。
所以的API请求依然会通过IIS的 pipe,到了API controller ,User 依然是我们的 context.User&
如果遇到是 self host 的话,其实也可以用上面的概念来做。只不过不使用cookie 改成使用 http header 来替代。
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(2, loginName, DateTime.Now, DateTime.Now.AddDays(1), true, data);
string cookieValue = FormsAuthentication.Encrypt(ticket);
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookieValue);
这个加密解密做好其实原理依旧是通的啦。&
简单的说 认证与授权 ,不外乎就是 &请求时附上身份,响应前验证身份。&
阅读(...) 评论()27666人阅读
Asp.net Web技术(9)
WebAPI(3)
前言:Web 用户的身份验证,及页面操作权限验证是B/S系统的基础功能,一个功能复杂的业务应用系统,通过角色授权来控制用户访问,本文通过Form认证,Mvc的Controller基类及Action的权限验证来实现Web系统登录,Mvc前端权限校验以及WebApi服务端的访问校验功能。
1. Web Form认证介绍
Web应用的访问方式因为是基于浏览器的Http地址请求,所以需要验证用户身份的合法性。目前常见的方式是Form认证,其处理逻辑描述如下:
1. 用户首先要在登录页面输入用户名和密码,然后登录系统,获取合法身份的票据,再执行后续业务处理操作;
2. 用户在没有登录的情况下提交Http页面访问请求,如果该页面不允许匿名访问,则直接跳转到登录页面;
3. 对于允许匿名访问的页面请求,系统不做权限验证,直接处理业务数据,并返回给前端;
4. 对于不同权限要求的页面Action操作,系统需要校验用户角色,计算权限列表,如果请求操作在权限列表中,则正常访问,如果不在权限列表中,则提示“未授权的访问操作”到异常处理页面。
2. WebApi 服务端Basic 方式验证
WebApi服务端接收访问请求,需要做安全验证处理,验证处理步骤如下:
1. 如果是合法的Http请求,在Http请求头中会有用户身份的票据信息,服务端会读取票据信息,并校验票据信息是否完整有效,如果满足校验要求,则进行业务数据的处理,并返回给请求发起方;
2. 如果没有票据信息,或者票据信息不是合法的,则返回“未授权的访问”异常消息给前端,由前端处理此异常。
3. 登录及权限验证流程
流程处理步骤说明:
1. 用户打开浏览器,并在地址栏中输入页面请求地址,提交;
2. 浏览器解析Http请求,发送到Web服务器;Web服务器验证用户请求,首先判断是否有登录的票据信息;
3. 用户没有登录票据信息,则跳转到登录页面;
4. 用户输入用户名和密码信息;
5. 浏览器提交登录表单数据给Web服务器;
6. Web服务需要验证用户名和密码是否匹配,发送api请求给api服务器;
7. api用户账户服务根据用户名,读取存储在数据库中的用户资料,判断密码是否匹配;
1)如果用户名和密码不匹配,则提示密码错误等信息,然该用户重新填写登录资料;
2)如果验证通过,则保存用户票据信息;
8. 接第3步,如果用户有登录票据信息,则跳转到用户请求的页面;
9. 验证用户对当前要操作的页面或页面元素是否有权限操作,首先需要发起api服务请求,获取用户的权限数据;
10. api用户权限服务根据用户名,查找该用户的角色信息,并计算用户权限列表,封装为Json数据并返回;
11. 当用户有权限操作页面或页面元素时,跳转到页面,并由页面Controller提交业务数据处理请求到api服务器;
& &如果用户没有权限访问该页面或页面元素时,则显示“未授权的访问操作”,跳转到系统异常处理页面。
12. api业务服务处理业务逻辑,并将结果以Json 数据返回;
13. 返回渲染后的页面给浏览器前端,并呈现业务数据到页面;
14. 用户填写业务数据,或者查找业务数据;
15. 当填写或查找完业务数据后,用户提交表单数据;
16. 浏览器脚本提交get,post等请求给web服务器,由web服务器再次解析请求操作,重复步骤2的后续流程;
17. 当api服务器验证用户身份是,没有可信用户票据,系统提示“未授权的访问操作”,跳转到系统异常处理页面。
4. Mvc前端代码示例
4.1 用户登录AccountController
public class AccountController : Controller
// GET: /Logon/
public ActionResult Login(string returnUrl)
ViewBag.ReturnUrl = returnU
return View();
[HttpPost]
public ActionResult Logon(LoginUser loginUser, string returnUrl)
string strUserName = loginUser.UserN
string strPassword = loginUser.P
var accountModel = new AccountModel();
//验证用户是否是系统注册用户
if (accountModel.ValidateUserLogin(strUserName, strPassword))
if (Url.IsLocalUrl(returnUrl))
//创建用户ticket信息
accountModel.CreateLoginUserTicket(strUserName, strPassword);
//读取用户权限数据
accountModel.GetUserAuthorities(strUserName);
return new RedirectResult(returnUrl);
return RedirectToAction(&Index&, &Home&);
throw new ApplicationException(&无效登录用户!&);
/// &summary&
/// 用户注销,注销之前,清除用户ticket
/// &/summary&
/// &returns&&/returns&
[HttpPost]
public ActionResult Logout()
var accountModel = new AccountModel();
accountModel.Logout();
return RedirectToAction(&Login&, &Account&);
4.2 用户模型AccountModel
public class AccountModel
/// &summary&
/// 创建登录用户的票据信息
/// &/summary&
/// &param name=&strUserName&&&/param&
internal void CreateLoginUserTicket(string strUserName, string strPassword)
//构造Form验证的票据信息
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, strUserName, DateTime.Now, DateTime.Now.AddMinutes(90),
true, string.Format(&{0}:{1}&, strUserName, strPassword), FormsAuthentication.FormsCookiePath);
string ticString = FormsAuthentication.Encrypt(ticket);
//把票据信息写入Cookie和Session
//SetAuthCookie方法用于标识用户的Identity状态为true
HttpContext.Current.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, ticString));
FormsAuthentication.SetAuthCookie(strUserName, true);
HttpContext.Current.Session[&USER_LOGON_TICKET&] = ticS
//重写HttpContext中的用户身份,可以封装自定义角色数据;
//判断是否合法用户,可以检查:HttpContext.User.Identity.IsAuthenticated的属性值
string[] roles = ticket.UserData.Split(',');
IIdentity identity = new FormsIdentity(ticket);
IPrincipal principal = new GenericPrincipal(identity, roles);
HttpContext.Current.User =
/// &summary&
/// 获取用户权限列表数据
/// &/summary&
/// &param name=&userName&&&/param&
/// &returns&&/returns&
internal string GetUserAuthorities(string userName)
//从WebApi 访问用户权限数据,然后写入Session
string jsonAuth = &[{\&Controller\&: \&SampleController\&, \&Actions\&:\&Apply,Process,Complete\&}, {\&Controller\&: \&Product\&, \&Actions\&: \&List,Get,Detail\&}]&;
var userAuthList = ServiceStack.Text.JsonSerializer.DeserializeFromString(jsonAuth, typeof(UserAuthModel[]));
HttpContext.Current.Session[&USER_AUTHORITIES&] = userAuthL
return jsonA
/// &summary&
/// 读取数据库用户表数据,判断用户密码是否匹配
/// &/summary&
/// &param name=&name&&&/param&
/// &param name=&password&&&/param&
/// &returns&&/returns&
internal bool ValidateUserLogin(string name, string password)
//bool isValid = password == passwordInD
/// &summary&
/// 用户注销执行的操作
/// &/summary&
internal void Logout()
FormsAuthentication.SignOut();
4.3 控制器基类WebControllerBase
/// &summary&
/// 前端Mvc控制器基类
/// &/summary&
[Authorize]
public abstract class WebControllerBase : Controller
/// &summary&
/// 对应api的Url
/// &/summary&
public string ApiUrl
/// &summary&
/// 用户权限列表
/// &/summary&
public UserAuthModel[] UserAuthList
return AuthorizedUser.Current.UserAuthL
/// &summary&
/// 登录用户票据
/// &/summary&
public string UserLoginTicket
return AuthorizedUser.Current.UserLoginT
4.4 权限属性RequireAuthorizationAttribute
/// &summary&
/// 权限验证属性类
/// &/summary&
public class RequireAuthorizeAttribute : AuthorizeAttribute
/// &summary&
/// 用户权限列表
/// &/summary&
public UserAuthModel[] UserAuthList
return AuthorizedUser.Current.UserAuthL
/// &summary&
/// 登录用户票据
/// &/summary&
public string UserLoginTicket
return AuthorizedUser.Current.UserLoginT
public override void OnAuthorization(AuthorizationContext filterContext)
base.OnAuthorization(filterContext);
////验证是否是登录用户
var identity = filterContext.HttpContext.User.I
if (identity.IsAuthenticated)
var actionName = filterContext.ActionDescriptor.ActionN
var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerN
//验证用户操作是否在权限列表中
if (HasActionQulification(actionName, controllerName, identity.Name))
if (!string.IsNullOrEmpty(UserLoginTicket))
//有效登录用户,有权限访问此Action,则写入Cookie信息
filterContext.HttpContext.Response.Cookies[FormsAuthentication.FormsCookieName].Value = UserLoginT
//用户的Session, Cookie都过期,需要重新登录
filterContext.HttpContext.Response.Redirect(&~/Account/Login&, false);
//虽然是登录用户,但没有该Action的权限,跳转到“未授权访问”页面
filterContext.HttpContext.Response.Redirect(&~/Home/UnAuthorized&, true);
//未登录用户,则判断是否是匿名访问
var attr = filterContext.ActionDescriptor.GetCustomAttributes(true).OfType&AllowAnonymousAttribute&();
bool isAnonymous = attr.Any(a =& a is AllowAnonymousAttribute);
if (!isAnonymous)
//未验证(登录)的用户, 而且是非匿名访问,则转向登录页面
filterContext.HttpContext.Response.Redirect(&~/Account/Login&, true);
/// &summary&
/// 从权限列表验证用户是否有权访问Action
/// &/summary&
/// &param name=&actionName&&&/param&
/// &param name=&controllerName&&&/param&
/// &returns&&/returns&
private bool HasActionQulification(string actionName, string controllerName, string userName)
//从该用户的权限数据列表中查找是否有当前Controller和Action的item
var auth = UserAuthList.FirstOrDefault(a =&
bool rightAction =
bool rightController = a.Controller == controllerN
if (rightController)
string[] actions = a.Actions.Split(',');
rightAction = actions.Contains(actionName);
return rightA
//此处可以校验用户的其它权限条件
//var notAllowed = HasOtherLimition(userName);
//var result = (auth != null) && notA
return (auth != null);
4.5 业务Controller示例
public class ProductController : WebControllerBase
[AllowAnonymous]
public ActionResult Query()
return View(&ProductQuery&);
//[AllowAnonymous]
[RequireAuthorize]
public ActionResult Detail(string id)
var cookie = HttpContext.Request.C
string url = base.ApiUrl + &/Get/& +
HttpClient httpClient = HttpClientHelper.Create(url, base.UserLoginTicket);
string result = httpClient.GetString();
var model = JsonSerializer.DeserializeFromString&Product&(result);
ViewData[&PRODUCT_ADD_OR_EDIT&] = &E&;
return View(&ProductForm&, model);
5. &WebApi 服务端代码示例
5.1 控制器基类ApiControllerBase
/// &summary&
/// Controller的基类,用于实现适合业务场景的基础功能
/// &/summary&
/// &typeparam name=&T&&&/typeparam&
[BasicAuthentication]
public abstract class ApiControllerBase : ApiController
5.2 权限属性BaseAuthenticationAttribute
/// &summary&
/// 基本验证Attribtue,用以Action的权限处理
/// &/summary&
public class BasicAuthenticationAttribute : ActionFilterAttribute
/// &summary&
/// 检查用户是否有该Action执行的操作权限
/// &/summary&
/// &param name=&actionContext&&&/param&
public override void OnActionExecuting(HttpActionContext actionContext)
//检验用户ticket信息,用户ticket信息来自调用发起方
if (actionContext.Request.Headers.Authorization != null)
//解密用户ticket,并校验用户名密码是否匹配
var encryptTicket = actionContext.Request.Headers.Authorization.P
if (ValidateUserTicket(encryptTicket))
base.OnActionExecuting(actionContext);
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
//检查web.config配置是否要求权限校验
bool isRquired = (WebConfigurationManager.AppSettings[&WebApiAuthenticatedFlag&].ToString() == &true&);
if (isRquired)
//如果请求Header不包含ticket,则判断是否是匿名调用
var attr = actionContext.ActionDescriptor.GetCustomAttributes&AllowAnonymousAttribute&().OfType&AllowAnonymousAttribute&();
bool isAnonymous = attr.Any(a =& a is AllowAnonymousAttribute);
//是匿名用户,则继续执行;非匿名用户,抛出“未授权访问”信息
if (isAnonymous)
base.OnActionExecuting(actionContext);
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
base.OnActionExecuting(actionContext);
/// &summary&
/// 校验用户ticket信息
/// &/summary&
/// &param name=&encryptTicket&&&/param&
/// &returns&&/returns&
private bool ValidateUserTicket(string encryptTicket)
var userTicket = FormsAuthentication.Decrypt(encryptTicket);
var userTicketData = userTicket.UserD
string userName = userTicketData.Substring(0, userTicketData.IndexOf(&:&));
string password = userTicketData.Substring(userTicketData.IndexOf(&:&) + 1);
//检查用户名、密码是否正确,验证是合法用户
//var isQuilified = CheckUser(userName, password);
5.3 api服务Controller实例
public class ProductController : ApiControllerBase
public object Find(string id)
return ProductServiceInstance.Find(2);
// GET api/product/5
[AllowAnonymous]
public Product Get(string id)
var headers = Request.H
var p = ProductServiceInstance.GetById&Product, EPProduct&(long.Parse(id));
if (p == null)
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest)
Content = new StringContent(&id3 not found&), ReasonPhrase = &product id not exist.& });
6. 其它配置说明
6.1 Mvc前端Web.Config 配置
&system.web&
&compilation debug=&true& targetFramework=&4.5&&
&assemblies&
&add assembly=&System.Web.Http.Data.Helpers, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf& /&
&/assemblies&
&/compilation&
&httpRuntime targetFramework=&4.5& /&
&authentication mode=&Forms&&
&forms loginUrl=&~/Account/Login& defaultUrl=&~/Home/Index& protection=&All& timeout=&90& name=&.AuthCookie&&&/forms&
&/authentication&
&machineKey validationKey=&3FFA12388DDF585BA5D35E7BC87E3F0AB47FBBEBD12240DD3BEA2BEAEC4ABA213F22AD27E8FAD77DCFEE908D193A17C1FC8DCE51B71A4AE54920& decryptionKey=&ECB6A3AF9ABBF3F16E8B0B13CCEE538EBBA97D0BB& validation=&SHA1& decryption=&AES& /&
&/system.web&
machineKey节点配置,是应用于对用户ticket数据加密和解密。
6.2 WebApi服务端Web.Config配置
&system.web&
&machineKey validationKey=&3FF112388DDF585BA5D35E7BC87E3F0AB47FBBEBD12240DD3BEA2BEAEC4ABA213F22AD27E8FAD77DCFEE908D193A17C1FC8DCE51B71A4AE54920& decryptionKey=&ECB6A3AF9ABBF3F16E8B0B13CCEE538EBBA97D0BB& validation=&SHA1& decryption=&AES& /&
&/system.web&
machineKey节点配置,是应用于对用户ticket数据加密和解密。
Web系统的用户登录及页面操作权限验证在处理逻辑上比较复杂,需要考虑到Form认证、匿名访问,Session和Cookie存储,以及Session和Cookie的过期处理,本文实现了整个权限验证框架的基本功能,供系统架构设计人员以及Web开发人员参考。
Demo项目代码地址:
/besley/DemoUserAuthorization/
8. 春风吹又起--后记
由于项目的需要,彻底打造了登录及权限验证的一个新开源项目SlickSafe.NET,欢迎大家挪步。
/besley/slicksafe
/slickflow/p/6478887.html
在线DEMO地址:
(用户名密码:admin/123456, jack/123456)
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:177610次
积分:1896
积分:1896
排名:千里之外
原创:22篇
转载:43篇
评论:64条
(2)(1)(2)(1)(1)(2)(1)(2)(1)(1)(1)(1)(1)(4)(2)(5)(1)(1)(2)(13)(6)(2)(8)(1)(1)(1)(1)

我要回帖

更多关于 webform 动态添加控件 的文章

 

随机推荐