开发提供数据的WebApi服务,最重要的是数据的安全性。那么对于我们来说,如何确保数据的安全是要思考的问题。
在ASP.NET WebService服务中可以通过SoapHead验证机制来实现,那么在ASP.NET WebApi中我们应该如何保证我们的接口的安全呢?
- 什么是JWT?
JSON Web Token(JWT)是一个开放标准,它定义了一种紧凑的,自包含的方式,用于作为JSON对象在各方之间安全地传输信息,该信息可以被验证和信任,因为它是数字签名的
简单理解就是一个字符串
- JWT长什么样子?
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串
- JWT构成
1、第一部分头部header
header典型的由两部分组成,token的类型("JWT")和算法名称(比如:HMAC HS256--哈希256)
例如
Base64({
"alg":"HS256"
"typ":"JWT"
})
然后用Base64对这个JSON编码就得到JWT的第一部分
2、第二部分载荷Payload--载荷就是存放有效信息的地方
Payload声明有3种类型
a、register:预定义的
b、public:公有的
c、private:私有的
2.1、标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub:jwt所面向的用户
aud: 接收jwt的一方
exp:jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2.2、公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
2.3、私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息
例如:
Base64({
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
})
对payload进行Base64编码就得到JWT的第二部分
3、第三部分签名Signature
为了得到签名部分,你必须有编码过的header,编码过的payload,一个秘钥,签名算法是header中指定的那个,然后对它们签名即可,例如:HMACSHA256(base64UrlEncode(header)+ "." + base64UrlEncode(payload),secret)。
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的签名算法配合secret进行签名,然后就构成了jwt的第三部分
将这三部分用.连接成一个完整的字符串,构成了最终的jwt
签名是用于验证消息在传递过程中有没有更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方
- 注意
secret密钥是保存在服务器端的,jwt的签发生成是在服务器端做的,jwt的验签也是在服务器端做的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了
- 如何应用
一般是在请求头里加入Authorization,并加上Bearer标注:
- 基于JWT实现Token签名认证基本思路如下
基本流程上是这样的:
1、用户使用用户名和密码来请求服务器
2、服务器进行验证用户的信息
3、服务器通过验证,发送给用户一个token
4、客户端存储token,并在每次请求时在request header附上这个token值,服务端并不存储token
5、服务器验证token值,验证成功后,则返回数据
- 实现原理图
1、POST/user/login with username and password
2、Create a JWT with a secret
3、Returns the JWT to the Brower
4、Sends the JWT on the Authorization Header
5、Check JWT signature Get user info from the JWT
6、Sends response to the client
服务器端做两件事情,1--签发token值,2--验证token值
JWT并不存储token
- 代码具体实现
jwt官网:https://jwt.io/
1、签发token
1.1、ApiTokenService用于验证用户名和密码通过后签发token
public class ApiTokenServiceController : ApiController { ////// 获取访问口令令牌 /// /// [HttpGet] [HttpPost] public ResponseResult GetToken(string appId, string appSecret) { var result = ResponseResult .Default(); if (string.IsNullOrEmpty(appId)) { return ResponseResult .Faild("appid不能为空!"); } if (string.IsNullOrEmpty(appSecret)) { return ResponseResult .Faild("appsecret不能为空!"); } try { //1、模拟从数据库中验证appId和appSecret if (appId.Equals("admin") && appSecret.Equals("123456")) { //2、登录成功后将token以jwt字符串的形式返回给客户端。 var payload = new Dictionary<string, object> { { "iat", JwtHelper.ConvertDateTimeInt(DateTime.Now) },//token创建时间,unix时间戳。unix { "exp", JwtHelper.ConvertDateTimeInt(DateTime.Now.AddMinutes(10)) },//expire 指定token的生命周期。unix { "AppId",appId }, { "AppSecret", appSecret }, }; LoginResponseResult entity = new LoginResponseResult { AppId = appId, AppSecret = appSecret, Token = JwtHelper.GenerateJwtEncode(payload), }; result = ResponseResult .Success(entity, "获取token成功!"); } else { result = ResponseResult .Faild("获取token失败!"); } } catch (System.Exception ex) { result = ResponseResult .Exception(ex.Message); } return result; } /// /// 刷新新的口令令牌 /// /// [HttpGet] [HttpPost] public ResponseResult RefreshToken() { string tokenParam = WebHelper.GetHeadersParamValue("token"); var result = ResponseResult .Default(); if (string.IsNullOrEmpty(tokenParam)) { return ResponseResult .Faild("token不能为空!"); } var tokenVaule = JwtHelper.GetJwtDecode(tokenParam); if (tokenVaule == null) { var resp = Request.CreateResponse (HttpStatusCode.OK, ResponseResult.NotAuthorization("token过期或无效!"), "application/json"); throw new HttpResponseException(resp); } try { string appId = tokenVaule["AppId"].ToString(); string appSecret = tokenVaule["AppSecret"].ToString(); //2、登录成功后将token以jwt字符串的形式返回给客户端。 //对象初始化器 var payload = new Dictionary<string, object> { { "iat", JwtHelper.ConvertDateTimeInt(DateTime.Now) },//token创建时间,unix时间戳。unix { "exp", JwtHelper.ConvertDateTimeInt(DateTime.Now.AddMinutes(10)) },//expire 指定token的生命周期。unix { "AppId",appId }, { "AppSecret",appSecret }, }; LoginResponseResult entity = new LoginResponseResult { AppId = appId, AppSecret = appSecret, Token = JwtHelp.GenerateJwtEncode(payload), }; result = ResponseResult .Success(entity, "口令刷新成功!"); } catch (System.Exception ex) { result = ResponseResult .Exception(ex.Message); } return result; } }
1.2、响应结果实体ResponseResult
public class ResponseResult { public ResponseResult() { } ////// 状态 /// public int RequestStatus { get; set; } /// /// 消息内容 /// public string Msg { get; set; } public static ResponseResult Default() { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Default; result.Msg = ""; return result; } public static ResponseResult Success(string message = "") { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Succeed; result.Msg = message; return result; } public static ResponseResult Exception(string message) { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Exception; result.Msg = message; return result; } public static ResponseResult Faild(string message) { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.Faild; result.Msg = message; return result; } public static ResponseResult NotAuthorization(string message) { var result = new ResponseResult(); result.RequestStatus = (int)ResponseResultStatus.NotAuthorization; result.Msg = message; return result; } } public class ResponseResult : ResponseResult where T : class, new() { public ResponseResult() { this.Data = new T(); } public T Data { get; set; } public static ResponseResult Default() { var result = new ResponseResult (); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.Default; result.Msg = ""; return result; } public static ResponseResult Success(T t, string message = "") { var result = new ResponseResult (); result.Data = t; result.RequestStatus = (int)ResponseResultStatus.Succeed; result.Msg = message; return result; } public static ResponseResult Exception(string message) { var result = new ResponseResult (); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.Exception; result.Msg = message; return result; } public static ResponseResult Faild(string message) { var result = new ResponseResult (); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.Faild; result.Msg = message; return result; } public static ResponseResult NotAuthorization(string message) { var result = new ResponseResult (); result.Data = default(T); result.RequestStatus = (int)ResponseResultStatus.NotAuthorization; result.Msg = message; return result; } } public enum ResponseResultStatus { Default = 0, Succeed = 100, Faild = 101, Exception = 102, NotAuthorization = 403 }
1.3、LoginResponseResult
public class LoginResponseResult { public string AppId { get; set; } public string AppSecret { get; set; } public string Token { get; set; } }
2、JwtHelper
通过nuget引用jwt组件,掌握JWT组件的使用,学会使用JWT组件生成token值,以及验证token值
public class JwtHelper { ////// jwt的secret千万不能泄密了,只能让服务端自己知道。jwt的secret用户服务端token的签发和验证。 /// private static string secret = "davidmengdavidmeng"; /// /// 生成JwtToken /// /// /// public static string GenerateJwtEncode(Dictionary<string, object> payload) { IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var token = encoder.Encode(payload, secret); return token; } /// /// 根据jwtToken获取payload /// /// jwtToken /// public static IDictionary<string, object> GetJwtDecode(string token) { return GetJwtDecode string, object>>(token); } public static T GetJwtDecode (string token) { try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); return decoder.DecodeToObject (token, secret, verify: true); } catch (TokenExpiredException) { Console.WriteLine("Token has expired"); } catch (SignatureVerificationException) { Console.WriteLine("Token has invalid signature"); } return default(T); } /// /// Unix时间戳转为C#格式时间 /// /// Unix时间戳格式,例如1482115779 /// C#格式时间 public static DateTime GetTime(string timeStamp) { DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); long lTime = long.Parse(timeStamp + "0000000"); TimeSpan toNow = new TimeSpan(lTime); return dtStart.Add(toNow); } /// /// DateTime时间格式转换为Unix时间戳格式 /// /// DateTime时间格式 /// Unix时间戳格式 public static int ConvertDateTimeInt(System.DateTime time) { System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1)); return (int)(time - startTime).TotalSeconds; } }
3、BaseApiTokenController
让业务接口继承自BaseApiTokenController,而BaseApiTokenController继承自ApiController,实现重写Initialize方法,在里面实现用JWT组件验证用户的JWT Token的有效性(用户token口令无效或者是否已经过期),这样每一个继承自BaseApiTokenController的业务接口都将验证token
public class BaseApiTokenController : ApiController { protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext) { base.Initialize(controllerContext); string tokenParam = WebHelper.GetHeadersParamValue("token"); #region 1、验证token、参数合法性 if (string.IsNullOrEmpty(tokenParam)) { var resp = Request.CreateResponse(HttpStatusCode.OK, ResponseResult.NotAuthorization("token参数不能为空!"), "application /json"); throw new HttpResponseException(resp); } #endregion #region 2、验证用户的JWT Token的有效性(用户token口令无效或者是否已经过期) var tokenVaule = JwtHelper.GetJwtDecode(tokenParam); if (tokenVaule == null) { var resp = Request.CreateResponse (HttpStatusCode.OK, ResponseResult.NotAuthorization("token过期或无效!"), "application/json"); throw new HttpResponseException(resp); } #endregion } }
订单服务OrderServiceController
public class OrderServiceController : BaseApiTokenController { [HttpGet] [HttpPost] public IEnumerable<string> GetOrder() { return new string[] { "ASP.NET WebApi 基于JWT实现Token签名认证" }; } }
4、界面
4.1、登录输入正确的用户名和密码
4.2、数据库验证用户名和密码通过,签发token
4.3、获取到token后,跳转到另外一个界面,单击获取订单按钮访问订单服务
请求时把jwt token,加入到header中
4.4、订单服务通过token验证,进入到订单服务