nodeclub icon indicating copy to clipboard operation
nodeclub copied to clipboard

accessToken的安全性问题

Open zhanzhenzhen opened this issue 8 years ago • 3 comments

我打算在我的网站中使用cnode的API,于是研究了一下API。我发现accessToken貌似是固定的,这样会不会带来几个问题:

  1. 某用户不小心把自己的accessToken泄露了出去,那么这个账号就废了,因为任何人都可以永远代替他发贴了。
  2. 就算用户很小心,但是万一某个第三方应用(比如我)怀有恶意,那么用户就算发现它有恶意,也无法补救。会造成大量的用户账号废了。第三方应用越多,就越危险。

所以建议在论坛加入刷新accessToken的功能。用户修改密码后可以自动刷新accessToken,但因为有GitHub登录方式,所以也需要有一个按钮用来手动刷新accessToken。

(补:我不知道如何修改密码,因为我是用GitHub登录的,好像没有cnode密码。我不知道如果有密码的话修改了之后会不会自动刷新accessToken)

zhanzhenzhen avatar Mar 09 '17 00:03 zhanzhenzhen

用github登陆的话,确实cnode这边是没有密码的。

目前 accessToken 没有刷新或者修改的方式。可以考虑以后加上。

alsotang avatar Mar 09 '17 03:03 alsotang

建议采用通用的jwt方式,有过期时间,修改也相当方便 NPM:jsonwebtoken

hhdem avatar Apr 23 '17 13:04 hhdem

大概修改几个地方:

  1. 增加文件 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);
            }


        }));
    });
};
  1. 修改 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: '无权限操作'});
    }
};
  1. 在 api_router_v1.js 里面增加一行
router.post('/auth/signin', authController.signIn);

// 需要做校验的方法加入 middleware.auth 方法
router.post('path', middleware.auth, method);
  1. 注释掉 proxy/user.js 中newAndSave方法里面的accessToken初始化
user.accessToken = uuid.v4();

hhdem avatar Apr 24 '17 05:04 hhdem