Logic đăng nhập phụ trợ Node.js

Oct 12 2020

Vì vậy, tôi đã viết một logic đăng nhập khá nguyên thủy trong Node.js, logic này xác thực người dùng và xử lý JWT. Nó có tốt không về mặt bảo mật, hiệu quả, xây dựng, không đồng bộ / đồng bộ hóa, ghi nhật ký. BẢO MẬT là mối quan tâm chính của tôi. Ở định dạng đẹp hơn, câu hỏi sẽ là:

  • BẢO MẬT : Trang web của tôi có bảo mật theo bất kỳ cách nào, hình dạng hoặc hình thức nào không? Tôi đang tự hỏi liệu tôi có thể triển khai bất kỳ biện pháp bảo mật nào ngoài những biện pháp được tích hợp sẵn với các phương pháp được cung cấp hay không Node.js. Ngoài ra, tôi biết mật khẩu là hiển nhiên để đoán, nhưng chúng như vậy để đảm bảo đăng nhập khi những người dùng khác nhau hoạt động.
  • HIỆU QUẢ : Cách tôi kiểm tra tên người dùng và mật khẩu có hiệu quả không? Có cách nào tốt hơn để làm điều này không?
  • XÂY DỰNG : Cách tôi tải trang web của mình có được chấp nhận không? Đọc từ một tệp và sau đó kết thúc phản hồi?
  • ASYNC / SYNC : Tôi biết tôi chuẩn bị asyncvà syncgọi cùng một lúc. Có vấn đề gì không?
  • ĐĂNG NHẬP : Tôi ghi lại tất cả các kết nối đến máy chủ và tất cả các lần đăng nhập. Đây có phải là một cách thực hành tốt hay tôi đang làm quá mức những gì mà việc ghi nhật ký phải đạt được? (Nguồn: Máy chủ đăng nhập bằng Node.js )

Mã của tôi là:

// main login logic
app.post('/api/login', apiLimiter, async function (req, res) {
  // TODO: implement cookie check whether a still valid token is set and if so respond with cookie already set
  // TODO: add roles into jwt and add roles checking into other code
  // TODO: if wrong password send a response telling there's a wrong password/username
  try  {
    const pw = req.body.password;
    const submittedUser = req.body.email;
    
    User.findOne({eMail: req.body.email}, async function (err, user) {
      if (err) throw err;
      console.log(user);
      const match = await bcrypt.compare(pw, user.password);
      console.log(match);
      if (match === true && user.eMail == submittedUser) {
        jwt2.sign({user}, 'secrettokenhere', { expiresIn: '15min'}, (err, token) =>{
          res.cookie(
            'access_token', 'Bearer '+ token, {
              //domain: 'localhost',
              path: '/',
              expires: new Date(Date.now() + 900000), // cookie will be removed after 15 mins
              httpOnly: true // in production also add secure: true
            })
            .json(
              user
            );
        });
      }
      else {
        res.status(200).send("Bad login");
      }
    });    
  } catch (err) {
    res.status(500).send();
    console.log(err);
  }
});

Tái bút sẽ có một câu hỏi riêng tiếp theo với logic Frontend.

Trả lời

6 CertainPerformance Oct 12 2020 at 17:59

Async lỗi đăng nhập Các try/ catchxung quanh User.findOnekhông thực hiện bất cứ điều gì vì findOnelà không đồng bộ. Khi findOnekhông thành công, nó sẽ chuyển lỗi cho lệnh gọi lại, nhưng khi lệnh gọi lại xảy ra throw err, không có gì để bắt lỗi không đồng bộ, vì vậy sẽ không có phản hồi nào được gửi đến người dùng. Một vấn đề khác là bạn không kiểm tra xem usercó tồn tại hay không - nếu không , lỗi sẽ xuất hiện khi bạn cố gắng truy cập thuộc tính của nó password. (Bạn cũng không kiểm tra xem .signkết quả có gây ra lỗi hay không)

Bạn cũng có thể cân nhắc sử dụng Promises thay vì gọi lại - findOneđã trả về Promise và Promise thường được ưu tiên hơn vì chúng dễ dàng xử lý hơn và việc xử lý lỗi có thể sạch hơn.

An ninh có vẻ hợp lý, mặc dù:

  • Đảm bảo kết nối chỉ được phép qua HTTPS. Yêu cầu HTTP có thể bị chặn.
  • Ngay cả khi lỗi đăng nhập ở trên đã được khắc phục, việc triển khai hiện tại của bạn có thể thông báo cho bất kỳ ai biết liệu một email nhất định đã được đăng ký hay chưa. Ví dụ: nếu tôi biết địa chỉ email của bạn, tôi có thể thử đăng nhập với tư cách là bạn và kiểm tra phản hồi để xem liệu mật khẩu có sai hay không hoặc không tồn tại email như vậy. Điều này có thể không được mong muốn. Nếu bạn muốn sửa nó, hãy nhập vào khối khi mật khẩu không khớp giống như khi không tìm thấy email, để người dùng không thể phân biệt được - bạn muốn gửi thư trả lời Bad logintrong cả hai trường hợp.
  • Một lưu ý tương tự, khi xác thực không thành công, mã trạng thái thích hợp để gửi là 401 (trái phép), không phải 200.

