1.是什么
json web token
优点:
- 无状态,
- 跨域,
- 能把json对象通过jwt转成token ,并且还原json对象
- 加密安全
1.Token 和 JWT 的区别
相同:
- 都是访问资源的令牌
- 都可以记录用户的信息
- 都是使服务端无状态化
- 都是只有验证成功后,客户端才能访问服务端上受保护的资源 区别:
- Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
- JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
1.2常见的前后端鉴权方式
- Session-Cookie
- Token 验证(包括 JWT,SSO)
- 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部分组成
- 标准中注册的声明
- 公共的声明
- 私有的声明 // 包括需要传递的用户信息;
{
//标准
"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