nodeclub
nodeclub copied to clipboard
accessToken的安全性问题
我打算在我的网站中使用cnode的API,于是研究了一下API。我发现accessToken貌似是固定的,这样会不会带来几个问题:
- 某用户不小心把自己的accessToken泄露了出去,那么这个账号就废了,因为任何人都可以永远代替他发贴了。
- 就算用户很小心,但是万一某个第三方应用(比如我)怀有恶意,那么用户就算发现它有恶意,也无法补救。会造成大量的用户账号废了。第三方应用越多,就越危险。
所以建议在论坛加入刷新accessToken的功能。用户修改密码后可以自动刷新accessToken,但因为有GitHub登录方式,所以也需要有一个按钮用来手动刷新accessToken。
(补:我不知道如何修改密码,因为我是用GitHub登录的,好像没有cnode密码。我不知道如果有密码的话修改了之后会不会自动刷新accessToken)
用github登陆的话,确实cnode这边是没有密码的。
目前 accessToken 没有刷新或者修改的方式。可以考虑以后加上。
建议采用通用的jwt方式,有过期时间,修改也相当方便 NPM:jsonwebtoken
大概修改几个地方:
- 增加文件 auth.js 到 api / v1 文件夹下
var _ = require('lodash');
var eventproxy = require('eventproxy');
var jwt = require('jsonwebtoken');
var TopicModel = require('../../models').User;
var UserProxy = require('../../proxy').User;
var TopicProxy = require('../../proxy').Topic;
var authMiddleWare = require('../../middlewares/auth');
var tools = require('../../common/tools');
var config = require('../../config');
/**
* @api {post} /v1/auth/signin 登录
* @apiDescription
* API登录接口 获得 accessToken 信息
* @apiName authSignin
* @apiGroup auth
*
* @apiParam {String} loginname
* @apiParam {String} password
*
* @apiPermission none
* @apiSampleRequest /v1/auth/signin
*
* @apiVersion 1.0.0
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
*/
exports.signIn = function (req, res, next) {
req.checkBody({
'loginname': {
notEmpty: {
options: [true],
errorMessage: 'loginname 不能为空'
}
},
'password': {
notEmpty: {
options: [true],
errorMessage: 'password 不能为空'
},
isLength: {
options: [6],
errorMessage: 'password 不能小于 6 位'
}
}
});
var ep = new eventproxy();
var loginname = req.body.loginname;
var password = req.body.password;
if (req.validationErrors()) {
return res.status(400).json({success: false, err_message: '参数验证失败', err: req.validationErrors()}).end();
}
ep.on('login_error', function (login_error) {
return res.status(403).json({success: false, err_message: '调用登录接口失败', err: login_error}).end();
});
ep.on('login_success', function (accessToken) {
return res.status(200).json({success: true, accessToken: accessToken});
});
ep.on('generate_token', function(user){
let accessToken = jwt.sign({
loginname: user.loginname,
_id: user._id
}, config.jwt_token, {expiresIn: 3600});
TopicModel.findOneAndUpdate({_id: user._id}, {'accessToken': accessToken}, {upsert: true}, function (err, user) { // , 'tokenExpires': tokenExp
if (err) {
return ep.emit('login_error', '保存token出错');
}
ep.emit('login_success', accessToken);
});
});
ep.fail(next);
var getUser;
if (loginname.indexOf('@') !== -1) {
getUser = UserProxy.getUserByMail;
} else {
getUser = UserProxy.getUserByLoginName;
}
getUser(loginname, function (err, user) {
if (err) {
return next(err);
}
if (!user) {
return ep.emit('login_error');
}
var passhash = user.pass;
tools.bcompare(password, passhash, ep.done(function (bool) {
if (!bool) {
return ep.emit('login_error');
}
if (!user.active) {
// DONE (hhdem) 提醒激活不然无法继续进行下一步操作
return ep.emit('login_error', '没有激活, 无法通过api登录');
}
// store session cookie
authMiddleWare.gen_session(user, res);
if (!!user.accessToken) {
jwt.verify(user.accessToken, config.jwt_token, function (err, decoded) {
if (err) {
if (err.name === 'TokenExpiredError') {
return ep.emit('generate_token', user);
}
return ep.emit('login_error', '对已存在token做校验时报错');
}
ep.emit('login_success', user.accessToken);
});
} else {
// 如果没有token 则生成 token
ep.emit('generate_token', user);
}
}));
});
};
- 修改 api / v1 / middleware.js 的 auth 方法
// 非登录用户直接屏蔽
var auth = function (req, res, next) {
let bearerHeader = req.headers["authorization"];
var ep = new eventproxy();
ep.fail(next);
// 使用 jwt 的形式做校验
if (typeof bearerHeader !== 'undefined') {
// authorization 中有用户信息
const bearer = bearerHeader.split(" ");
const bearerToken = bearer[1];
jwt.verify(bearerToken, config.jwt_token, function (err, decoded) {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.send({success: false, error_msg: '您的Token已过期'});
}
return res.send({success: false, error_msg: '登录出错'});
}
ep.emit('auth_success');
});
ep.on('auth_success', function() {
UserModel.findOne({accessToken: bearerToken}, ep.done(function (user) {
if (!user) {
res.status(401);
return res.send({success: false, error_msg: '错误的accessToken'});
}
if (user.is_block) {
res.status(403);
return res.send({success: false, error_msg: '您的账户被禁用'});
}
req.session.user = user;
next();
}));
});
} else {
return res.send({success: false, error_msg: '无权限操作'});
}
};
- 在 api_router_v1.js 里面增加一行
router.post('/auth/signin', authController.signIn);
// 需要做校验的方法加入 middleware.auth 方法
router.post('path', middleware.auth, method);
- 注释掉 proxy/user.js 中newAndSave方法里面的accessToken初始化
user.accessToken = uuid.v4();