1.4 Koa 集成JWT
什么是JWT
JWT 即为JSON Web Token的简写目前最流行的跨域认证解决方案。
JWT是通过在用户经过服务器认证后,由服务器发送给客户端一串JSON字符串,而这串字符串内部存储用户的信息,以及登录过期时间等等,在用户在其后的每次请求的请求头中带上这串JWT字符,达到认证的效果。
格式
JSON 数据格式
{
"username": "zhangsan",
"role": "user",
"expiratAt": ""
}
字符数据格式
Header.Payload.Signature
// 分别是三串加密字符串
请求格式
Authorization: Bearer <token> //注意中间有个空格
Header 格式
{
"alg": "HS256", // 加密方式 默认是hs256
"typ": "JWT" //令牌类型
}
Payload
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
//官方指定的 7个字段
//可以自己定义新的字段不受限制
//JWT 的payload 默认是不加密的 所以不要将敏感数据放在着
Signature
服务器对于JWT的签名,放置JWT被篡改 ,签名需要Header 和Payload 以及一个 服务器的Key 进行加密
HMACSHA256( //默认是 HMACSHA256 加密
base64UrlEncode(header) + "." + // 头部
base64UrlEncode(payload), //payload
secret) // 加密钥
常见的使用方式
可以存储在
cookie 内部
存储在localstorage 中
使用
cookie 发送 不能跨域
随着 Header 添加字段 Authorization: Bearer //注意中间有个空格
post 请求的时候放在post请求的数据体内
因为Axios 可以设置拦截器与请求头自动 所以常放在Header 中与Axios一起使用
因为JWT 的服务器控制权比较低,也就是令牌发送后服务器就不能确定这个另外的使用者是否更换了,所以过期时间一般设置较短,另外容易发生JWT 被劫持,因此必须配合HTTPS使用。
JWT 建议加密
JWT黑名单
客户端要求失效,可以对非正常操作过期时间直接设置为0的方式,
如果token 储存在redis 中,记录uid-time键值对,在redis 中设置黑名单,对于黑名单用户拒绝服务
客户端可以一键设置黑名单
用户重置密码 将token失效。
jwt续签问题,一种解决方式是jwt中存储过期时间,服务端设置刷新时间,请求是判断是否在过期时间或刷新时间,在刷新时间内进行token刷新,而失效token记入黑名单;
而黑名单过大问题,可以采用记录UID-刷新时间方式解决,判断jwt签发时间,jwt签发时间小于UID-刷新时间的记为失效
还可以对用户的短期登录频次以及错误进行记录
实现
自己的代码实现
安装
//安装 base64URl 解析
npm install base64url
生成token
const base64UrlEncode = require('base64url')
const crypto = require('crypto');
exports.login =async ctx=>{
//过期时间
const expireAt = Date.now() + 24 * 60 * 60 * 1000;
const privateKey = fs.readFileSync("../private_key.pem").toString(); //私钥
const header = base64UrlEncode( // header并进行base 64的编码
JSON.stringify({
alg: "HS256",
typ: "JWT"
})
);
const payload = base64UrlEncode( // payload 并进行base64 编码
JSON.stringify({ username: ctx.request.body.username, expireAt })
);
const Algorithm = "SHA256"; //加密方式
const sign = crypto.createSign(Algorithm); //创建签名
// 使用 update 方法更新数据
sign.update(header + "." + payload);
// 生成签名 以 hex 格式输入数据
const singniture = sign.sign(privateKey, "hex");
//生成token
const token = header + "." + payload + "." + singniture;
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({ //发送给前端
token,
userID: ctx.request.body.username,
expireAt
});
}
前端接收到token存储
const login = async (user)=>{
const {username,password} = user
password = md5(password) // 前端的MD5 加密密码
//登录
const result = await axios.post('localhost:8080/users/login',{
username,
password
})
//拿到登录成功的result 中的token
const {token,userID,expireAt} =result.data
//存储到localStorage 中
localStorage.set("token",token)
localStorage.set("userID",userID)
localStorage.set("expireAt",expireAt)
}
前端每次请求都带上token
// 这里可以使用axios的全局设置
const AUTH_TOKEN = localStorage.get('token')? "Bearer "+localStorage.get('token'):""
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// 也可以使用拦截器进行个性化设置 例如对于每次的post 提交增加token
// 设置可以根据请求的URL 细化
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout:1500
});
instance.interceptors.request.use((request)=>{
if(request.method==="POST"){
const AUTH_TOKEN = localStorage.get('token')? "Bearer "+localStorage.get('token'):""
request.headers.common['Authorization'] = AUTH_TOKEN;
}
return request
})
后端对于token 的验证
后端再次拿到前端的token 主要是为了进行签名验证,查看签名是否被改动过,以及过期时间。
const base64UrlEncode = require("base64url");
const crypto = require("crypto");
const fs = require("fs");
const publicKey = fs.readFileSync("./public_key.pem").toString();
router.all("/*", async (ctx, next) => {
const auth = ctx.header.authorization;
if (auth) {
const Algorithm = "SHA256";
// 创建验证
const verify = crypto.createVerify(Algorithm);
verify.update(data);
//验证签名
const result = verify.verify(publicKey, auth, "hex");
if(result){//签名验证通过
// 校验过期时间
const payload = base64UrlEncode.decode(auth.split('.')[1])
if(payload.exp<Date.now()){ //过期了
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({ // 可能要求重新登录
error:"sorry! 过期了!",
statusCode:401
});
}else{ //没过期
await next()
}
}else{//没通过返回无权限
ctx.set("Content-Type", "application/json");
ctx.body = JSON.stringify({ //可能要求重新登录
error:"sorry!",
statusCode:401
});
}
}else{
// 没有auth 字段 URL验证是否可以访问
}
});
nodejs
使用jsonwebtoken
包
nodejs
使用jsonwebtoken
包// 加密方法
jwt.sign(payload, secretOrPrivateKey, [options, callback])
//验证方法
jwt.verify(token, secretOrPublicKey, [options, callback])
加密方法 jwt.sign
jwt.sign
第一个参数 载荷
第二个参数 加密方式 或者是字符串 或者是一个key 文件
第三个参数为options 可选 见地址 这些选项可以被签名生成过程时被添加到 payload 和header 中
第四个为callback 接收err 和token
//常见用法
exports.login = async ctx=>{
const keyFile =await rs.readFile('../private_key.pem').toString()
const payload ={
username,
userid,
http://foremp.api.com:true
}
const token = await jwt.sign(payload, keyFile, {
expiresIn: '1h', //2h 3days 等等
keyid:111
})
}
验证 jwt.verify
与加密相对应
第一个参数 需要被解密验证的token
第二个参数 加密方式 或者是字符串 或者是一个对应的publickkey 文件
选项 见官方地址
// 使用
var cert = fs.readFileSync('public.pem'); // get public key
try{
const decoded = jwt.verify(token, cert,
{ audience: 'urn:foo', // 验证选项
issuer: 'urn:issuer',
jwtid: 'jwtid',
subject: 'subject' });
}catch(err){
if(err) //如果验证没通过会抛出错误异常
err = {
name: 'TokenExpiredError', //过期
message: 'jwt expired',
expiredAt: 1408621000
}
}
jwt.decode(token [, options]) 对token解码
返回值为 字符串状态的 header 和payload 可以自己手动来进行验证。
koa-jwt
koa-jwt
用来查看请求中是否有token信息的中间件
app.use(
jwtKoa({secret: SECRET})
.unless({
path: [/\/login/] // 不需要通过jwt验证的请求路径
})
)
router.get('/login', async (ctx) => {
let token = jwt.sign({
name: 'dva'
}, SECRET)
console.log(token, 'token')
ctx.body = {
token
}
})
router.get('/try', async (ctx) => {
let token = ctx.header.authorization
let result = jwt.verify(token, SECRET)
ctx.body = {
result
}
})
JWT 的常见问题解决
jWT白名单
Last updated
Was this helpful?