Asp.Net WebApi一個簡單的Token驗證
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
{
/// <summary>
/// 基礎Api控制器 所有的都繼承他
/// </summary>
public class BaseApiController : ApiController
{
/// <summary>
/// 構造函數賦值
/// </summary>
public BaseApiController()
{
TokenValue = HttpContext.Current.Session[LoginID] ?? "";
HttpContext.Current.Request.Headers.Add("TokenValue", TokenValue.ToString());
}
/// <summary>
/// 數據庫上下文
/// </summary>
public WYDBContext db = WYDBContextFactory.GetDbContext();
/// <summary>
/// token值 登錄后賦值請求api的時候添加到header中
/// </summary>
public static object TokenValue { get; set; } = "";
/// <summary>
/// 登錄者賬號
/// </summary>
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
{
/// <summary>
/// token驗證
/// </summary>
public class TokenCheckFilter: AuthorizeAttribute
{
/// <summary>
/// 重寫基類的驗證方式,加入自定義的Ticket驗證
/// </summary>
/// <param name="actionContext"></param>
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<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>();
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;
}
/// <summary>
/// 重寫HandleUnauthorizedRequest
/// </summary>
/// <param name="filterContext"></param>
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);
}
/// <summary>
/// 重寫Application_PostAuthorizeRequest
/// </summary>
protected void Application_PostAuthorizeRequest()
{
//對Session的支持,不然運行調用會直接異常
HttpContext.Current.SetSessionStateBehavior(System.Web.SessionState.SessionStateBehavior.Required);
}
}
5、現在來寫一個登陸
新建一個控制器LoginController繼承BaseApiController 里面寫一個登陸的方法Login 登陸頁面就直接在Home的index里面寫一個簡單的就行了這個控制器訪問就不受限制了加上注解
[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
{
/// <summary>
/// 區域查詢
/// </summary>
[TokenCheckFilter]
public class AreaOpController : BaseApiController
{
/// <summary>
/// 獲取全部區域
/// </summary>
/// <returns></returns>
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;
}
}
/// <summary>
/// 查詢某個省市直轄市自治區下所有的信息
/// </summary>
/// <param name="name">省名稱(全名)</param>
/// <returns></returns>
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;
}
}
/// <summary>
/// 遞歸獲取樹形數據
/// </summary>
/// <param name="areasDTOs"></param>
/// <param name="parentID"></param>
/// <returns></returns>
public IEnumerable<object> GetProvinceCity(IEnumerable<AddressAll> 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<Result>(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=標題 ---------------------> 查詢數據按標題







