koa2实现用户登录、jwt签发token
功能点:
1.注册账号:异常判断-用户名已存在;密码加密存库
2.账号登录:异常判断-用户不存在、密码错误;jwt签发token凭证;前端接收存入localStorage
3.token校验:接口携带token axios config.headers['Authorization'] = 'Bearer ' + token 后端接收校验;异常判断-校验失败、过期,前端跳转登录
新建 config\index.js
const config = { jwt: { SECRET: 'custom' // jwt密钥 }, bcrypt: { saltRounds: 12 // 生成salt迭代次数 }, crypto: { JOINSTR: 'custom' // md5拼接字符串 }, } module.exports = config
新建 utils\user.js
const crypto = require('crypto') // node 自带 const bcrypt = require('bcryptjs') // 需安装依赖 npm i bcryptjs const { bcrypt: bcryptConfig, crypto: cryptoConfig } = require('../config') // md5加密 function md5(v) { const { JOINSTR } = cryptoConfig return crypto.createHash('md5').update(v + JOINSTR).digest('hex') } // 密码加密 function encryptPassword(password) { const { saltRounds } = bcryptConfig const pwdEnCode = md5(password) const salt = bcrypt.genSaltSync(saltRounds) return bcrypt.hashSync(pwdEnCode, salt) } // 密码验证 function verifyPassword(inputPwd, userPwd) { const pwdEnCode = md5(inputPwd) return bcrypt.compareSync(pwdEnCode, userPwd) } module.exports = { encryptPassword, verifyPassword }
调用方式
const { encryptPassword, verifyPassword } = require('../utils/user')
const { jwt } = require('../config')
// 注册模块中 - 密码加密 const pwdEncrypt = encryptPassword(password) // 登录模块中 - 密码校验 const verifyPwd = verifyPassword(password, userInfo.password) // 登录模块中 - 签发token token: jsonwebtoken.sign( { name: userInfo.username, id: userInfo.id }, jwt.SECRET, { expiresIn: '1d' } )
// app.js中
// token校验失败处理 app.use(async (ctx, next) => { return next().catch((err) => { if (err.status === 401) { ctx.status = 401; ctx.body = { code: 401, msg: err.message } } else { throw err; } }) }); // token验证 app.use(koajwt({ secret: jwt.SECRET }).unless({ // 不验证接口 path: [/^\/login$/, /^\/register$/] }));
完整代码 https://gitee.com/senjer/txintegration-node.git
后端

const router = require('koa-router')() const exec = require('../utils/mysql'); const jsonwebtoken = require('jsonwebtoken') const { jwt } = require('../config') const { encryptPassword, verifyPassword } = require('../utils/user') router.prefix('/') router.get('/', async (ctx, next) => { await ctx.render('index', { title: 'demo' }) }) router.post('/register', async(ctx, next) => { const { username, password } = ctx.request.body const userInfo = await exec(`select * from user where username = '${username}'`) if (userInfo.length) { ctx.body = { code: 201, msg: '用户名已存在' } return } const pwdEncrypt = encryptPassword(password) const sql = `insert into user (username, password) values ('${username}', '${pwdEncrypt}')` await exec(sql) ctx.body = { code: 200, msg: '', } }) router.post('/login', async (ctx, next) => { const { username, password } = ctx.request.body const userInfo = (await exec(`select * from user where username = '${username}'`))[0] if (!userInfo) { ctx.body = { code: 201, msg: '用户名不存在', } return } const verifyPwd = verifyPassword(password, userInfo.password) if (verifyPwd) { ctx.body = { code: 200, msg: '登录成功', data: { token: jsonwebtoken.sign( { name: userInfo.username, id: userInfo.id }, jwt.SECRET, { expiresIn: '1d' } ) } } } else { ctx.body = { code: 201, msg: '用户名密码不匹配' } } }) router.post('/list', async (ctx, next) => { const { page, limit } = ctx.request.body // POST 参数 const sql = `select u.*, group_concat(c.course) as course from user u left join course c on c.userId = u.id group by u.id limit ${(page - 1) * limit}, ${limit};` const list = await exec(sql) ctx.body = { code: 200, msg: '', data: { list } } }) module.exports = router

const Koa = require('koa') const app = new Koa() const views = require('koa-views') const json = require('koa-json') const onerror = require('koa-onerror') const bodyparser = require('koa-bodyparser') const cors = require('koa2-cors'); const logger = require('koa-logger') const koajwt = require('koa-jwt') const { jwt } = require('./config') const index = require('./routes') // error handler onerror(app) // middlewares app.use(bodyparser({ enableTypes:['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(require('koa-static')(__dirname + '/public')) app.use(views(__dirname + '/views', { map: { html: 'ejs' } })) app.use(cors({ origin: function (ctx) { if (ctx.url === '/login') { return "*" } return 'http://localhost:8080' }, exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'], maxAge: 5, credentials: true, allowMethods: ['GET', 'POST', 'DELETE'], allowHeaders: ['Content-Type', 'Authorization', 'Accept'], })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) // token校验失败处理 app.use(async (ctx, next) => { return next().catch((err) => { if (err.status === 401) { ctx.status = 401; ctx.body = { code: 401, msg: err.message } } else { throw err; } }) }); // token验证 app.use(koajwt({ secret: jwt.SECRET }).unless({ // 不验证接口 path: [/^\/login$/, /^\/register$/, /^\/$/] })); // routes app.use(index.routes(), index.allowedMethods()) // error-handling app.on('error', (err, ctx) => { console.error('server error', err, ctx) }); module.exports = app
前端

const baseUrl = '' let service = axios.create({ baseUrl: baseUrl, timeout: 30 * 1000, crossDomain: true, withCredentials: true }) // 设置 post 默认 Content-Type service.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8'; // 请求拦截器 service.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config }, (error) => { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use((response) => { let { data } = response; return data }, (error) => { let { status } = error.response; if (status == 401) { // token 过期 window.location.href = '/#/login' } } ) export default service