前言
如果你的接口是开放的,谁都可以成功调用,那么会非常危险。因此除非你真的想做开放式服务,否则要对用户的请求做权限控制
举例:假如我想自己写一个“张三版新浪微博”的APP。
新浪微博开放了微博的接口,所有人可以调用这些接口“发微博”、“看微博”等。当然不是随便调用,而是要到新浪微博的开放平台后台申请一个AppKey(或者AppId),申请成功后,新浪微博就会分配一个AppKey和一个App、AppSecret给你。这个AppKey就是供“张三版新浪微博”这个App用的,新浪微博的用户也可以用同样的用户名密码登录“张山版新浪微博”这个App发微博、看微博,和官方版本的新浪微博是互通的。
无论是认证还是授权,都需要传递认证信息,对于认证来讲就是AppKey、AppSecret,对于授权来讲就是用户的账号、密码。
接口传输这些认证信息方法有很多,常用的有:
1> 每次请求,直接把“用户名/AppKey”、“密码/AppSecret”通过表单、QueryString或者报文头传递;这种安全性比较差,因为随时可以被截获。
2> 首次先使用“用户名/AppKey”、“密码/AppSecret”获取Access_Token(相当于SessionId,在服务器端生成guid,用guid做key,用用户名做value保存到redis、memcached等地方),以后的请求都带着Access_Token,Access_Token存在有效期,过时后要重新获取Access_Token。缺点是Access_Token有过期重新登录的问题,而移动端app经常需要一段时间不用打开还要能直接用。需要有一个类似于Session的中心回话服务器。WebAPI也可以使用asp.net 的Session,但是不建议使用。
3> 登录时,服务器端把“用户名/AppKey”、“密码/AppSecret”采用JWT等的加密后返回给客户端,客户端以后发送请求的时候把JWT加密的内容放到请求中,服务端再解密获取用户名。优点是不需要会话状态服务器,有利于分布式部署,还有可以避免Session的过期问题,可以一直能用;缺点是一旦加密解密算法泄露,会带来安全性问题。(推荐)
为什么使用JWT(JSON Web Tokens)
在传统的基于session的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会创建一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。
JWT 是一种无状态的分布式的身份验证方式,与 Session 相反,Jwt 将用户信息存放在 Token 的 payload 字段保存在客户端,通过 RSA 加密的方式,保证数据不会被篡改,验证数据有效性。
我们需要做的第一件事就是让客户端通过他们的账号密码交换token。这里有2种可能的方法在RESTful API里面。第一种是使用POST请求来通过验证,使服务端发送带有token的响应。除此之外,你可以使用GET请求,这需要他们使用参数提供凭证(指URL),或者更好的使用请求头。
JWT(Json Web Token)是现在流行的一种对Restful接口进行验证的机制的基础。JWT的特点:把用户信息放到一个JWT字符串中,用户信息部分是明文的,再加上一部分签名区域,签名部分是服务器对于“明文部分+秘钥”加密的,这个加密信息只有服务器端才能解析。用户端只是存储、转发这个JWT字符串。如果客户端篡改了明文部分,那么服务器端解密时候会报错。
JWT由三块组成,可以把用户名、用户Id等保存到Payload部分
头:Header
JWT第一部分是header,header主要包含两个部分,alg指加密类型,可选值为HS256
、RSA
等等,typ=JWT
为固定值,表示token的类型。
{
"typ": "JWT",
"alg": "HS256"
}
载体:Payload
JWT第二部分是Payload,Payload 部分也是一个 JSON 对象,它是token的详细内容,用来存放实际需要传递的数据,一般包括:iss
(发行者 即:JWT签发者),exp
(过期时间 即:JWT的过期时间,这个过期时间必须要大于签发时间), sub
(用户信息)aud
(接收者 即:接收JWT的一方),iat
(签发时间 即:JWT的签发时间)
nbf (生效时间 即:定义在什么时间之前,该jwt都是不可用的)
jti (编号 即:JWT的唯一身份标识【值一般是GUID】,主要用来作为一次性token,从而回避重放攻击:具体做法就是用户请求一次,然后我们获取这个jti的值,然后给这个值加入黑名单【在Redis中设定一个黑名单表】用户第二次用这个Token来请求,我们在得到这个jti的值,去黑名单中查询下是否有这个值,如果有值则表示是重放请求)
以及其他信息,详细介绍请参考官网,也可以包含自定义字段。JWT 规定了以上7个官方字段,供选用
例如:我们可以自定义一个Payload
{
"sub": "招商银行",
"name": "张三",
"admin": true,
"jti": "2ab2dc85-0589-4174-b86e-b23dc9ae82a0",
"iat": 1542037098, //签发日期的时间戳
"exp": 1542040698 //过期日期的时间戳
}
签名:Signature
JWT第三部分是Signature,Signature 部分是对前两部分的签名,防止数据篡改。
这部分的内容是这样计算得来的:
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
Token案例
using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using Microsoft.AspNetCore.Mvc;
namespace JwtApp.Controllers
{
[Route("api/[controller]")]
public class HomeController : Controller
{
//创建Token
[Route(nameof(CreateToken))]
public IActionResult CreateToken()
{
var jti = Guid.NewGuid();
var exp = (DateTime.UtcNow.AddSeconds(100) - new DateTime(1970, 1, 1)).TotalSeconds;
var payload = new Dictionary<string, object>
{
{ "sub", "招商银行" },
{ "userName", "张三" },
{ "admin",true},
{ "jti", jti},
{ "exp",exp}
};
var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//秘钥:不要泄露
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 Content(token);
}
//解密Token
[Route(nameof(DecryptToken))]
public IActionResult DecryptToken(string token)
{
var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//秘钥:不要泄露
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);
var json = decoder.Decode(token, secret, verify: true);
return Content(json);
}
catch (FormatException)
{
return Content("Token格式无效");
}
catch (TokenExpiredException)
{
return Content("Token已经过期");
}
catch (SignatureVerificationException)
{
return Content("Token无效");
}
}
}
}
安装JWT:如果你的环境是.NET FrameWork4.5.2 这建议使用
Install-Package JWT -Version 3.0.1 这个版本
但是也有疑问,客户端在拿到服务器签发的 JWT 后,如果客户端被拦截,Head 里面的 JWT 不修改,篡改 Body 里面的数据,服务端也会通过。
如果保证这点呢?
WT 是方便授权的,只要保证不能被伪造就行了。防劫持那是另外的问题。防中间人劫持可以使用 https。防客户端劫持可以缩短有效时间,可以使用加密硬件,这个需要分情况。支付宝类就可以把时间限制的很短,比如一次交易时间内。v2ex 这种就随便了,设置个一年都没问题。