Cách tôi kiểm tra tên người dùng và mật khẩu có hiệu quả không?

Trông tôi hoàn toàn bình thường.

ĐĂNG NHẬP: Tôi ghi lại tất cả các kết nối đến máy chủ và tất cả các lần đăng nhập. Đây có phải là một thực hành tốt

Nếu một người đang ghi nhật ký, các nỗ lực đăng nhập là một trong những điều quan trọng nhất để ghi nhật ký. Nhưng console.logkhông phải là cách thích hợp để làm điều đó, ít nhất là không đơn độc - giả sử rằng một số người dùng lo lắng về thông tin đăng nhập của họ, bạn sẽ kiểm tra những lần đăng nhập gần đây của họ như thế nào? Control-F-ing thông qua trình duyệt ứng dụng không phải là cách dễ quản lý để thực hiện. Tôi không chắc tiêu chuẩn ngành cho việc này là gì, nhưng bạn có thể cân nhắc lưu vào cơ sở dữ liệu ghi nhật ký.

Đăng nhập email Bạn thực hiện:

const pw = req.body.password;
const submittedUser = req.body.email;

User.findOne({eMail: req.body.email}, async function (err, user) {
  if (err) throw err;
  console.log(user);
  const match = await bcrypt.compare(pw, user.password);
  console.log(match);
  if (match === true && user.eMail == submittedUser) {

Bạn đặt email yêu cầu vào một biến có tên submittedUser, điều này nghe có vẻ không trực quan đối với tôi - tốt hơn là sử dụng một tên biến chỉ ra rằng nó chứa một chuỗi email, không phải người dùng, chẳng hạn như email- bạn có thể hủy cả hai cùng một lúc với:

const { password, email } = req.body;

Sau đó, sử dụng các biến đó thay vì duyệt req.bodylại.

Sau khi .findOne, không cần phải làm gì cả user.eMail == submittedUser- việc kiểm tra đó phải là findOnetuyệt vời với sự ràng buộc của cuộc gọi.

Phần thân chứa email, nhưng cơ sở dữ liệu chứa eMail. Cách viết hoa khác nhau, đó là một sự không nhất quán kỳ lạ có thể gây ra lỗi chính tả và lỗi. Tôi khuyên bạn nên sử dụng một tên thuộc tính duy nhất - có thể là vậy email, vì cách viết hoa đó phổ biến hơn nhiều.

Thay vì so sánh kết quả phù hợp === true, bạn chỉ có thể kiểm tra xem kết quả phù hợp có trung thực hay không - hoặc, một tùy chọn khác, để giảm bớt sự thụt lề không cần thiết sẽ là bỏ qua nếu kết quả phù hợp không trung thực và xử lý lỗi trong a .catch.

Vì .signlà dựa trên cuộc gọi lại, nhưng bạn muốn làm việc với Promises, hãy đặt nó dựa trên Promise util.promisify.

Nhìn chung, tôi hy vọng sẽ làm cho mã trông giống như sau:

const jwtSignPromisified = util.promisify(jwt2.sign).bind(jwt2);
app.post('/api/login', apiLimiter, async function (req, res) {
    const failLogin = () => {
        logLoginAttempt(email, false); // or something - 2nd param indicates success
        // could also pass IP address
        res.status(401).send('Bad login');
    };
    const { password, email } = req.body;
    try {
        const user = await User.findOne({ email });
        if (!user) return failLogin();
        const match = await bcrypt.compare(password, user.password);
        if (!match) return failLogin();
        logLoginAttempt(email, true);
        const token = await jwtSignPromisified({ user }, 'secrettokenhere', { expiresIn: '15min' });
        res.cookie(
            'access_token', 'Bearer ' + token, {
            //domain: 'localhost',
            path: '/',
            expires: new Date(Date.now() + 900000), // cookie will be removed after 15 mins
            httpOnly: true // in production also add secure: true
        })
            .json(user);
    } catch (error) {
        // This should not be entered under normal circumstances:
        logServerError(error);
        res.status(500).send('Unexpected server error');
    }
});

ở đâu logLoginAttemptvà logServerErrorlưu vào cơ sở dữ liệu ghi nhật ký có thể được kiểm tra.