http协议是无状态协议,即协议对于处理没有记忆能力。而且每次连接只处理一个请求。服务器处理完客户的请求,并收到客户端的应答后,断开连接。
所以,服务器端需要使用一种手段来验证客户端的身份,并且还要预防黑客的攻击
前两种:
使用 cookie 和 session 的方式:
const Koa = require('koa');
const router = require('koa-router')();
const app = new Koa();
// cookie和session
const koaSession = require('koa-generic-session');
const session = koaSession({
key: 'sessionid',
maxAge: 1000, /** (number) maxAge in ms (default is 1 days),cookie的过期时间 */
overwrite: true, /** (boolean) can overwrite or not (default true) */
httpOnly: true, /** cookie是否只有服务器端可以访问 (boolean) httpOnly or not (default true) */
signed: true /** 加密? */
}, app)
// 加盐操作:在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
app.keys = ['aaa', 'bbb'] // 数组形式
router.get('/text/login', function (ctx, next) {
// 在服务器为登录的客户端,设置一个加密的cookie (服务器自动为浏览器保存一个cookie,每次请求都会带上cookie)
ctx.session.name = 'ikun'
ctx.body = '登录成功~'
})
router.get('/text/list', function (ctx, next) {
const value = ctx.session.name
if(value === 'ikun') ctx.body = 'userList~'
ctx.body = '没有权限,先登录'
})
app.use(session) // 存入中间件
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(7788,()=>{
console.log('starting at port 3000');
});
解释:
-
上图可以发现:直接在浏览器通过url访问(客户端没有执行其他操作),服务端自动设置了cookie(pc端),在下一次请求会自动携带该cookie
- 使用了加盐模式,会出现俩个对应的
sessionid
和sessionid.sig
,黑客需要同时破解才可以伪造信息 - 而且可以发现,在其他页面,这里的cookie也是存在着,这是区分不同页面的标识
- 使用了加盐模式,会出现俩个对应的
-
cookie和session缺点
-
cookie会被附加在每一个 HTTP 请求中,无形增加了流量(某些请求不需要)
-
cookie是明文传递的,存在安全性问题
-
cookie的大小限制是 4kb,对于复杂需求是不够的
-
对于pc端浏览器可以自动保存,但是对于移动端需要自己手动设置cookie和session
-
对于分布式系统和服务器集群中保证不同系统正确解析session的困难性
- 解决高并发出现的不同系统
-
第三种
token
使用 token (令牌)验证身份
- 验证用户账号密码正确情况,给用户颁发一个令牌(可以作为后续用户访问接口的有效凭证)
- 登录生成颁发token,访问某些接口验证token
JWT 实现 token 机制
三部分组成
-
header
- alg:采用的加密算法,默认是 HMAC SHA256(HS256),采用一个密钥进行加密和解密(对称加密)
- typ:JWT(JSON Web Token),固定值,写 JWT 即可
- 会通过base64算法进行编码
-
payload
- 携带的数据,比如我们可以将用户的id和name放到payload中
- 默认也会携带 iat(issued at),令牌签发时间
- 设置过期时间 exp(expiration time)
- 会通过base64 算法进行编码
-
signature
- 设置一个 secretKey,通过将前两个结果合并后进行 HMAC SHA256的算法
- HMAC SHA256(base54Url ( header ) + . + base64Url ( payload ), secretKey)
- secretKey不可暴露(暴露就可以模拟颁发token,也可以解密token)
代码实现:(对称加密)
const Koa = require('koa');
const router = require('koa-router')();
const jwt = require('jsonwebtoken') // jwt鉴权
const app = new Koa();
const secretkey = 'aabbcc' // 不可以暴露
router.get('/text/login', function (ctx, next) {
const payload = {id: 111, name: 'lhy'}
const token = jwt.sign(payload, secretkey, {
expiresIn: 60
})
ctx.body = {
code: 0, token, message: '登陆成功'
}
})
router.get('/text/list', function (ctx, next) {
const authorization = ctx.headers.authorization // 一般设置将token存在请求头的headers.authorization中
const token = authorization.replace('Bearer', '')
try {
const result = jwt.verify(token, secretkey) // 验证,如果验证失败会throw error(invalid token)
ctx.body = {code: 0, data: [1,2,3]}
} catch(e) {
ctx.body = {code: -1010, message: '无效token'}
}
})
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(7788,()=>{
console.log('starting at port 3000');
});
注意:
有安全性问题:对于分布式系统或者服务器集群,如果黑客攻击某一个子系统获得了 secretkey 值,而且不同系统用的是同一套加密算法(对称加密),则可以通过伪造系统给用户颁发 token。
解决:
-
用非对称加密(RS256):
- 私钥:private_key发布令牌
- 公钥:public_key验证令牌
- 公钥和私钥一般都是直接通过命令生成的
-
父系统用 私钥 颁发token(父系统有充足安全措施,不怕被黑客攻击),子系统用 公钥 验证(黑客攻击获得公钥,无法伪造颁发token)
- 在 linux 或者 Max 系统可以直接打开终端
- 在 windows 系统需要用 git bash 打开openssl
-
使用(windows 系统):
- 新建 keys 文件夹,用git bash 打开输入:
openssl # 会进入 OpenSSL> genrsa -out private.key 2048 # 生成秘钥(.key结尾) 大小2048 # secretOrPrivateKey has a minimum key size of 2048 bits for RS256 rsa -in private.key -pubout -out public.key # 生成公钥
具体实现:
// token非对称加密
const fs = require('fs') // './' 返回你执行node命令的路径
const path = require('path')
// __dirname总是指向被执行js文件的绝对路径,在/d1/d2/1.js文件中写了__dirname,它的值就是/d1/d2
const privateKey = fs.readFileSync(path.resolve(__dirname,'keys/private.key')); // 同步读取
const publicKey = fs.readFileSync(path.resolve(__dirname,'keys/public.key')); // 读取到的是一个buffer二进制流
router.get('/text3/login', function (ctx, next) {
const payload = {id: 111, name: 'lhy'}
const token = jwt.sign(payload, privateKey, { // token接受buffer二进制流
expiresIn: 60,
algorithm: 'RS256' // 默认是HA256对称加密,需要改为非对称加密
})
ctx.body = {
code: 0, token, message: '登陆成功'
}
})
router.get('/text3/list', function (ctx, next) {
const authorization = ctx.headers.authorization
const token = authorization.replace('Bearer', '')
try {
const result = jwt.verify(token, publicKey, {
algorithm: ['RS256'] // 传入数组,解密失败用下一种算法解密
}) // 用公钥验证
ctx.body = {code: 0, data: [1,2,3]}
} catch(e) {
ctx.body = {code: -1010, message: '无效token'}
}
})