认证Express中间件(SSR)

Feathers身份验证还支持验证Express中间件的路由,并可用于服务器端呈现.此配方显示如何创建登录表单, /logout 端点和受保护的 /chat 端点,该端点呈现来自我们的所有用户和最近的聊天消息 chat/readme.

关键步骤是:

  1. 通过oAuth或本地身份验证流程获取JWT

  2. 在cookie中设置JWT(因为浏览器会在每次请求时发送它)

  3. 在需要保护的任何中间件之前,添加 cookieParser()authenticate('jwt') authentication Express中间件.这将从JWT中的用户信息设置 req.user, 或者如果没有JWT则显示401错误页面或它是无效的.

配置

为了使浏览器在每次请求时都发送JWT,必须在身份验证配置中启用cookie.

注解

如果您使用的是oAuth2,则已启用Cookie.

如果尚未启用,请将以下内容添加到 config/default.json 中的 authentication 部分:

"cookie": {
  "enabled": true,
  "name": "feathers-jwt"
}

我们希望通过向 /authentication 端点提交普通的HTML表单来使用用户名和密码登录进行身份验证.默认情况下,对该端点的成功POST将使用我们的JWT呈现JSON.这适用于REST API,但在我们的例子中,我们希望被重定向到受保护的页面.我们可以通过在 config/default.jsonauthentication 配置的 local 策略部分设置 successRedirect 来做到这一点:

"local": {
  "entity": "user",
  "usernameField": "email",
  "passwordField": "password",
  "successRedirect": "/chat"
}

设置中间件

../api/authentication/jwt 将在cookie中查找JWT,但只有解析cookie的路径才能访问它.这可以通过 cookie-parser Express中间件 来完成:

npm install cookie-parser

现在我们可以通过首先向链中添加 cookieParser(),authenticate('jwt') 来保护任何Express路由.

注解

只有在实际需要由cookie中的JWT保护的路由之前注册cookie解析器中间件才能防止CSRF安全问题.

由于我们想要在服务器上呈现视图,我们必须注册一个 Express模板引擎. 在本例中,我们将使用 EJS:

npm install ejs

接下来,我们可以将 src/middleware/index.js 更新为

  • 将视图引擎设置为EJS(Express中视图的默认文件夹是项目根目录中的 views/)

  • 注册一个 /login 路由,呈现 views/login.ejs

  • 注册一个protected ../api/application,然后渲染 views/chat.ejs

  • 注册一个 /logout 路由,删除cookie并重定向回登录页面

注解

我们也可以使用 feathers generate middleware 生成中间件,但由于它们都很短,我们现在可以将它保存在同一个文件中.

const cookieParser = require('cookie-parser');
const { authenticate } = require('@feathersjs/authentication').express;

module.exports = function (app) {
  // Use EJS as the view engine (using the `views` folder by default)
  app.set('view engine', 'ejs');

  // Render the /login view
  app.get('/login', (req, res) => res.render('login'));

  // Render the protected chat page
  app.get('/chat', cookieParser(), authenticate('jwt'), async (req, res) => {
    // `req.user` is set by `authenticate('jwt')`
    const { user } = req;
    // Since we are rendering on the server we have to pass the authenticated user
    // from `req.user` as `params.user` to our services
    const params = {
      user, query: {}
    };
    // Find the list of users
    const users = await app.service('users').find(params);
    // Find the most recent messages
    const messages = await app.service('messages').find(params);

    res.render('chat', { user, users, messages });
  });

  // For the logout route we remove the JWT from the cookie
  // and redirect back to the login page
  app.get('/logout', cookieParser(), (req, res) => {
    res.clearCookie('feathers-jwt');
    res.redirect('/login');
  });
};

注解

npm ls @feathersjs/authentication-jwt 必须显示已安装2.0.0或更高版本.

查看

登录表单必须向 /authentication 端点发出POST请求,并发送与任何其他API客户端相同的字段.在我们的案例中具体:

