一、前言
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"); }