有一些api是非常敏感的,比如获取用户个人信息,查看所有用户列表,修改密码等。如果不对这些api进行保护,那么别人就可以很容易地获取并调用这些 api 进行操作。
为什么前端做权限控制是不够的呢?因为前端的路由控制仅仅是视觉上的控制,前端可以隐藏某个页面或者某个按钮,但是发送请求的方式还是有很多,完全可以跳过操作页面来发送某个请求。所以就算前端的权限控制做的非常严密,后台依旧需要验证每个接口。
前端的权限控制主要有三种:路由控制(路由的跳转)、视图控制(按钮级别)和请求控制(请求拦截器)。这几种方式之后再详谈,前端做完权限控制,在调用之前,我们在服务端必须先对操作者进行“身份认证”,这就是所谓的鉴权。
JWT的组成部分
在该网站JWT,可以解码或编码一个JWT。一个JWT形如:
1、它由三部分组成:Header(头部)、Payload(负载)、Signature(签名)。 Header部分是一个JSON对象,描述JWT的元数据。一般描述信息为该Token的加密算法以及Token的类型。{"alg": "HS256","typ": "JWT"}的意思就是,该token使用HS256加密,token类型是JWT。这个部分基本相当于明文,它将这个JSON对象做了一个Base64转码,变成一个字符串。Base64编码解码是有算法的,解码过程是可逆的。头部信息默认携带着两个字段。
2、Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。有7个官方字段,还可以在这个部分定义私有字段。一般存放用户名、用户身份以及一些JWT的描述字段。它也只是做了一个Base64编码,因此肯定不能在其中存放秘密信息,比如说登录密码之类的。
3、Signature是对前面两个部分的签名,防止数据篡改,如果前面两段信息被人修改了发送给服务器端,此时服务器端是可利用签名来验证信息的正确性的。签名需要密钥,密钥是服务器端保存的,用户不知道。算出签名以后,把 Header、Payload、Signature
三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT的特点
1、JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
2、JWT 不加密的情况下,不能将秘密数据写入 JWT。
3、JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
4、JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
5、JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
6、为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
认证的流程大体如下:
首先用户登录的接口是不用token认证的,因为这个接口本身就是token的产生来源。前端输入用户名和密码后请求服务器登录接口,服务器验证用户名密码正确后,生成token并返回给前端,前端存储token,并在后面的请求中把token带在请求头中传给服务器,服务器验证token有效,才可以进行下一步操作。
一、服务器生成token
由于我们的服务端使用 Koa2 框架进行开发,除了要使用到 jsonwebtoken 库之外,还要使用一个 koa-jwt 中间件,该中间件针对 Koa 对 jsonwebtoken 进行了封装,使用起来更加方便。
const router = require('koa-router')();
const jwt = require('jsonwebtoken');
const userModel = require('../models/userModel.js');
//用来签名的密钥
const secret = "it's a secret";
router.post('/login', async (ctx) => {
const data = ctx.request.body;
const result = await userModel.findOne({
name: data.name,
password: data.password
})
if(result !== null){
//生成 token 返回给客户端
const token = jwt.sign({
name: result.name,
_id: result._id
}, secret, { expiresIn: '2h' });
return ctx.body = {
code: 200,
token: token,
msg: '登录成功'
}
}else{
return ctx.body = {
code: 400,
token: null,
msg: '用户名或密码错误'
}
}
});
module.exports = router;
二、前端获取token
接下来就是前端获取 token,这里是在 vue.js 中使用 axios 进行请求,请求成功之后拿到 token 保存到 localStorage 中。
submit(){
axios.post('/login', {
name: this.username,
password: this.password
}).then(res => {
if(res.code === 200){
localStorage.setItem('token', res.data.token);
}else{
this.$message('登录失败')
}
})
}
然后前端在请求后端api时,就把 token 带在请求头中传给服务器进行验证。每次请求都要获取 localStorage 中的 token,这样很麻烦,这里使用了 axios 的请求拦截器,进行全局设置,对每次请求都进行了取 token 放到 headers 中的操作。
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
config.headers.common['Authorization'] = 'Bearer ' + token;
return config;
})
三、服务器验证token
const koa = require('koa');
const koajwt = require('koa-jwt');
const app = new koa();
const secret = "it's a secret";
app.use(koajwt({
secret
}).unless({
path: [/\/register/, /\/login/]
}));
分析koa-jwt源码
我们在node_mudules里面找到koa-jwt/lib/resolvers文件夹下的auth-header.js文件,看下koa-jwt做了些什么
(可以看到它是先判断请求头中是否带了 authorization,如果有,则通过正则将 token 从 authorization 中分离出来,这里我们也看到了Bearer这个单词。如果没有 authorization,则代表了客户端没有传 token 到服务器,这时候就抛出 401 错误状态。)
再看看上一级的vertify.js。
(可以看到在 verify.js 中,它就是调用 jsonwebtoken 原生提供的 verify() 方法进行验证返回结果。** jsonwebtoken 的 sign() 方法用于生成 token ,而 verify() 方法当然则是用来解析 token。**属于jwt配对生产的两个方法,所以koa-jwt这个中间件也没做什么事,无非就是用正则解析请求头,调用jwt的vertify方法验证token,在koa-jwt文件夹的index.js中,koa-jwt还调用koa-unless进行路由权限分发)