egg中 jwt使用

1,419 阅读4分钟

1.是什么

json web token

优点:

  • 无状态,
  • 跨域,
  • 能把json对象通过jwt转成token ,并且还原json对象
  • 加密安全

1.Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源 区别:
  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

1.2常见的前后端鉴权方式

  1. Session-Cookie
  2. Token 验证(包括 JWT,SSO)
  3. OAuth2.0(开放授权)

2.格式

  • Header(头部)+ Payload(负载/载荷)+ Signature(签名)
  • 例如:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

1.header 加密方式

  • 代表加密方式 可以反解密,一个json对象,描述JWT的元数据
  • 声明类型,这里是 jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
  如:{"typ":"JWT","alg":"HS256"}

2.payload 传输内容

  • 代表具体的传输内容 可以解密,一个json对象
  • 3部分组成
  1. 标准中注册的声明
  2. 公共的声明
  3. 私有的声明 // 包括需要传递的用户信息;
{ 
  //标准
  "iss": "Online JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.gusibi.com", 
  "sub": "uid", 
  //公共/私有
  "nickname": "goodspeed", 
  "username": "goodspeed", 
  "scopes": [ "admin", "user" ] 
}

3.signature 签名

  • signature 是指:通过header里描述的加密方式,把 (payload+secret)加密 = 加密结果
  • 其中secret 私钥是用户自己保存好的私钥

3.验证流程

前端

  • 前端发送用户名密码调用登录接口到后端,
  • 后端返回jwt
  • 前端存储 JWT,到浏览器如:localStorage.setItem('jwt_token', response.data.token)
  • 在http请求头加上 Authorization: Bearer xxx.yyy.zzz

后端

  • 登录接口后端:查询数据库验证用户名和密码成功,根据自己的密钥 + 用户基本信息生成JWT并返回给前端
  • 每次其他接口请求,后端都会从 Authorization 头拿到 JWT 检查格式是否正确
  • 验证签名是否合法(确保没被篡改)
  • 检查 Payload 中的 exp(过期时间)等声明是否有效
  • 如果一切 OK,可以从中获取用户信息来进行业务逻辑处理

3.实战

1.客户端请求设置

vue/nuxt 设置头部信息

// /plugins/axios.js
import Vue from 'vue'
import axios from 'axios' 
let service = axios.create({
  timeout:5000,
  // 前缀
  baseURL:'/api'
})
const TOKEN_KEY = 'USER_TOKEN'

// @ todo 拦截器 管理token
export default({store, redirect})=>{
  // 请求拦截
  service.interceptors.request.use(
    config=>{
      // 请求加token
      const token = window.localStorage.getItem(TOKEN_KEY)
      // 设置url白名单 
      if(token){
        config.headers.common['Authorization'] = 'Bearer '+token //添加 jwt 使用的 Bearer空格 格式
      }
      return config
    },
    err=>{
      return Promise.reject(err)
    }
  )
}

2.1 egg-jwt后端实现

npm i egg-jwt -s //安装库
//config/plugin.js
jwt:
{ enable: true, 
package: 'egg-jwt', 
}
//config/config.default.js
  config.jwt = {
    secret: 'Great4-M',
    enable: true, // default is false
    match: /^\/api/, // optional
  }
//service/actionToken.js
//产生一个token ,_id为数据库id
const { Service } = require('egg')
class ActionTokenService extends Service {
    async apply(_id) {
        const { ctx } = this
        return ctx.app.jwt.sign({
            data: {
                _id: _id
            },
            exp: Math.floor(Date.now() / 1000 + (60 * 60 * 7))
        }, ctx.app.config.jwt.secret)
    }

}
module.exports = ActionTokenService

//service/userAccess.js
const { Service } = require('egg')
class UserAccessService extends Service {
    async login(payload) {
        const { ctx, service } = this
        const user = await service.user.findByMobile(payload.mobile)
        console.log('88888mobile'+payload.moblie)
        if (!user) {
            ctx.throw(404, 'user not found')
        }
        let verifyPsw = await ctx.compare(payload.password, user.password)
        if (!verifyPsw) {
            ctx.throw(404, 'user password is error')
        }
        // 生成Token令牌
        return { token: await service.actionToken.apply(user._id) }
    }
    async logout() {
    }