{
  "strategy": "local",
  "email": "user@example.com",
  "password": "mypassword"
}

emailpasswords 是正常的输入字段,我们可以将 strategy 添加为隐藏字段.表单必须向 /authentication 端点提交POST请求.由于服务可以接受JSON和URL编码形式,因此我们不需要做任何其他事情. views/login.ejs 的登录页面如下所示:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
  <title>Feathers chat login</title>
  <link rel="shortcut icon" href="favicon.ico">
  <link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/base.css">
  <link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/chat.css">
</head>
<body>
  <div id="app" class="flex flex-column">
    <main class="login container">
      <div class="row">
        <div class="col-12 col-6-tablet push-3-tablet text-center heading">
          <h1 class="font-100">Log in</h1>
        </div>
      </div>
      <div class="row">
        <div class="col-12 col-6-tablet push-3-tablet col-4-desktop push-4-desktop">
          <form class="form" method="post" action="/authentication">
            <input type="hidden" name="strategy" value="local">
            <fieldset>
              <input class="block" type="email" name="email" placeholder="email">
            </fieldset>

            <fieldset>
              <input class="block" type="password" name="password" placeholder="password">
            </fieldset>

            <button type="submit" id="login" class="button button-primary block signup">
              Log in
            </button>
          </form>
        </div>
      </div>
    </main>
  </div>
</body>
</html>

views/chat.ejs 页面有 users, user (经过身份验证的用户)和 messages 属性,我们在 /chat 中间件中传递了它们.渲染消息和用户看起来类似于 chat/frontend:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
  <title>Feathers chat</title>
  <link rel="shortcut icon" href="favicon.ico">
  <link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/base.css">
  <link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/chat.css">
</head>
<body>
  <div id="app" class="flex flex-column">
    <main class="flex flex-column">
      <header class="title-bar flex flex-row flex-center">
        <div class="title-wrapper block center-element">
          <img class="logo" src="http://feathersjs.com/img/feathers-logo-wide.png"
            alt="Feathers Logo">
          <span class="title">Chat</span>
        </div>
      </header>

      <div class="flex flex-row flex-1 clear">
        <aside class="sidebar col col-3 flex flex-column flex-space-between">
          <header class="flex flex-row flex-center">
            <h4 class="font-300 text-center">
              <span class="font-600 online-count">
                <%= users.total %>
              </span> users
            </h4>
          </header>

          <ul class="flex flex-column flex-1 list-unstyled user-list">
            <% users.data.forEach(user => { %><li>
              <a class="block relative" href="#">
                <img src="<%= user.avatar %>" alt="" class="avatar">
                <span class="absolute username"><%= user.email %></span>
              </a>
            </li><% }); %>
          </ul>
          <footer class="flex flex-row flex-center">
            <a href="/logout" id="logout" class="button button-primary">
              Sign Out
            </a>
          </footer>
        </aside>

        <div class="flex flex-column col col-9">
          <main class="chat flex flex-column flex-1 clear">
            <% messages.data.forEach(message => { %>
            <div class="message flex flex-row">
              <img src="<%= message.user && message.user.avatar %>"
                alt="<%= message.user && message.user.email %>" class="avatar">
              <div class="message-wrapper">
                <p class="message-header">
                  <span class="username font-600">
                    <%= message.user && message.user.email %>
                  </span>
                  <span class="sent-date font-300"><%= new Date(message.createdAt).toString() %></span>
                </p>
                <p class="message-content font-300"><%= message.text %></p>
              </div>
            </div>
            <% }); %>
          </main>
        </div>
      </div>
    </main>
  </div>
</body>
</html>

如果我们现在启动服务器(npm start)并转到 localhost:3030/login 我们可以看到登录页面.我们可以使用在 chat/frontend 中创建的用户之一的登录信息,一旦成功,我们将被重定向到 /chat, 显示所有当前消息和用户的列表,然后单击 Sign out 按钮会将我们注销并重定向到登录页面.