1、前言:
WebAPI主要开放数据给手机APP,Pad,其他需要得知数据的系统,或者软件应用。Web 用户的身份验证,及页面操作权限验证是B/S系统的基础功能。我上次写的《Asp.Net MVC WebAPI的创建与前台Jquery ajax后台HttpClient调用详解》这种跟明显安全性不是那么好,于是乎这个就来了 ,用户需要访问的API都必须带有票据过来,说白了就是登陆之后含有用户信息的Token。开始撸...
2、新建一个WebApi项目,在App_Start文件夹下面新建一个BaseApiController控制器,这是基础的Api控制器,后面有要验证的接口都继承这个控制器:
using LoginReqToken.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; namespace LoginReqToken.App_Start { ////// 基础Api控制器 所有的都继承他 /// public class BaseApiController : ApiController { /// /// 构造函数赋值 /// public BaseApiController() { TokenValue = HttpContext.Current.Session[LoginID] ?? ""; HttpContext.Current.Request.Headers.Add("TokenValue", TokenValue.ToString()); } /// /// 数据库上下文 /// public WYDBContext db = WYDBContextFactory.GetDbContext(); /// /// token值 登录后赋值请求api的时候添加到header中 /// public static object TokenValue { get; set; } = ""; /// /// 登录者账号 /// public static string LoginID { get; set; } = ""; } }
这个构造函数里主动加一个header头信息 ,因为每次访问的时候都要执行构造函数,在那边验证的时候都要从Header中取出来,计算出用户名 是否跟Session缓存的一致这样判断的
3、在建一个TokenCheckFilter.cs继承AuthorizeAttribute重写基类的验证方式,重写HandleUnauthorizedRequest
using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Web; using System.Web.Helpers; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Security; namespace LoginReqToken.App_Start { ////// token验证 /// public class TokenCheckFilter: AuthorizeAttribute { /// /// 重写基类的验证方式,加入自定义的Ticket验证 /// /// public override void OnAuthorization(HttpActionContext actionContext) { var content = actionContext.Request.Properties["MS_HttpContext"] as HttpContextBase; //获取token(请求头里面的值) var token = HttpContext.Current.Request.Headers["TokenValue"] ?? ""; //是否为空 if (!string.IsNullOrEmpty(token.ToString())) { //解密用户ticket,并校验用户名密码是否匹配 if (ValidateTicket(token.ToString())) base.IsAuthorized(actionContext); else HandleUnauthorizedRequest(actionContext); } //如果取不到身份验证信息,并且不允许匿名访问,则返回未验证403 else { var attributes = actionContext.ActionDescriptor.GetCustomAttributes ().OfType (); bool isAnonymous = attributes.Any(a => a is AllowAnonymousAttribute); if (isAnonymous) base.OnAuthorization(actionContext); else HandleUnauthorizedRequest(actionContext); } } //校验用户名密码(对Session匹配,或数据库数据匹配) private bool ValidateTicket(string encryptToken) { //解密Ticket var strTicket = FormsAuthentication.Decrypt(encryptToken).UserData; //从Ticket里面获取用户名和密码 var index = strTicket.IndexOf("&"); string userName = strTicket.Substring(0, index); string password = strTicket.Substring(index + 1); //取得session,不通过说明用户退出,或者session已经过期 var token = HttpContext.Current.Session[userName]; if (token == null) return false; //对比session中的令牌 if (token.ToString() == encryptToken) return true; return false; } /// /// 重写HandleUnauthorizedRequest /// /// protected override void HandleUnauthorizedRequest(HttpActionContext filterContext) { base.HandleUnauthorizedRequest(filterContext); var response = filterContext.Response = filterContext.Response ?? new HttpResponseMessage(); //状态码401改为其他状态码来避免被重定向。最合理的是改为403,表示服务器拒绝。 response.StatusCode = HttpStatusCode.Forbidden; var content = new { success = false, errs = new[] { "服务端拒绝访问:你没有权限?,或者掉线了?" } }; response.Content = new StringContent(Json.Encode(content), Encoding.UTF8, "application/json"); } } }
4、在WebApiConfig.cs配置文件里面修改一下路由加上/{action},这样就能调用到具体的哪一个了
Webapi默认是不支持Session的,所以我们需要在Global加载时候添加对Session的支持,在Global.asax里面重写Application_PostAuthorizeRequest,不然运行调用会直接异常
public class WebApiApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } ////// 重写Application_PostAuthorizeRequest /// protected void Application_PostAuthorizeRequest() { //对Session的支持,不然运行调用会直接异常 HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required); } }
5、现在来写一个登陆,新建一个控制器LoginController继承BaseApiController 里面写一个登陆的方法Login 登陆页面就直接在Home的index里面写一个简单的就行了这个控制器访问就不受限制了加上注解AllowAnonymous
[AllowAnonymous] public class LoginController : BaseApiController { [HttpGet] public object Login(string uName, string uPassword) { var user = db.Users.Where(x => x.LoginID == uName && x.Password == uPassword).FirstOrDefault(); if (user==null) { return Json(new { ret = 0, data = "", msg = "用户名密码错误" }); } FormsAuthenticationTicket token = new FormsAuthenticationTicket(0, uName, DateTime.Now, DateTime.Now.AddHours(12), true, $"{uName}&{uPassword}", FormsAuthentication.FormsCookiePath); //返回登录结果、用户信息、用户验证票据信息 var _token = FormsAuthentication.Encrypt(token); //将身份信息保存在session中,验证当前请求是否是有效请求 LoginID = uName; TokenValue = _token; HttpContext.Current.Session[LoginID] = _token; return Json(new { ret = 1, data = _token, msg = "登录成功!" }); } }
登陆页面 简单而粗暴
<br /><br /> <input type="text" name="txtLoginID" id="txtLoginID" /> <br /><br /> <input type="password" name="txtPassword" id="txtPassword" /> <br /><br /> <input type="button" id="btnSave" value="登录验证" /> <script type="text/javascript" src="~/Scripts/jquery-3.3.1.js">script> <script type="text/javascript"> $(document).ready(function () { $("#btnSave").click(function () { $.ajax({ type: "GET", url: "/Api/Login/Login", dataType: "json", data: { "uName": $("#txtLoginID").val(), "uPassword": $("#txtPassword").val()}, success: function (data) { if (data.ret > 0) { alert(data.msg+"Token: "+data.data); } else { alert(data.msg); } }, error: function (ret) { console.log(ret); } }); }); }); script>
登陆这个我是写了链接数据库的自己练习可以最易更改一个固定的值 现在应该可以看到返回的Token数据了
6、现在就可以写Api了 都继承BaseApiController这个控制器的方法上面需要验证的都要加上验证的注解,我是整个控制都要就直接写在类上面了,随便写一个举举例子,就比如全国省市县的查询
using LoginReqToken.App_Start; using LoginReqToken.Models; using LoginReqToken.Models.DTO; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; namespace LoginReqToken.Controllers { ////// 区域查询 /// [TokenCheckFilter] public class AreaOpController : BaseApiController { /// /// 获取全部区域 /// /// public Result GetAllAreas() { var data = db.AddressAll.OrderBy(x => x.ID); if(data.Count()>0) { var ret = new Result() { Ret = 1, Code = "200", Msg = "获取数据成功", Data = JsonConvert.SerializeObject(data) }; return ret; } else { var ret = new Result() { Ret = 0, Code = "400", Msg = "接口失败异常", Data = "" }; return ret; } } /// /// 查询某个省市直辖市自治区下所有的信息 /// /// 省名称(全名) /// public Result GetProvinceByName(string name) { var codeID = db.AddressAll.FirstOrDefault(x => x.Name == name)?.ID; if(codeID<=0) { var ret = new Result() { Ret = 1, Code = "F", Msg = "没有查到相关数据", Data = "" }; return ret; } var bb = db.AddressAll.Where(x=>x.ID>0).AsEnumerable(); var data = GetProvinceCity(bb,codeID).ToList(); if (data.Count() > 0) { var ret = new Result() { Ret = 1, Code = "200", Msg = "获取数据成功", Data = JsonConvert.SerializeObject(data) }; return ret; } else { var ret = new Result() { Ret = 0, Code = "500", Msg = "查询不到数据或者接口调用出错", Data = "" }; return ret; } } /// /// 递归获取树形数据 /// /// /// /// public IEnumerable<object> GetProvinceCity(IEnumerable areasDTOs,int? parentID) { var data = areasDTOs as AddressAll[] ?? areasDTOs.ToArray(); var ret = data.Where(n => n.ParentID == parentID).Select(n => new { n.ID, n.Code, n.ParentID, n.Name, n.LevelNum, n.OrderNum, children = GetProvinceCity(data, n.ID) }); return ret; } } }
记录一个EF随意取数据库条数信息是这么写的 var data = db.CnblogsList.OrderBy(p => Guid.NewGuid()).Take(100);
现在看效果图
没有登陆的时候是进不去的 postman上面的效果也看一下
效果都是一样的,如果登录了就可以直接访问 了 不用加参数 ,只有方法需要参数的就可以加
这里贴一个调用的代码:
HttpClient bb = new HttpClient(); //获取端口 HttpContent httpContent = new StringContent(""); httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); var dl = bb.GetAsync("http://localhost:63828/api/Login/login?uName=admin&uPassword=admin888").Result.Content.ReadAsStringAsync().Result; var token = JsonConvert.DeserializeObject(dl); for (var i=0;i<100;i++) { var ret = bb.GetAsync("http://localhost:63828/api/Cnblog/GetAllArtic").Result.Content.ReadAsStringAsync().Result; }
7、总结:
1)、总体思路,如果是合法的Http请求,在Http请求头中会有用户身份的票据信息,服务端会读取票据信息,并校验票据信息是否完整有效,如果满足校验要求,则进行业务数据的处理,并返回给请求发起方;
2) 如果没有票据信息,或者票据信息不是合法的,则返回“未授权的访问”异常消息给前端,由前端处理此异常。
3)、登录的时候判断用户名跟密码对不对,对了就生成用户信息的Token,Session保存一个Token,BaseApiController里面的登录名跟Token也赋值了。保存这些票据信息。
4)、当用户有权限操作页面或页面元素时,跳转到页面,并由页面Controller提交业务数据处理请求到api服务器; 如果用户没有权限访问该页面或页面元素时,则显示“未授权的访问操作”,跳转到系统异常处理页面。
5)、 api业务服务处理业务逻辑,并将结果以Json 数据返回,返回渲染后的页面给浏览器前端,并呈现业务数据到页面;
8、测试地址:
http://www.yijianlan.com:8001/ ---------------------->先登录,用户名 test密码 123456 可以调用调试的接口 然后访问看看,其他的js 调用, 其他平台的我没有试过,还不知道问题,欢迎指教!
http://www.yijianlan.com:8001/api/AreaOp/GetProvinceByName?name=省全称 --------> 查看某个省市的所有子集
http://www.yijianlan.com:8001/api/AreaOp/GetAllAreas --------------------------------------------> 获取全部区域(全国首位省市县)
http://www.yijianlan.com:8001/api/Cnblog/GetAllArtic -----------------------------------------------> 获取博客园数据(这是以前爬虫抓的有2年了吧),随机一百条
http://www.yijianlan.com:8001/api/Cnblog/GetArticByName?name=标题 ---------------------> 查询数据按标题