想达到的效果
使用 /admin
作为管理页面的前缀
javascript//路由配置 app.js const adminRouter = require('./admin'); app.use('/admin', adminRouter);
要求除访问 /admin/login
直接返回外,其他都需要判断token,如果验证未通过就跳转到 /admin/login
,登录之后跳转回之前访问的地址,如果之前的地址没有则跳转到 /admin
。
JWT 配置
使用 express-jwt 拦截请求,统一处理 token 的验证
使用 jsonwebtoken 来签发 token
json"dependencies": { ... "cookie-parser": "~1.4.4" "express": "~4.16.1", "express-jwt": "^5.3.3", "jsonwebtoken": "^8.5.1" }
文件目录
txt▪app.js ▪package.json +public -routers ▪admin.js -views -admin ▪index.html ▪login.html
routers/admin.js
javascript//已在app.js中配置 //app.use('/admin',require('./routers/admin')) var express = require('express'); var router = express.Router(); var jwt = require('jsonwebtoken'); var expressJWT = require('express-jwt'); module.exports = router; const secretKey = 'this is secretKey form admin'; router.use(expressJWT({ secret: secretKey, }).unless({ path: ['/admin/login'] //除/login外其他都要验证Token })); //GET `/admin` page router.get('/', function (req, res, next) { //由自动跳转加上的跳回地址 var backUrl = req.query.backUrl; res.render('admin/index', { title: '网站管理', // 登录前跳转过来的地址,由前端接收后跳转回去 backUrl:backUrl, }); }); // GET `/admin/login` page router.get('/login', function (req, res, next) { res.render('admin/login', { title: '登录-管理员' }); }); //提交登录表单到 `/admin/login` router.post('/login', function (req, res) { var username = req.body.username; var password = req.body.password; //判断用户密码是否匹配 if(匹配){ //生成token var token = jwt.sign( {sub:'admin login'}, secretKey, {expiresIn: '1d'} ); res.json({ status:200, message:'登录成功', token: token }); }else{ res.json({ status: 400, message: '用户名或密码错误' }); } }); router.use(function (err, req, res, next) { if (err.name === 'UnauthorizedError') { //所有验证失败或过期跳转到登录,记录跳转的地址 var backUrl = encodeURIComponent(req.originalUrl); res.redirect(302, '/admin/login?back_to='+backUrl); res.end(); return; } next(err); });
遇到的问题
由于 express-jwt
token 默认验证在Header的 Authorization
字段。现在有一个问题就是用户如果直接在地址栏输入地址访问,这样没办法携带自定义的header的,所以验证必然会失败,即便是已经成功登陆过。
解决方案
1. 使用 cookie
使用 cookie 代替 header 中的 Authorization 字段,token 保存在cookie中
cookie 在每次请求时都会自动提交到服务端,只需修改express-jwt 的getToken
从cookie中获取token字段即可。
app.js 中配置 cookie-parser
javascript// cookieSignKey 可选,设置cookie时可以使用sign:true使之生效,也可以不使用 // 使用sign时,在路由部分使用req.signedCookies.cookieName来获取cookie // 未使用sign的cookie,使用req.cookies.cookieName获取 ... app.use(cookieParser("cookieSignKey")); ...
修改 routers/admin.js 中的对应部分
javascript... router.use(expressJWT({ secret: secretKey, getToken: function fromHeaderOrCookie(req) { //express-jwt 默认是解析 'Bearer '+token 结构的token //这个地方使用自定义的getToken方法,可以去掉这一部分对header的验证 if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { return req.headers.authorization.split(' ')[1]; } else if (req.cookies && req.cookies.token) { //如果写cookie时 sign=true, 这里是 req.signedCookies.token return req.cookies.token; } return null; } }).unless({ //除`/login`外其他都要验证Token path: ['/admin/login'] })); ... //提交登录表单到 `/admin/login` router.post('/login', function (req, res) { var username = req.body.username; var password = req.body.password; //判断用户密码是否匹配 if(匹配){ //生成token var token = jwt.sign({sub:'admin login'}, secretKey ,{expiresIn: '1d'}); //设置cookie res.cookie('token', token, { //不设置默认为当前域名 // domain:'.xxx.com' path: '/admin', signed: true, //前端不可获取该cookie httpOnly: true }); res.json({ status:200, message:'登录成功' }); }else{ res.json({ status: 400, message: '用户名或密码错误' }); } });
2. 分离页面
使用 Ajax 异步加载具体页面内容时在header中填入 token
说明
express-jwt
不在拦截/admin/*
,只拦截/admin/protected/*
。
访问/admin/*
时,返回的页面不包括具体内容,只有公共内容比如header、footer、和导航,具体的内容在公共内容的scripts中使用ajax访问/admin/protected/
中的指定内容异步加载,在使用ajax的时候添加上Authorization
header。
token
存储在localStorage
中。
更改后的文件目录
txt▪app.js ▪package.json +public -routers -admin ▪index.js ▪protectd.js -views -admin ▪index.html ▪index_content.html ▪login.html
app.js中修改路由
javascript... app.use('/admin',require('./routers/admin')); ...
routers/admin/index.js 部分
javascript... //分发/admin/protected/*的路由 router.use('/protected',require('./protected')); //同时去除router.use(expressJWT({})部分 //和下方的验证token失败后的处理 //router.use(function (err, req, res, next) {}))部分 router.get('/', function (req, res) { res.render('admin/index', { title: '网站管理', //异步加载时使用的url content_url: "/admin/protected", }); }); router.get('/login', function (req, res) { var backUrl = req.query.backUrl; res.render('admin/login', { title: '登录-管理员', backurl: back_to, }); }); //登录后跳转页面由前端处理 router.post('/login', function (req, res) { var username = req.body.username; var password = req.body.password; //判断用户密码是否匹配 if(匹配){ //生成token var token = jwt.sign( {sub:'admin login'}, secretKey, {expiresIn: '1d'} ); res.json({ status:200, message:'登录成功', token: token }); }else{ res.json({ status: 400, message: '用户名或密码错误' }); } }); ...
routers/admin/protected.js 部分
javascript... router.use(expressJWT({ secret: secretKey, })); /* GET /admin/protected page. */ router.get('/', function (req, res) { res.render('admin/index_content', { param: ..., }); }); //token验证失败处理 router.use(function (err, req, res, next) { if (err.name === 'UnauthorizedError') { return res.status(401).json({ message: 'Invalid Token' }); } next(err); }); ...
views/admin/login.html 部分
javascript... <scripts> $(this).ajaxSubmit({ url: "/admin/login", type: 'POST', datatype: "json", data: { username: ... password: ... }, timeout: 3000, success: function (result) { if (result['status'] == 200) { //保存token到localStorage localStorage.setItem('token',result['token']); //跳转页面到返回页或admin首页 $(location).attr('href', return_to || '/admin'); } else { alert(result["message"]); }, error: function (data) { alert(data.statusText || data); } }); </scripts> ...
views/admin/index.html 部分
javascript<!doctype html> <html> <body> <div id="contentDiv"> </div> </body> ... <scripts> // scripts 获取页面具体内容 $(document).ready(() => { $.ajax({ //内容地址,后端render时传递的 url: content_url, datatype: "html", type: 'GET', async: 'true', timeout: 3000, // 添加 token 到header, express-jwt 默认需加 'Bearer '在token前 headers: { "Authorization": 'Bearer ' + localStorage.getItem('token') }, success: function (result) { //更新内容视图 $("#contentDiv").html(result); }, error: function (data) { //身份验证失败,跳转到登录页面,并传递当前页面 var gotoUrl = '/admin/login?return_to=' + encodeURIComponent(location.pathname); if (data.status == 401) { location.href = gotoUrl; }else { alert(data); } } }); }); </scripts> </html>