.NET WebAPI 用ActionFilterAttribute實現token令牌驗證與對Action的權限控制
項目背景是一個社區類的APP(求輕吐...),博主主要負責后台業務及接口。以前沒玩過webAPI,但是領導要求必須用這個(具體原因鬼知道),只好硬着頭皮上了。
最近剛做完權限這一塊,分享出來給大家。歡迎各種吐槽批判踐踏...
先說說用戶身份的識別,簡單的做了一個token機制。用戶登錄,后台產生令牌,發放令牌,用戶攜帶令牌訪問...
1.cache管理類,由於博主使用的HttpRuntime.Cache來存儲token,IIS重啟或者意外關閉等情況會造成cache清空,只好在數據庫做了cache的備份,在cache為空的時候查詢數據庫是否有cache數據,有則是cache被意外清空,需要重新放在cache中。
/// <summary>
/// 緩存管理
/// 將令牌、用戶憑證以及過期時間的關系數據存放於Cache中
/// </summary>
public class CacheManager
{
private static readonly ITempCacheService tempCacheService = ServiceLocator.Instance.GetService<ITempCacheService>();
/// <summary>
/// 初始化緩存數據結構
/// </summary>
/// token 令牌
/// uuid 用戶ID憑證
/// userType 用戶類別
/// timeout 過期時間
/// <remarks>
/// </remarks>
private static void CacheInit()
{
if (HttpRuntime.Cache["PASSPORT.TOKEN"] == null)
{
DataTable dt = new DataTable();
dt.Columns.Add("token", Type.GetType("System.String"));
dt.Columns["token"].Unique = true;
dt.Columns.Add("uuid", Type.GetType("System.Object"));
dt.Columns["uuid"].DefaultValue = null;
dt.Columns.Add("userType", Type.GetType("System.String"));
dt.Columns["userType"].DefaultValue = null;
dt.Columns.Add("timeout", Type.GetType("System.DateTime"));
dt.Columns["timeout"].DefaultValue = DateTime.Now.AddDays(7);
DataColumn[] keys = new DataColumn[1];
keys[0] = dt.Columns["token"];
dt.PrimaryKey = keys;
var tempCaches = tempCacheService.GetAllCaches();
if (tempCaches.Any())
{
foreach (var tempCacheDTOShow in tempCaches)
{
DataRow dr = dt.NewRow();
dr["token"] = tempCacheDTOShow.UserToken;
dr["uuid"] = tempCacheDTOShow.UserAccountId;
dr["userType"] = tempCacheDTOShow.UserType.ToString();
dr["timeout"] = tempCacheDTOShow.EndTime;
dt.Rows.Add(dr);
}
}
//Cache的過期時間為 令牌過期時間*2
HttpRuntime.Cache.Insert("PASSPORT.TOKEN", dt, null, DateTime.MaxValue, TimeSpan.FromDays(7 * 2));
}
}
/// <summary>
/// 獲取用戶UUID標識
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static Guid GetUUID(string token)
{
CacheInit();
DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"];
DataRow[] dr = dt.Select("token = '" + token + "'");
if (dr.Length > 0)
{
return new Guid(dr[0]["uuid"].ToString());
}
return Guid.Empty;
}
/// <summary>
/// 獲取用戶類別(分為員工、企業、客服、管理員等,后期做權限驗證使用)
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static string GetUserType(string token)
{
CacheInit();
DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"];
DataRow[] dr = dt.Select("token = '" + token + "'");
if (dr.Length > 0)
{
return dr[0]["userType"].ToString();
}
return null;
}
/// <summary>
/// 判斷令牌是否存在
/// </summary>
/// <param name="token">令牌</param>
/// <returns></returns>
public static bool TokenIsExist(string token)
{
CacheInit();
DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"];
DataRow[] dr = dt.Select("token = '" + token + "'");
if (dr.Length > 0)
{
var timeout = DateTime.Parse(dr[0]["timeout"].ToString());
if (timeout > DateTime.Now)
{
return true;
}
else
{
RemoveToken(token);
return false;
}
}
return false;
}
/// <summary>
/// 移除某令牌
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static bool RemoveToken(string token)
{
CacheInit();
DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"];
DataRow[] dr = dt.Select("token = '" + token + "'");
if (dr.Length > 0)
{
dt.Rows.Remove(dr[0]);
}
return true;
}
/// <summary>
/// 更新令牌過期時間
/// </summary>
/// <param name="token">令牌</param>
/// <param name="time">過期時間</param>
public static void TokenTimeUpdate(string token, DateTime time)
{
CacheInit();
DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"];
DataRow[] dr = dt.Select("token = '" + token + "'");
if (dr.Length > 0)
{
dr[0]["timeout"] = time;
}
}
/// <summary>
/// 添加令牌
/// </summary>
/// <param name="token">令牌</param>
/// <param name="uuid">用戶ID憑證</param>
/// <param name="userType">用戶類別</param>
/// <param name="timeout">過期時間</param>
public static void TokenInsert(string token, object uuid, string userType, DateTime timeout)
{
CacheInit();
// token不存在則添加
if (!TokenIsExist(token))
{
DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"];
DataRow dr = dt.NewRow();
dr["token"] = token;
dr["uuid"] = uuid;
dr["userType"] = userType;
dr["timeout"] = timeout;
dt.Rows.Add(dr);
HttpRuntime.Cache["PASSPORT.TOKEN"] = dt;
tempCacheService.Add_TempCaches(new List<TempCacheDTO_ADD>()
{
new TempCacheDTO_ADD()
{
EndTime = timeout,
UserAccountId = new Guid(uuid.ToString()),
UserToken = new Guid(token),
UserType = (UserType)Enum.Parse(typeof(UserType),userType)
}
});
}
// token存在則更新過期時間
else
{
TokenTimeUpdate(token, timeout);
tempCacheService.Update_TempCaches(new Guid(token), timeout);
}
}
}
2.接下來就是對用戶攜帶的token進行驗證了,通過繼承ActionFilterAttribute來實現,在這里還需要考慮到匿名訪問API,對於部分API,是允許匿名訪問(不登錄訪問)的。所以,先寫一個代表匿名的Attribute:
/// <summary>
/// 匿名訪問標記
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AnonymousAttribute : Attribute
{
}
然后給允許匿名訪問的Action打上[Anonymous]標簽就OK,再來看我們的token驗證代碼:
/// <summary>
/// 用戶令牌驗證/// </summary>
public class TokenProjectorAttribute : ActionFilterAttribute
{
private const string UserToken = "token";
private readonly IAccountInfoService accountInfoService = ServiceLocator.Instance.GetService<IAccountInfoService>();
private readonly ITempCacheService tempCacheService = ServiceLocator.Instance.GetService<ITempCacheService>();
public override void OnActionExecuting(HttpActionContext actionContext)
{
// 匿名訪問驗證
var anonymousAction = actionContext.ActionDescriptor.GetCustomAttributes<AnonymousAttribute>();
if (!anonymousAction.Any())
{
// 驗證token
var token = TokenVerification(actionContext);
}
base.OnActionExecuting(actionContext);
}
/// <summary>
/// 身份令牌驗證
/// </summary>
/// <param name="actionContext"></param>
protected virtual string TokenVerification(HttpActionContext actionContext)
{
// 獲取token
var token = GetToken(actionContext.ActionArguments, actionContext.Request.Method);
// 判斷token是否有效
if (!CacheManager.TokenIsExist(token))
{
throw new UserLoginException("Token已失效,請重新登陸!");
}
// 判斷用戶是否被凍結
if (accountInfoService.Exist_User_IsForzen(AccountHelper.GetUUID(token)))
{
CacheManager.RemoveToken(token);
tempCacheService.Delete_OneTempCaches(new Guid(token));
throw new UserLoginException("此用戶已被凍結,請聯系客服!");
}
return token;
}
private string GetToken(Dictionary<string, object> actionArguments, HttpMethod type)
{
var token = "";
if (type == HttpMethod.Post)
{
foreach (var value in actionArguments.Values)
{
token = value.GetType().GetProperty(UserToken) == null
? GetToken(actionArguments, HttpMethod.Get)
: value.GetType().GetProperty(UserToken).GetValue(value).ToString();
}
}
else if (type == HttpMethod.Get)
{
if (!actionArguments.ContainsKey(UserToken))
{
throw new Exception("未附帶token!");
}
if (actionArguments[UserToken] != null)
{
token = actionArguments[UserToken].ToString();
}
else
{
throw new Exception("token不能為空!");
}
}
else
{
throw new Exception("暫未開放其它訪問方式!");
}
return token;
}
}
這里對GetToken方法做一下解釋:
1.博主只做了POST與GET方法的驗證,其他請求未使用也就沒做,歡迎大家補充
2.POST方式里面的回調是解決POST請求接口只有一個簡單參數的情況,例如下面的接口:
/// <summary>
/// 手動向新用戶推送短信
/// </summary>
/// <returns></returns>
[HttpPost]
[Route("api/Common/PushNewUserSMS")]public PushNewWorkSMSResult PushNewUserSMS([FromBody]string token)
{
sendMessagesService.PushNewUserSMS();
return new PushNewWorkSMSResult() { Code = 0 };
}
當然,POST方式一般都會把參數寫進一個類里,對於一個參數的情況,博主不喜歡那么干。這么寫,需要AJAX提交時空變量名才能獲取:
// 推送新用戶營銷短信
function pushNewUserSMS() {
$(".tuiguang").unbind("click");
// 注意下面的參數為空“”
$.post(Config.Api.Common.PushNewUserSMS, { "": $.cookie("MPCBtoken") }, function (data) {
if (data.Code == 0) {
alert("發送成功!");
_initDatas();
} else {
Config.Method.JudgeCode(data, 1);
}
});
}
這樣,我們就只需要在每個controller上打上[TokenProjector]標簽,再在允許匿名的Action上打上[Anonymous]標簽就能輕松的搞定token驗證了。
3.除了token驗證外呢,我門還想對Action進行用戶角色的控制,比如一個獲取登錄用戶錢包余額的Action(A),肯定只有員工、企業才能訪問,管理員、客服沒有錢包,所以不允許訪問,從業務上應該是去訪問另外一個獲取指定用戶錢包余額的Action(B),當然這個Action又不能對員工、企業開放權限。這就涉及到需要實現一個控制Action訪問權限的功能,上面我們對用戶的token進行了驗證,那么拿到token就拿到了用戶基本信息(包括角色),那就只需要做一個對Action的權限標注就能解決問題了,我們先寫一個代表權限控制的Attribute:
/// <summary>
/// 權限控制標記
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class ModuleAuthorizationAttribute : Attribute
{
public ModuleAuthorizationAttribute(params string[] authorization)
{
this.Authorizations = authorization;
}
/// <summary>
/// 允許訪問角色
/// </summary>
public string[] Authorizations { get; set; }
}
在每個需要權限控制的Action上打上[ModuleAuthorization]標簽,並注明訪問角色:
/// <summary>
/// 獲取錢包余額
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
[HttpGet]
[Route("api/Account/GetWalletBalance")]
[ModuleAuthorization(new[] { "Staff", "Enterprise" })]
public GetWalletBalanceResult GetWalletBalance(string token)
{
var result = this.walletService.Get_Wallet_Balance(AccountHelper.GetUUID(token));
return new GetWalletBalanceResult()
{
Code = 0,
Balance = result
};
}
/// <summary>
/// 管理員
/// 處理提現申請
/// </summary>
/// <param name="handleTempWithdrawalsModel"></param>
/// <returns></returns>
[HttpPost]
[Route("api/Account/HandleTempWithdrawals")]
[ModuleAuthorization(new[] { "PlatformCustomer" })]
public HandleTempWithdrawalsResult HandleTempWithdrawals(
[FromBody] HandleTempWithdrawalsModel handleTempWithdrawalsModel)
{
walletService.Handle_TempWithdrawals(AccountHelper.GetUUID(handleTempWithdrawalsModel.token),
handleTempWithdrawalsModel.message, handleTempWithdrawalsModel.tempID,
handleTempWithdrawalsModel.isSuccess);
return new HandleTempWithdrawalsResult() { Code = 0 };
}
然后我們修改TokenProjectorAttribute這個類,在驗證token后做權限驗證,權限驗證方法如下:
/// <summary>
/// Action 訪問權限驗證
/// </summary>
/// <param name="token">身份令牌</param>
/// <param name="actionContext"></param>
/// <returns></returns>
protected virtual void AuthorizeCore(string token, HttpActionContext actionContext)
{
// 權限控制Action驗證
var moduleAuthorizationAction = actionContext.ActionDescriptor.GetCustomAttributes<ModuleAuthorizationAttribute>();
if (moduleAuthorizationAction.Any())
{
var userRole = AccountHelper.GetUserType(token);
if (!moduleAuthorizationAction[0].Authorizations.Contains(userRole.ToString()))
{
throw new Exception("用戶非法跨權限訪問,token:" + token);
}
}
}
OK,終於實現了webAPI對用戶令牌與Action權限的驗證。
當然,博主也是剛接觸webAPI,再者業務需求較簡單,如有不對之處,歡迎大家指出,必定虛心求教。

