使用Node和Express 4进行基本的HTTP身份验证

169

看起来在 Express v3 中实现基本的 HTTP 身份验证是很简单的:

app.use(express.basicAuth('username', 'password'));

第四个版本(我正在使用4.2)移除了basicAuth中间件,所以我有点困惑。我有以下代码,但它不会提示浏览器要求用户输入凭据,这正是我想要的(也是我想象中旧方法的作用):

app.use(function(req, res, next) {
    var user = auth(req);

    if (user === undefined || user['name'] !== 'username' || user['pass'] !== 'password') {
        res.writeHead(401, 'Access invalid for user', {'Content-Type' : 'text/plain'});
        res.end('Invalid credentials');
    } else {
        next();
    }
});

5
厚颜无耻地推销一下:我维护了一个相当受欢迎的模块,它可以轻松实现大多数您需要的标准功能:express-basic-auth - LionC
我最近fork了@LionC的包,因为我不得不在极短的时间内为公司项目进行适应(启用上下文感知授权者):https://www.npmjs.com/package/spresso-authy - castarco
2
@LionC,从express-basic-auth的文档中并不清楚如何将其应用于一个路由。 - Oleg Abrazhaev
10个回答

192

使用原生 JavaScript (ES6) 实现简单的基础认证

app.use((req, res, next) => {

  // -----------------------------------------------------------------------
  // authentication middleware

  const auth = {login: 'yourlogin', password: 'yourpassword'} // change this

  // parse login and password from headers
  const b64auth = (req.headers.authorization || '').split(' ')[1] || ''
  const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':')

  // Verify login and password are set and correct
  if (login && password && login === auth.login && password === auth.password) {
    // Access granted...
    return next()
  }

  // Access denied...
  res.set('WWW-Authenticate', 'Basic realm="401"') // change this
  res.status(401).send('Authentication required.') // custom message

  // -----------------------------------------------------------------------

})

注意:这个“中间件”可以在任何处理程序中使用。只需移除next()并反转逻辑即可。请参见下面的1语句示例,或查看此答案的编辑历史记录

为什么?

  • req.headers.authorization包含值"Basic <base64字符串>",但它也可能为空,我们不希望它失败,因此采用奇怪的||''组合
  • Node不知道atob()btoa(),因此使用Buffer

ES6 -> ES5

const就是var..差不多
(x, y) => {...}就是function(x, y) {...}
const [login, password] = ...split()就是两个var赋值在一个语句中

启发来源(使用包)


上面是一个超级简单的示例,旨在快速部署到您的游乐场服务器。但正如评论中指出的那样,密码也可能包含冒号字符:。为了正确从b64auth中提取它,可以使用以下内容。

  // parse login and password from headers
  const b64auth = (req.headers.authorization || '').split(' ')[1] || ''
  const strauth = Buffer.from(b64auth, 'base64').toString()
  const splitIndex = strauth.indexOf(':')
  const login = strauth.substring(0, splitIndex)
  const password = strauth.substring(splitIndex + 1)

  // using shorter regex by @adabru
  // const [_, login, password] = strauth.match(/(.*?):(.*)/) || []

一句话理解基本身份认证

......另一方面,如果您只使用一个或很少的登录,那么这是您所需要的最低限度:(您甚至无需解析凭据)

function (req, res) {
//btoa('yourlogin:yourpassword') -> "eW91cmxvZ2luOnlvdXJwYXNzd29yZA=="
//btoa('otherlogin:otherpassword') -> "b3RoZXJsb2dpbjpvdGhlcnBhc3N3b3Jk"

  // Verify credentials
  if (  req.headers.authorization !== 'Basic eW91cmxvZ2luOnlvdXJwYXNzd29yZA=='
     && req.headers.authorization !== 'Basic b3RoZXJsb2dpbjpvdGhlcnBhc3N3b3Jk')        
    return res.status(401).send('Authentication required.') // Access denied.   

  // Access granted...
  res.send('hello world')
  // or call next() if you use it as middleware (as snippet #1)
}

提示:你是否需要同时拥有“安全”和“公共”路径?考虑使用express.router替代。


var securedRoutes = require('express').Router()

securedRoutes.use(/* auth-middleware from above */)
securedRoutes.get('path1', /* ... */) 

app.use('/secure', securedRoutes)
app.get('public', /* ... */)

// example.com/public       // no-auth
// example.com/secure/path1 // requires auth

