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)  // 加密钥

常见的使用方式

可以存储在

  1. cookie 内部

  2. 存储在localstorage 中

使用

  1. cookie 发送 不能跨域

  2. 随着 Header 添加字段 Authorization: Bearer //注意中间有个空格

  3. post 请求的时候放在post请求的数据体内

  4. 因为Axios 可以设置拦截器与请求头自动 所以常放在Header 中与Axios一起使用

  5. 因为JWT 的服务器控制权比较低,也就是令牌发送后服务器就不能确定这个另外的使用者是否更换了,所以过期时间一般设置较短,另外容易发生JWT 被劫持,因此必须配合HTTPS使用。

  6. JWT 建议加密

JWT黑名单

  1. 客户端要求失效,可以对非正常操作过期时间直接设置为0的方式,

  2. 如果token 储存在redis 中,记录uid-time键值对,在redis 中设置黑名单,对于黑名单用户拒绝服务

  3. 客户端可以一键设置黑名单

  4. 用户重置密码 将token失效。

  5. jwt续签问题,一种解决方式是jwt中存储过期时间,服务端设置刷新时间,请求是判断是否在过期时间或刷新时间,在刷新时间内进行token刷新,而失效token记入黑名单;

  6. 而黑名单过大问题,可以采用记录UID-刷新时间方式解决,判断jwt签发时间,jwt签发时间小于UID-刷新时间的记为失效

  7. 还可以对用户的短期登录频次以及错误进行记录

实现

自己的代码实现

安装

//安装 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

// 加密方法
jwt.sign(payload, secretOrPrivateKey, [options, callback])
//验证方法
jwt.verify(token, secretOrPublicKey, [options, callback])

加密方法 jwt.sign

  1. 第一个参数 载荷

  2. 第二个参数 加密方式 或者是字符串 或者是一个key 文件

  3. 第三个参数为options 可选 见地址 这些选项可以被签名生成过程时被添加到 payload 和header 中

  4. 第四个为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

与加密相对应

  1. 第一个参数 需要被解密验证的token

  2. 第二个参数 加密方式 或者是字符串 或者是一个对应的publickkey 文件

  3. 选项 见官方地址

// 使用
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 详细分析 作者:嚣张小飞

jWT白名单

Last updated

Was this helpful?