我们使用jsonwebtoken来生成和验证token,然后手写一个登录和授权的逻辑。使用koa-jwt中间件。
参考文章:JSON Web Token 入门教程
JWT原理
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
JWT 的三个部分
使用JWT使用token作为验证信息,比如下面这个就是一个token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZjMwMDE4OTg1NzcxYTUwNjQwOWU1MjQiLCJuYW1lIjoid3d3IiwiaWF0IjoxNTk2OTgxNjc4LCJleHAiOjE1OTcwNjgwNzh9.3wM-YYxZ2ccAzCwvC4jUVQUSkaeMRuEjuAdZocQAyWA
这个长的字符串由两个小数点分割成三个部分
- Header(头部)
- Payload(负载)
- Signature(签名)
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段供选用。Payload就是有效载荷,传递的主要内容放置在这里。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,任何放在token里面的信息,如果被截获了,对任何人别人是可读的。因此,我们不应该在Payload里面存放任何黑客可以利用的用户信息。但是可以使用RSA加密算法得到加密。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.
)分隔,就可以返回给用户。
验证过程
- 用户向认证服务器提交用户名和密码,认证服务器也可以和应用服务器部署在一起,但往往是独立的居多;
- 认证服务器校验用户名和密码组合,然后创建一个JWT token,token的Payload里面包含用户的身份信息,以及过期时间戳;
- 认证服务器使用密钥对Header和Payload进行签名,然后发送给客户浏览器;
- 浏览器获取到经过签名的JWT token,然后在之后的每个HTTP请求中附带着发送给应用服务器。经过签名的JWT就像一个临时的用户凭证,代替了用户名和密码组合,之后都是JWT token和应用服务器打交道了;
- 应用服务器检查JWT签名,确认Payload确实是由密钥拥有者签过名的;
- Payload身份信息代表了某个用户;
- 只有认证服务器拥有私钥,并且认证服务器只把token发给提供了正确密码的用户;
- 因此应用服务器可以认为这个token是由认证服务器颁发的也是安全的,因为该用户具有了正确的密码;
- 应用服务器继续完成HTTP请求,并认为这些请求确实属于这个用户;
这样的话,黑客假扮合法用户的办法要么是盗到了用户名和密码组合,要么盗到了认证服务器上的签名私钥。
操作步骤
实现登录
写一个登录逻辑,首先验证传进来的数据类型是否正确,然后在数据库中查询这个用户。如果找到了就把user中的id和name取出。使用jsonwebtoken模块加密。secret是提前保存的密钥,就是一个string。
async login(ctx) {
ctx.verifyParams({
name: {type: `string`, required: true}, //默认require也是true
password: {type: `string`, required: true},
})
const user = await User.findOne(ctx.request.body);
if (!user) {
ctx.throw(401, "账号密码不正确");
}
const {_id, name} = user;
const token = jsonwebtoken.sign({_id, name}, secret, {expiresIn: `1d`});//有效时间1d
ctx.body = {token};
}
这样登陆成功就会返回一个token
账号密码不正确,也会报错。
实现授权
首先写一个授权中间件,目的是当请求某个特定的操作时,比如delete和update。需要识别这个用户,并将验证信息解码保存到ctx.state.user
const auth=async (ctx,next)=>{
const {authorization=''}=ctx.request.header;
const token=authorization.replace("Bearer ",'');//token默认有一个Bearer头
try {
const user=jsonwebtoken.verify(token,secret);
ctx.state.user=user; //约定
}catch (err){
ctx.throw(401,err.message);//401验证不符合
}
await next();
}
然后写一个验证中间件,上一个中间件会把用户请求附带的token解码成用户信息,验证中间件可以根据这个信息判断用户是不是有这个权限来进行操作。
async checkOwner(ctx, next) {
if (ctx.params.id != ctx.state.users._id) {
ctx.throw(4.3, "没有权限");
}
await next();
}
这样在进行敏感操作时,就可以保证用户只能修改自己的信息了
router.patch(`/:id`,auth,checkOwner,update) //put是整体替换
router.delete(`/:id`,auth,checkOwner,deleteByid)
使用koa-jwt中间件
koa-jwt中间件为我们写好了auth逻辑,只需要简单传递secret即可
const jwt=require("koa-jwt");
const auth=jwt({secret});
效果和
const auth=async (ctx,next)=>{
const {authorization=''}=ctx.request.header;
const token=authorization.replace("Bearer ",'');//token默认有一个Bearer头
try {
const user=jsonwebtoken.verify(token,secret);
ctx.state.user=user; //约定
}catch (err){
ctx.throw(401,err.message);//401验证不符合
}
await next();
}
一样,其它不需要任何改动。
使用postman验证接口
postman如果手动输入token很麻烦,这里写了一段程序可以方便的帮助我们添加token。
var jsonData=pm.response.json();
pm.globals.set("token",jsonData.token);
在需要输入token的地方,使用这个全局变量即可