7
不要使用.split(':'),因为包含至少一个冒号的密码会导致其出错。这样的密码根据RFC 2617是有效的。 - Steve
2
你也可以使用正则表达式 const [_, login, password] = strauth.match(/(.*?):(.*)/) || [] 来匹配冒号部分。 - adabru
4
使用!==比较密码会使您容易受到定时攻击的影响。请确保使用恒定时间的字符串比较。参考:https://en.wikipedia.org/wiki/Timing_attack - hraban
2
因为时序攻击漏洞被踩了,应该使用 https://nodejs.org/api/crypto.html#crypto_crypto_timingsafeequal_a_b。例如,可以看看 express-basic-auth 是如何实现的,https://github.com/LionC/express-basic-auth/blob/fa600c90707a69f1f3a0fa9f0295f412fc207e4e/index.js#L5-L17。 - Thibaud Colas
2
@Qwerty,我没有一篇好的关于这个主题的文章(维基百科有帮助吗?请参见@hraban分享的https://en.wikipedia.org/wiki/Timing_attack)。你的观点很到位-网络因素使时序攻击变得不太实用。但是依赖这一点将是“安全性通过混淆”的情况-它可能在这里和那里有所帮助,但它不如实现恒定时间比较的保护好。请参见https://nvd.nist.gov/vuln/detail/CVE-2015-7576,了解Rails中类似漏洞及其严重程度评级的情况。 - Thibaud Colas
显示剩余4条评论

131

TL;DR:

express.basicAuth已经被移除
basic-auth-connect已经被弃用
basic-auth没有任何逻辑
http-auth过于繁琐
express-basic-auth是你需要的

更多信息:

由于您正在使用Express,因此可以使用express-basic-auth中间件。

请参阅文档:

示例:

const app = require('express')();
const basicAuth = require('express-basic-auth');
 
app.use(basicAuth({
    users: { admin: 'supersecret123' },
    challenge: true // <--- needed to actually show the login dialog!
}));

23
我花了一些时间才弄清楚challenge: true选项的含义。 - Vitalii Zurian
1
@VitaliiZurian 好观点 - 我已经将它添加到答案中。谢谢你指出来。 - rsp
10
你知道如何只将此应用于特定路由吗? - Jorge L Hernandez
客户端的URL会是什么样子? - GGEv
我认为这是最佳答案。快速且有帮助。 - Igor Kurkov
显示剩余6条评论

59

在Express v4中,很多中间件从Express核心中移除并放入单独的模块中。基本认证模块在这里:https://github.com/expressjs/basic-auth-connect

您的示例只需要更改为:

var basicAuth = require('basic-auth-connect');
app.use(basicAuth('username', 'password'));

21
该模块声称已被弃用(尽管它提供的替代方案似乎不太令人满意)。 - Arnout Engelen
3
这段话的意思是:这个中间件的使用文档非常匮乏,没有任何实际应用的例子可供参考。虽然它可能适合作为中间件使用,但它的调用方式却无法获取。虽然提供的示例非常通用,但却无法提供有用的使用信息。 - Wylie Kulik
是的,这个已经被弃用了,虽然推荐使用的那个文档很少,但代码非常简单。https://github.com/jshttp/basic-auth/blob/master/index.js - Loourr
1
我已经在这个答案中描述了如何使用basic-auth库。 - Loourr
一个完整的模块如何存在于在代码中明文输入密码??至少通过使用base64进行比较来混淆似乎稍微好一些。 - user1944491

36

我使用原始的basicAuth代码找到了答案:

app.use(function(req, res, next) {
    var user = auth(req);

    if (user === undefined || user['name'] !== 'username' || user['pass'] !== 'password') {
        res.statusCode = 401;
        res.setHeader('WWW-Authenticate', 'Basic realm="MyRealmName"');
        res.end('Unauthorized');
    } else {
        next();
    }
});

12
此模块已被认为是废弃的,请使用 jshttp/basic-auth 替代(API相同,因此答案仍然适用)。 - Michael

34

我在 Express 4.0 中使用http-auth替换了基本身份验证,代码如下:

var auth = require('http-auth');

var basic = auth.basic({
        realm: "Web."
    }, function (username, password, callback) { // Custom authentication method.
        callback(username === "userName" && password === "password");
    }
);

app.get('/the_url', auth.connect(basic), routes.theRoute);

1
这真的是即插即用。非常棒。 - sidonaldson

21

似乎有多个模块可以实现此功能,其中一些已经不再使用。

以下这个看起来是活跃的:
https://github.com/jshttp/basic-auth

这是一个使用示例:

// auth.js

var auth = require('basic-auth');

var admins = {
  'art@vandelay-ind.org': { password: 'pa$$w0rd!' },
};


module.exports = function(req, res, next) {

  var user = auth(req);
  if (!user || !admins[user.name] || admins[user.name].password !== user.pass) {
    res.set('WWW-Authenticate', 'Basic realm="example"');
    return res.status(401).send();
  }
  return next();
};




// app.js

var auth = require('./auth');
var express = require('express');

var app = express();

// ... some not authenticated middlewares

app.use(auth);

// ... some authenticated middlewares

请确保将auth中间件放在正确的位置,该中间件之前的任何中间件都将无法进行身份验证。


我实际上支持'basic-auth-connect',虽然名称不好,但在功能上它比'basic-auth'更好。后者所做的只是解析授权标头。你仍然必须自己实现协议(即发送正确的标头)。 - FDIM
太好了!谢谢你。这个方法有效并且解释得非常清楚。 - Tania Rascia
我尝试了这个,但它只是不断地让我登录,形成了一个循环。 - jdog

7

安装express-basic-auth依赖项:

 npm i express-basic-auth

在创建应用程序时,请使用 auth 包

const app = require('express')();
const basicAuth = require('express-basic-auth');

并且像这样设置中间件:

app.use(basicAuth({
    users: { 'my-username': 'my-password' },
    challenge: true,
}));

7
function auth (req, res, next) {
  console.log(req.headers);
  var authHeader = req.headers.authorization;
  if (!authHeader) {
      var err = new Error('You are not authenticated!');
      res.setHeader('WWW-Authenticate', 'Basic');
      err.status = 401;
      next(err);
      return;
  }
  var auth = new Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
  var user = auth[0];
  var pass = auth[1];
  if (user == 'admin' && pass == 'password') {
      next(); // authorized
  } else {
      var err = new Error('You are not authenticated!');
      res.setHeader('WWW-Authenticate', 'Basic');      
      err.status = 401;
      next(err);
  }
}
app.use(auth);

1
希望这可以解决问题,但请附上代码的解释,以便用户真正理解他/她想要的内容。 - Jaimil Patel

6
我们可以在不需要任何模块的情况下实现基本授权。
//1.
var http = require('http');

//2.
var credentials = {
    userName: "vikas kohli",
    password: "vikas123"
};
var realm = 'Basic Authentication';

//3.
function authenticationStatus(resp) {
    resp.writeHead(401, { 'WWW-Authenticate': 'Basic realm="' + realm + '"' });
    resp.end('Authorization is needed');

};

//4.
var server = http.createServer(function (request, response) {
    var authentication, loginInfo;

    //5.
    if (!request.headers.authorization) {
        authenticationStatus (response);
        return;
    }

    //6.
    authentication = request.headers.authorization.replace(/^Basic/, '');

    //7.
    authentication = (new Buffer(authentication, 'base64')).toString('utf8');

    //8.
    loginInfo = authentication.split(':');

    //9.
    if (loginInfo[0] === credentials.userName && loginInfo[1] === credentials.password) {
        response.end('Great You are Authenticated...');
         // now you call url by commenting the above line and pass the next() function
    }else{

    authenticationStatus (response);

}

});
 server.listen(5050);

来源:- http://www.dotnetcurry.com/nodejs/1231/basic-authentication-using-nodejs

基本身份验证是一种简单的身份验证方法,它要求用户提供用户名和密码以访问受保护的资源。在Node.js中实现基本身份验证需要使用base64编码来处理用户名和密码。以下是一个示例应用程序,该应用程序使用基本身份验证来验证客户端请求是否具有适当的凭据。


1
Express已删除了此功能,现在建议您使用basic-auth库。
以下是如何使用的示例:
var http = require('http')
var auth = require('basic-auth')

// Create server
var server = http.createServer(function (req, res) {
  var credentials = auth(req)

  if (!credentials || credentials.name !== 'aladdin' || credentials.pass !== 'opensesame') {
    res.statusCode = 401
    res.setHeader('WWW-Authenticate', 'Basic realm="example"')
    res.end('Access denied')
  } else {
    res.end('Access granted')
  }
})

// Listen
server.listen(3000)

发送请求到这个路由,你需要包含一个基本认证格式的Authorization header

首先发送一个curl请求,你必须取得name:pass或在这种情况下是aladdin:opensesamebase64编码,它等于YWxhZGRpbjpvcGVuc2VzYW1l

你的curl请求将会像这样:

 curl -H "Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l" http://localhost:3000/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接