一、前言
webapi接口是開放給外部使用的,包括接口的地址,傳參的規范,還有返回結果的說明。正因為接口的開放性,使得接口的安全很重要。試想一下,用抓包工具(如fiddler),甚至瀏覽器獲取到接口的規范后(甚至可以猜到接口的其它規范),如果接口沒有做”安全“這一道防火牆,任何人都可以調用接口來獲取及提交數據,這真是太可怕了。
根據以往經驗,我們可以把資源(也就是一個接口)的權限分為三個等級:
1:公開可訪問
2:登錄用戶可訪問
3:有權限的登錄用戶可訪問
二、JWT
考慮http的無狀態性,且又必須讓服務器能區分每次的http請求是”誰“發出的,但又不想在http請求里攜帶很多信息(盡量每次的請求包比較小),我采用token技術。即將用戶的基本信息,如用戶id,用戶的角色等進行加密,並附在http請求頭里。服務器端只要對token進行解密后就能知道是誰發起的請求,JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准。該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。jwt參考如下網站:https://jwt.io/。
三、使用JWT實現身份驗證
微軟對webapi的安全拆分為authentication和authorization,authentication的職責是解決”用戶是誰“的問題,而authorization的職責是解決”是否有權限“的問題

2.在項目里新建文件夾Security,並創建IdentityBasicAuthentication類,繼承IAuthenticationFilter接口,代碼如下:
public class IdentityBasicAuthentication : IAuthenticationFilter { public bool AllowMultiple => throw new NotImplementedException(); public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { throw new NotImplementedException(); } public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { throw new NotImplementedException(); } }
3.WebapiConfig里面注冊IdentityBasicAuthentication
config.Filters.Add(new IdentityBasicAuthentication());
4.修改IdentityBasicAuthentication
public class IdentityBasicAuthentication : IAuthenticationFilter { public bool AllowMultiple { get; } /// <summary> /// 請求先經過AuthenticateAsync /// </summary> /// <param name="context"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { // 1、獲取token context.Request.Headers.TryGetValues("Authorization", out var tokenHeaders); // 2、如果沒有token,不做任何處理 if (tokenHeaders == null || !tokenHeaders.Any()) { return Task.FromResult(0); } // 3、如果token驗證通過,則寫入到identity,如果未通過則設置錯誤 var jwtHelper = new JWTHelper(); var payLoadClaims = jwtHelper.DecodeToObject(tokenHeaders.FirstOrDefault(), Config.JWTKey, out bool isValid, out string errMsg); if (isValid) { var identity = new ClaimsIdentity("jwt", "userName", "roles");//只要ClaimsIdentity設置了authenticationType,authenticated就為true,后面的authority根據authenticated=true來做權限 foreach (var keyValuePair in payLoadClaims) { identity.AddClaim(new Claim(keyValuePair.Key, keyValuePair.Value.ToString())); } // 最好是http上下文的principal和進程的currentPrincipal都設置 context.Principal = new ClaimsPrincipal(identity); Thread.CurrentPrincipal = new ClaimsPrincipal(identity); } else { context.ErrorResult = new ResponseMessageResult(Utils.toJson(HttpCode.SUCCESS, new { code = HttpCode.AuthenticationRequired, msg = errMsg })); } return Task.FromResult(0); } /// <summary> /// 請求后經過AuthenticateAsync /// </summary> /// <param name="context"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { return Task.FromResult(0); } }

public class JWTHelper { private IJsonSerializer _jsonSerializer; private IDateTimeProvider _dateTimeProvider; private IJwtValidator _jwtValidator; private IBase64UrlEncoder _base64UrlEncoder; private IJwtAlgorithm _jwtAlgorithm; private IJwtDecoder _jwtDecoder; private IJwtEncoder _jwtEncoder; public JWTHelper() { //非fluent寫法 this._jsonSerializer = new JsonNetSerializer(); this._dateTimeProvider = new UtcDateTimeProvider(); this._jwtValidator = new JwtValidator(_jsonSerializer, _dateTimeProvider); this._base64UrlEncoder = new JwtBase64UrlEncoder(); this._jwtAlgorithm = new HMACSHA256Algorithm(); this._jwtDecoder = new JwtDecoder(_jsonSerializer, _jwtValidator, _base64UrlEncoder); this._jwtEncoder = new JwtEncoder(_jwtAlgorithm, _jsonSerializer, _base64UrlEncoder); } public string Decode(string token, string key, out bool isValid, out string errMsg) { isValid = false; var result = string.Empty; try { result = _jwtDecoder.Decode(token, key, true); isValid = true; errMsg = "正確的token"; return result; } catch (TokenExpiredException) { errMsg = "token過期"; return result; } catch (SignatureVerificationException) { errMsg = "簽名無效"; return result; } catch (Exception) { errMsg = "token無效"; return result; } } public T DecodeToObject<T>(string token, string key, out bool isValid, out string errMsg) { isValid = false; try { var result = _jwtDecoder.DecodeToObject<T>(token, key, true); isValid = true; errMsg = "正確的token"; return result; } catch (TokenExpiredException) { errMsg = "token過期"; return default(T); } catch (SignatureVerificationException) { errMsg = "簽名無效"; return default(T); } catch (Exception) { errMsg = "token無效"; return default(T); } } public IDictionary<string, object> DecodeToObject(string token, string key, out bool isValid, out string errMsg) { isValid = false; try { var result = _jwtDecoder.DecodeToObject(token, key, true); isValid = true; errMsg = "正確的token"; return result; } catch (TokenExpiredException) { errMsg = "token過期"; return null; } catch (SignatureVerificationException) { errMsg = "簽名無效"; return null; } catch (Exception) { errMsg = "token無效"; return null; } } #region 解密 public string Encode(Dictionary<string, object> payload, string key, int expiredMinute = 30) { if (!payload.ContainsKey("exp")) { var exp = Math.Round((_dateTimeProvider.GetNow().AddMinutes(expiredMinute) - new DateTime(1970, 1, 1)).TotalSeconds); payload.Add("exp", exp); } return _jwtEncoder.Encode(payload, key); } #endregion }
6.新建控制器UserController.cs測試
[HttpPost] [Route("login")] public IHttpActionResult Login([FromBody] User user) { if (user != null) { //如果是張三 if (user.Name.ToString() == "張三") { Dictionary<string, object> dic = new Dictionary<string, object>(); dic.Add("userID", "1"); dic.Add("userName", "zhangsan"); dic.Add("roles","admin"); var token = new JWTHelper().Encode(dic, "abcdefg", 30);//dic:個人信息 abcdefg:key 30:過期時間 return Json(new MessageModel() { code = (int)HttpCode.OK, msg = "OK", data = token }); } else { return Json(new MessageModel() { code = (int)HttpCode.AuthenticationRequired, msg = "登錄失敗" }); } } else { return Json(new MessageModel() { code = (int)HttpCode.NULL_PARAM, msg = "參數為空" }); } } [Route("getuser"), HttpGet] public IHttpActionResult GetUser() { var user = (ClaimsPrincipal)User; var dic = new Dictionary<string, object>(); foreach (var userClaim in user.Claims) { dic.Add(userClaim.Type, userClaim.Value); } return Json(new MessageModel() { code = (int)HttpCode.OK, msg = "OK", data = dic }); } }
7.測試結果
過段時間再請求會提示token過期了
三、基於角色的權限控制
如果只是想簡單的實現基於角色的權限管理,我們基本上不用寫代碼,微軟已經提供了authorize特性,直接用就行。
1.User控制器新建2個請求:
/// <summary> /// 只有某種角色的用戶才有權限訪問 /// </summary> /// <returns></returns> [Route("onlyadmin"), HttpGet] [Authorize(Roles="admin")] public IHttpActionResult OnlyAdmin() { return Ok("僅管理員能訪問的請求"); } /// <summary> /// 只有某個用戶才有權限訪問 /// </summary> /// <returns></returns> [Route("onlyuser"), HttpGet] [Authorize(Users =("zhangsan"))] public IHttpActionResult OnlyUser() { return Ok("僅張三能訪問的請求"); }
2.修改張三的權限,啟動項目獲取token
訪問onlyadmin會提示拒絕授權
而訪問onlyuser是可以的
3.修改zhangsan的權限為admin,onlyuser改為lisi可以訪問
重啟項目,獲取token並請求onlyadmin和onlyuser
三、自定義權限控制
1.微軟提供的默認authorize特性在小項目和中型的對權限控制沒有復雜要求的項目里已經夠用了,但是並不能滿足所有的需求,比如我一個用戶可能不止一個角色,而如果用默認的authorize,會顯示未授權。
2. 如果要實現更加可控的基於角色的權限控制,只有自己寫Authorize filter。新建RoleAuthorizeAttribute繼承AuthorizeAttribute,並重寫IsAuthorized方法,代碼如下
public class RoleAuthorizeAttribute : AuthorizeAttribute { public string Roles { set; get; } protected override bool IsAuthorized(HttpActionContext actionContext) { IPrincipal principal = actionContext.ControllerContext.RequestContext.Principal; //首先principal不能為空,且principal.Identity是已經通過身份驗證的(即Identity.IsAuthenticated==true) //然后驗證接口權限是否在角色里 return (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated&&Role_isValid(actionContext)); } protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) { actionContext.Response = Utils.toJson(HttpStatusCode.OK, new { code = HttpStatusCode.Unauthorized, msg = "未授權" }); } /// <summary> /// 驗證用戶角色 /// </summary> /// <param name="actionContext"></param> /// <returns></returns> protected bool Role_isValid(HttpActionContext actionContext) { if (Roles!=null) { var authorization = actionContext.Request.Headers.Authorization; JWTHelper jwt = new JWTHelper(); bool isValid = false; var userinfo = jwt.Decode(authorization.Scheme, Config.JWTKey, out isValid, out string errMsg); if (isValid) { //token轉json字符串再轉數組 Dictionary<string, string> userDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(userinfo); var user_roles = userDict["roles"].Split(','); var need_roles= Roles.Split(','); //判斷角色是否在Roles里 foreach (var role in user_roles) { if (need_roles.Contains(role)) { return true; } } return false; } else { return false; } } else { return true; } } }
3.控制器新建一個請求,請求token並驗證接口
[Route("custom"), HttpGet] [RoleAuthorize(Roles = ("admin,user"))] public IHttpActionResult CustomRole() { return Ok("ok"); }