    async current() {
        const { ctx, service } = this
        // ctx.state.user 可以提取到JWT编码的data
        const _id = ctx.state.user.data._id
        const user = await service.user.find(_id)
        if (!user) {
            ctx.throw(404, 'user is not found')
        }
        user.password = 'How old are you?'
        return user
    }
}

module.exports = UserAccessService

// app/contract/userAccess.js
module.exports = {
    loginRequest: {
      mobile: { type: 'string', required: true, description: '手机号', example: '1880000000', format: /^1[34578]\d{9}$/, },
      password: { type: 'string', required: true, description: '密码', example: '111111', },
    },
  }

//controller/userAccess.js
'use strict'
const Controller = require('egg').Controller
/**
 * @Controller 用户鉴权
 */
class UserAccessController extends Controller {

  constructor(ctx) {
    super(ctx)
  }

  /**
   * @summary 用户登入
   * @description 用户登入
   * @router post /auth/jwt/login
   * @request body loginRequest *body
   * @response 200 baseResponse 创建成功
   */
  async login() {
    const { ctx, service } = this
    // 校验参数
    ctx.validate(ctx.rule.loginRequest);
    // 组装参数
    const payload = ctx.request.body || {}

    // 调用 Service 进行业务处理
    const res = await service.userAccess.login(payload)
    // 设置响应内容和响应状态码
    ctx.helper.success({ ctx, res })
  }

  /**
   * @summary 用户登出
   * @description 用户登出
   * @router post /auth/jwt/logout
   * @request body loginRequest *body
   * @response 200 baseResponse 创建成功
   */
  async logout() {
    const { ctx, service } = this
    // 调用 Service 进行业务处理
    await service.userAccess.logout()
    // 设置响应内容和响应状态码
    ctx.helper.success({ ctx })
  }
}

module.exports = UserAccessController

2.2自定义注册中间件,egg后端实现

npm i jsonwebtoken -s //安装jwt

server/config/config.default.js  
module.exports = appInfo => { 
  return {
    ...config,
    ...userConfig, 
    jwt: {
      secret: 'xxxxxxx', //定义配置信息
    },
  }
}

//app/middleware/jwt.js
// 解析token的中间件,也可以用egg-jwt,自己封装更适合了解原理
const jwt = require('jsonwebtoken')
module.exports = ({ app }) => {
  return async function verify(ctx, next) {
    if (!ctx.request.header.authorization) {
      ctx.body = {
        code: -666,
        message: '用户没有登录',
      }
      return
    }
    const token = ctx.request.header.authorization.replace('Bearer ', '')
    try {
      const ret = await jwt.verify(token, app.config.jwt.secret)
      ctx.state.email = ret.email
      ctx.state.userid = ret._id
      await next()
    } catch (err) {
      console.log(err)
      if (err.name === 'TokenExpiredError') {
        ctx.body = {
          code: -666,
          message: '登录过期了',
        }
      } else {
        ctx.body = {
          code: -1,
          message: '用户信息出错',
        }
      }
    }
  }
}
//app/router.js 
module.exports = app => {
  const { router, controller } = app
  const jwt = app.middleware.jwt({ app })
  router.get('/', controller.home.index)

  router.group({ name: 'user', prefix: '/user' }, router => {
    const {
      info,login, register,
    } = controller.user
    router.post('/register', register)
    router.post('/login', login) 
    router.get('/info', jwt, info) //针对需要鉴权的加上jwt的校验
    }
}


//app/controller/user.js
const md5 = require('md5')
const jwt = require('jsonwebtoken')
const BaseController = require('./base')
const HashSalt = 'xxxxxx11223' 

class UserController extends BaseController {

  async login() {
    // this.success('token')
    const { ctx, app } = this
    const { email, captcha, passwd, emailcode } = ctx.request.body  
    const user = await ctx.model.User.findOne({
      email,
      passwd: md5(passwd + HashSalt),
    })
    if (!user) {
      return this.error('用户名密码错误')
    }
    // 用户的信息加密成token 返回
    const token = jwt.sign({
      _id: user._id,
      email,
    }, app.config.jwt.secret, {
      expiresIn: '100h',
    })
    this.success({ token, email, nickname: user.nickname })
  } 
}

module.exports = UserController

3.egg的jwt 控制流程图

image.png