一文读懂HTTP Basic身份认证

18,455 阅读4分钟

Basic认证简介

在网络活动中,身份认证是非常重要的一环。Basic身份认证,是HTTP 1.0中引入的认证方案之一。虽然方案比较古老,同时存在安全缺陷,但由于实现简单,至今仍有不少网站在使用它。

本文通过实例,介绍Basic认证协议是如何实现的。同时,探讨Basic认证存在的安全缺陷。最后,附上Basic认证的服务端代码。

核心概念

Basic认证通过核对用户名、密码的方式,来实现用户身份的验证。

Basic认证中,最关键的4个要素:

  1. userid:用户的id。也就是我们常说的用户名。
  2. password:用户密码。
  3. realm:“领域”,其实就是指当前认证的保护范围。

同一个server,访问受限的资源多种多样,比如资金信息、机密文档等。可以针对不同的资源定义不同的 realm,并且只允许特定的用户访问。

跟Linux下的账户、分组体系很像,如下例子所示。

Basic认证实例

下面通过实例来讲解Basic认证是如何实现的,一共分4个步骤。假设:

  1. 用户访问的资源:/protected_docs
  2. 用户名、密码:chyingp、123456

步骤1:用户访问受限资源

如下,用户访问受限资源 /protected_docs。请求报文如下:

GET /protected_docs HTTP/1.1
Host: 127.0.0.1:3000

步骤2:服务端返回401要求身份认证

服务端发现 /protected_docs 为受限资源,于是向用户发送401状态码,要求进行身份认证。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm=protected_docs

响应首部中,通过WWW-Authenticate告知客户端,认证的方案是basic。同时以realm告知认证的范围。

WWW-Authenticate: Basic realm=<需要保护资源的范围>

步骤3:用户发送认证请求

用户收到服务端响应后,填写用户名、密码,然后向服务端发送认证请求。

以下为请求报文。Authorization请求首部中,包含了用户填写的用户名、密码。

GET /protected_docs HTTP/1.1
Authorization: Basic Y2h5aW5ncDoxMjM0NTY=

Authorization首部的格式为Basic base64(userid:password)。实际代码如下:

Buffer.from('chyingp:123456').toString('base64'); // Y2h5aW5ncDoxMjM0NTY=

步骤4:服务端验证请求

服务端收到用户的认证请求后,对请求进行验证。验证包含如下步骤:

  1. 根据用户请求资源的地址,确定资源对应的realm。
  2. 解析 Authorization 请求首部,获得用户名、密码。
  3. 判断用户是否有访问该realm的权限。
  4. 验证用户名、密码是否匹配。

一旦上述验证通过,则返回请求资源。如果验证失败,则返回401要求重新认证,或者返回403(Forbidden)。

安全缺陷

Basic认证的安全缺陷比较明显,它通过明文传输用户的密码,这会导致严重的安全问题。

  1. 在传输层未加密的情况下,用户明文密码可被中间人截获。
  2. 明文密码一旦泄露,如果用户其他站点也用了同样的明文密码(大概率),那么用户其他站点的安全防线也告破。

关于上述问题的建议:

  1. 传输层未加密的情况下,不要使用Basic认证。
  2. 如果使用Basic认证,登录密码由服务端生成。
  3. 如果可能,不要使用Basic认证。

除了安全缺陷,Basic认证还存在无法吊销认证的情况。

服务端代码示例

服务端代码如下,比较简单,这里不展开,有问题可留言交流。完整代码可 点击这里

const express = require('express');
const app = express();

const realms = [
  { realm: 'protected_docs', path: '/protected_docs', users: ['chyingp'] }
];

const users = [
  { usrname: 'chyingp', passwd: '123456' }
];

// 检查资源路径对应的realm,比如 path:'/protected_docs' => realm:'protected_docs'
function findRealm (path) {
  return realms.find(item => path.indexOf(item.path) !== -1);
}

// 根据用户名、密码,查找用户
function findUser (usrname, passwd) {
  return users.find(user => user.usrname === usrname && user.passwd === passwd);
}

// 判断用户是否在realm里
function isUserInRealm (realmItem, usrname) {
  return realmItem.users.indexOf(usrname) !== -1;
}

function notAuthorized (res) {  
  res.status = 403;
  res.end();
}

const protectedPath = '/protected_docs';

app.get(protectedPath, (req, res, next) => {

  const realmItem = findRealm(protectedPath);
  const realm = realmItem.realm; // 这里是 protected_docs
  const authorization = req.get('authorization');

  if (authorization) { // 身份认证

    const usernamePasswd = authorization.split(' ')[1]; // Basic Y2h5aW5ncDoxMjM0NTY
    const [usrname, passwd] = Buffer.from(usernamePasswd, 'base64').toString().split(':');

    if (isUserInRealm(realmItem, usrname) === false) { // 用户不在realm里
      return notAuthorized(res);
    }

    const user = findUser(usrname, passwd);

    if (!user) { // 用户账号、密码验证不通过
      return notAuthorized(res);
    }

    res.end(`welecom ${usrname}`);

  } else { // 告知用户需要身份认证

    res.statusCode = 401;
    res.set('WWW-Authenticate', 'Basic realm=' + encodeURIComponent(realm));
    res.end();
  }  
});

app.listen(3000);

参考链接

HTTP Authentication: Basic and Digest Access Authentication

Security Considerations

关于作者:程序猿小卡,前腾讯高级工程师,现任前海云汉金融科技前端技术负责人。专注技术架构、技术分享、项目管理。