我应该如何在Node.js和Redis中实现基于令牌的授权API?

时间:2014-11-12 19:05:56

标签: node.js api authentication

我在一个处理来自Mongo数据库的资源的网络应用程序中工作,对于我想要提供API的资源,所以未来的移动应用程序可以抓住它或从原始客户端使用它

但是,我希望网络应用程序使用相同的API,这里我对如何正确实现它感到有点困惑。

这是我到目前为止所做的:

API Auth:

app.route('/api/auth/')
    .post(function (request,response) {

        var email = request.body.email;
        var password = request.body.password;
        var login = new Account({"local.email":email,"local.password":password});

        Account.findOne({"local.email":email}, function (err,user) {
            if (err) {
                response.send(500);
            }

            if (!user) {
                response.send(404);
            }

            else {

                user.validPassword(password, function (err,matched) {
                    if (err) {
                        response.send(500);
                    }

                    if (matched) {
                        var uuidToken = uuid.v4();
                        redisClient.set(uuidToken,user._id,redis.print);
                        redisClient.expire(user._id,100);
                        response.send(uuid);
                    }

                    else {
                        response.send(403);
                    }
                });                    
            }
        });         
    });

所以基本上我收到消费者的用户名和密码,我根据数据库对其进行身份验证,如果匹配则我回复一个令牌,(实际上是UUID)。该令牌存储在Redis中,与数据库中的用户ID配对。任何API路由的未来任何请求都将验证此类令牌是否存在。

我想知道:

  • 我应该如何管理令牌TTL,并在以后的请求中续订?
  • 如何按时间窗口限制控制请求?
  • 我采取的方法是否有任何安全警告?

网站认证:

基本上我对数据库执行SAME用户名密码验证,然后我:

1。开始新的服务器会话。

2。当然,请提供会话ID的cookie。

第3。我创建了Redis UUID和用户ID记录,API将检查。我想这没关系,因为在请求POST /api/auth再次验证 时有任何意义。

我想知道:

  • 这是最好的方法吗?
  • 我是否应该包含任何令牌盐,以区分纯API消费请求和来自网络应用的请求?
  • 我采取的方法是否有任何安全警告?
  • 我应该包含更多代币吗?

这是POST /login

的示例
app.route('/login')

    .post(function (request,response,next) {

        var email = request.body.email;
        var password = request.body.password;

        var login = new Account({"local.email":email,"local.password":password});

        Account.findOne({"local.email":email}, function (err,user) {

            if (err) {
                                response.redirect('/error');
            }

            if (!user) {

                                var cookie = request.cookies.userAttempts;

                                if (cookie === undefined) {

                                    response.cookie('userAttempts',1);
                                }

                                else {

                                    response.cookie('userAttempts',(++cookie));
                                }

                                response.redirect('/');
            }

            else {

                user.validPassword(password, function (err,matched) {

                    if (err) {

                                    // Redirect error site or show err message.
                                    response.redirect('/error');
                    }

                    if (matched) {

                                    var session = request.session;
                                    session.userid = user._id;
                                    var uuidToken = uuid.v4();
                                    redisClient.set(uuidToken,user._id,redis.print);
                                    redisClient.expire(uuidToken,900);                                        
                                    response.cookie('email',email);
                                    response.redirect('/start');
                    }

                    else {

                            var cookie = request.cookies.passwordAttemps;

                            if (cookie === undefined)

                                response.cookie('passwordAttemps',1);

                            else {
                                var attemps = ++request.cookies.attemps
                                response.cookie('passwordAttemps', attemps)
                            }

                            response.redirect('/');                                    
                    }

                });                    
            }


        });
    })

我想我可以摆脱使用和编写典型的会话实现,并以某种方式依赖于API所具有的类似令牌。

1 个答案:

答案 0 :(得分:1)

你所拥有的是正确的轨道,基本上取代了一些cookie的功能。有一些事情要考虑,你已经触及了其中一些。

  1. 虽然使用UUID(v4我在猜测?)是好的,因为它是不确定的和“随机的”,它自己的令牌毫无价值。如果redis丢失数据,则令牌不再具有任何上下文。如果没有redis的帮助,你也不能执行到期。将其与可以自身携带上下文的JWT进行比较,可以由具有正确密钥的任何人解密,可以处理到期,并且可以执行进一步的常见应用程序级别约束(发行者,受众等)。

  2. 限速。 There are a number of ways to handle this除了您可能使用令牌作为在速率限制器中跨请求识别用户的密钥这一事实之外,其中很少与您选择的令牌方案直接相关。

  3. 透明地在Web应用程序和其他客户端(移动应用程序,桌面应用程序等)上传递令牌可能会非常痛苦。为了访问私有资源,用户需要在某个地方(可能是标题)中传递请求中的令牌,对于Web应用程序,这意味着您需要手动干预以在每个请求中包含令牌。这意味着手动编码所有经过身份验证的请求的ajax请求。虽然这可能很烦人,但至少可以做到,如果你正在编写单页应用程序,那么你很可能会这样做。任何移动或桌面客户端都可以这样说。既然您必须直接在代码中发出HTTP请求,为什么这很重要?现在想象一下,只有通过适当的身份验证才能访问返回html页面的HTTP GET端点。对于Web应用程序,用户很可能通过浏览器重定向或直接在URL栏中键入它来访问它。 How is the token added to the request?除了使用明确未使用的cookie,因为移动和桌面客户端没有实现它们,这实际上是不可能的。但是,如果您的API客户端始终可以修改HTTP请求结构,那么这不是一个真正的问题。

  4. 现在是一个无耻的插件,our team has a library we use for this。它主要在内部使用,因此对它的依赖关系(express,redis)很有见解,但希望它可以帮到你。事实上,该库几乎只是一个JWT包装器,它包含了您所拥有的内容。如果您决定使用它并注意任何问题或缺陷,请随时在github上提出任何问题。否则,在npm上有一大堆其他基于JWT的会话管理模块看起来很有前景。我会检查那些,因为那里的模块很可能比我们的更好。同样,我们的内部使用并且来自一组非常具体的用例,因此它捕获所有用户的可能性非常小。另一方面,听起来你正在使用类似的堆叠,所以也许鞋子适合。

    如果您使用我们的,那么在该模块的API表面中存在分割可能看起来很奇怪,您可以选择直接在JWT声明或redis中存储数据。这是故意的,我认为你的例子说明了双方的一个很好的用例。通常我们所做的是将用户的电子邮件和名称存储在JWT声明中,然后在会话中以redis格式存储更多动态会话数据。例如,在登录时,您需要将发行者,受众和用户的电子邮件添加到JWT声明中,但不要与“userAttempts”相关的任何内容。然后,在尝试失败时,您将添加或修改存储在与该JWT相关的redis中的会话数据的“userAttempts”。一旦设置了JWT,就不可能在不生成新内容的情况下修改其内容,因此请注意,如果您决定在JWT中保留相对动态的数据,您将在服务器和客户端之间不断交换新旧JWT。