Xác thực trang web trong Node.js: Đăng ký người dùng
Vietnamese (Tiếng Việt) translation by Vy Cam Nguyen (you can also view the original English article)
Giới thiệu
Xác thực cũng quan trọng trong API, đây cũng là một tính năng quan trọng trong các ứng dụng web nhất định — những ứng dụng có trang và thông tin bí mật chỉ cho phép người dùng đã đăng ký và được xác thực truy cập.
Trong hướng dẫn này, bạn sẽ xây dựng một ứng dụng web đơn giản trong khi học cách tạo ra đăng ký người dùng.
Thiết lập ứng dụng
Tạo một thư mục mới mà bạn sẽ làm việc. Với mục đích của hướng dẫn này, tôi gọi ứng dụng của tôi là site-auth. Khởi tạo npm trong thư mục mới bạn vừa tạo. Đây là cách khởi tạo npm.
1 |
npm init -y
|
Flag -y cho phép npm sử dụng các tùy chọn mặc định.
Chỉnh sửa dependency (package phụ thuộc) của file package.json của bạn để trông giống với file của tôi.
1 |
#package.json |
2 |
|
3 |
{
|
4 |
"name": "site-auth", |
5 |
"version": "1.0.0", |
6 |
"description": "", |
7 |
"main": "app.js", |
8 |
"scripts": { |
9 |
"test": "echo \"Error: no test specified\" && exit 1" |
10 |
},
|
11 |
"keywords": [], |
12 |
"author": "izuchukwu1", |
13 |
"license": "ISC", |
14 |
"dependencies": { |
15 |
"bcryptjs": "^2.4.3", |
16 |
"body-parser": "^1.17.1", |
17 |
"connect-flash": "^0.1.1", |
18 |
"cookie-parser": "^1.4.3", |
19 |
"express": "^4.15.2", |
20 |
"express-handlebars": "^3.0.0", |
21 |
"express-messages": "^1.0.1", |
22 |
"express-session": "^1.15.2", |
23 |
"joi": "^13.0.1", |
24 |
"mongoose": "^4.11.12", |
25 |
"morgan": "^1.8.1", |
26 |
"passport": "^0.4.0", |
27 |
"passport-local": "^1.0.0" |
28 |
}
|
29 |
}
|
Khi hoàn thành, chạy câu lệnh để cài đặt dependency.
1 |
npm install
|
Tạo file mới trong thư mục làm việc của bạn có tên app.js.
Bắt đầu bằng cách yêu cầu dependency bạn đã cài đặt và các file cần thiết.
1 |
#app.js |
2 |
|
3 |
const express = require('express'); |
4 |
const morgan = require('morgan') |
5 |
const path = require('path'); |
6 |
const cookieParser = require('cookie-parser'); |
7 |
const bodyParser = require('body-parser'); |
8 |
const expressHandlebars = require('express-handlebars'); |
9 |
const flash = require('connect-flash'); |
10 |
const session = require('express-session'); |
11 |
const mongoose = require('mongoose') |
12 |
const passport = require('passport') |
13 |
|
14 |
require('./config/passport') |
Những dependency này đã được cài đặt khi chạy lệnh npm install. Để sử dụng chúng trong ứng dụng của bạn, bạn phải request (yêu cầu) và save (lưu) chúng lại trong các biến tương ứng .
Trong hướng dẫn này, bạn sẽ sử dụng MongoDB làm database của bạn. Bạn sẽ lưu trữ thông tin người dùng trong database. Để làm việc cùng MongoDB, bạn sẽ sử dụng Mongoose - một công cụ mô hình hóa MongoDB cho Node.js. Việc thiết lập Mongoose thật dễ dàng, giống như sau:
1 |
#app.js |
2 |
|
3 |
mongoose.Promise = global.Promise |
4 |
mongoose.connect('mongodb://localhost:27017/site-auth')
|
Ngay lúc này, chúng ta hãy thiết lập middle.
1 |
// 1
|
2 |
const app = express() |
3 |
app.use(morgan('dev')) |
4 |
|
5 |
// 2
|
6 |
app.set('views', path.join(__dirname, 'views')) |
7 |
app.engine('handlebars', expressHandlebars({ defaultLayout: 'layout' })) |
8 |
app.set('view engine', 'handlebars') |
9 |
|
10 |
// 3
|
11 |
app.use(bodyParser.json()) |
12 |
app.use(bodyParser.urlencoded({ extended: false })) |
13 |
app.use(cookieParser()) |
14 |
app.use(express.static(path.join(__dirname, 'public'))) |
15 |
app.use(session({ |
16 |
cookie: { maxAge: 60000 }, |
17 |
secret: 'codeworkrsecret', |
18 |
saveUninitialized: false, |
19 |
resave: false |
20 |
}));
|
21 |
|
22 |
app.use(passport.initialize()) |
23 |
app.use(passport.session()) |
24 |
|
25 |
// 4
|
26 |
app.use(flash()) |
27 |
app.use((req, res, next) => { |
28 |
res.locals.success_mesages = req.flash('success') |
29 |
res.locals.error_messages = req.flash('error') |
30 |
next() |
31 |
})
|
32 |
|
33 |
// 5
|
34 |
app.use('/', require('./routes/index')) |
35 |
app.use('/users', require('./routes/users')) |
36 |
|
37 |
// 6
|
38 |
// catch 404 and forward to error handler
|
39 |
app.use((req, res, next) => { |
40 |
res.render('notFound') |
41 |
});
|
42 |
|
43 |
// 7
|
44 |
app.listen(5000, () => console.log('Server started listening on port 5000!')) |
- Express được khởi tạo và gán vào
app. - Middleware dùng xử lý các view được thiết lập. Đối với các view, bạn sẽ sử dụng
handlebars. - Bạn thiết lập middleware cho
bodyparser,cookie,sessionvàpassport. Passport sẽ được sử dụng khi người dùng muốn đăng nhập. - Tại một số điểm, bạn sẽ hiển thị các thông điệp nhanh (flash). Vì vậy, bạn cần phải thiết lập middleware cho việc này, và cũng tạo ra các kiểu thông điệp flash mà bạn muốn.
- Routes middleware sẽ xử lý mọi yêu cầu đến đường dẫn URL. Đường dẫn URL được chỉ định ở đây dùng cho index và đường dẫn người dùng.
- Middleware dùng xử lý lỗi 404. Middleware này khởi động khi có một yêu cầu không tham chiếu đến bất kỳ middleware nào được tạo ra ở bên trên.
- Máy chủ được thiết lập ở cổng 5000.
Thiết lập các View
Tạo một thư mục mới có tên là views. Bên trong thư mục views, tạo hai thư mục khác là layouts và partials. Bạn muốn đạt được một cấu trúc cây như thế này trong view của bạn, vậy hãy tạo các file cần thiết trong các thư mục tương ứng của chúng.
1 |
├── dashboard.handlebars |
2 |
├── index.handlebars |
3 |
├── layouts |
4 |
│ └── layout.handlebars |
5 |
├── login.handlebars |
6 |
├── notFound.handlebars |
7 |
├── partials |
8 |
│ └── navbar.handlebars |
9 |
└── register.handlebars |
Khi hoàn thành, giờ là lúc viết code.
1 |
#dashboard.handlebars |
2 |
|
3 |
<!-- Jumbotron -->
|
4 |
<div class="jumbotron"> |
5 |
<h1>User DashBoard</h1> |
6 |
</div>
|
Đây là trang dashboard (tổng quan) chỉ hiển thị với người dùng đã đăng ký. Trong với hướng dẫn này, nó sẽ là trang bí mật của bạn.
Bây giờ trang index cho ứng dụng sẽ trông thế này.
1 |
#index.handlebars |
2 |
|
3 |
<!-- Jumbotron -->
|
4 |
<div class="jumbotron"> |
5 |
<h1>Site Authentication!</h1> |
6 |
<p class="lead">Welcome aboard.</p> |
7 |
</div>
|
Ứng dụng cần một bố cục sẽ để sử dụng và đây là bố cục bạn sẽ sử dụng.
1 |
#layout/layout.handlebars |
2 |
|
3 |
<!DOCTYPE html>
|
4 |
<html>
|
5 |
<head>
|
6 |
<title>Site Authentication</title> |
7 |
<link rel="stylesheet" |
8 |
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" |
9 |
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" |
10 |
crossorigin="anonymous"> |
11 |
<link rel="stylesheet" |
12 |
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" |
13 |
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" |
14 |
crossorigin="anonymous"> |
15 |
<link rel="stylesheet" |
16 |
href="/css/style.css"> |
17 |
</head>
|
18 |
<body>
|
19 |
{{#if success_messages }}
|
20 |
<div class="alert alert-success">{{success_messages}}</div> |
21 |
{{/if}}
|
22 |
{{#if error_messages }}
|
23 |
<div class="alert alert-danger">{{error_messages}}</div> |
24 |
{{/if}}
|
25 |
<div class="container"> |
26 |
{{> navbar}}
|
27 |
{{{body}}}
|
28 |
</div>
|
29 |
|
30 |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" |
31 |
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" |
32 |
crossorigin="anonymous"></script> |
33 |
</body>
|
34 |
</html>
|
Bạn sẽ cần một trang login (đăng nhập) cho người dùng đã đăng ký.
1 |
#views/login.handlebars |
2 |
|
3 |
<form class="form-signin" action="/users/login" method="POST"> |
4 |
<h2 class="form-signin-heading">Please sign in</h2> |
5 |
|
6 |
<label for="inputEmail" class="sr-only">Email address</label> |
7 |
<input type="email" id="inputEmail" name="email" class="form-control" placeholder="Email address" required autofocus> |
8 |
|
9 |
<label for="inputPassword" class="sr-only">Password</label> |
10 |
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required> |
11 |
|
12 |
<br/>
|
13 |
|
14 |
<button class="btn btn-lg btn-default btn-block" type="submit">Sign in</button> |
15 |
</form>
|
File notFound.handlebars sẽ được sử dụng làm trang lỗi của bạn.
1 |
#views/notFound.handlebars |
2 |
|
3 |
<!-- Jumbotron -->
|
4 |
<div class="jumbotron"> |
5 |
<h1>Error</h1> |
6 |
</div>
|
Trang đăng ký của bạn được sẽ như thế này.
1 |
<form class="form-signin" action="/users/register" method="POST"> |
2 |
<h2 class="form-signin-heading">Please sign up</h2> |
3 |
|
4 |
<label for="inputEmail" class="sr-only">Email address</label> |
5 |
<input type="email" id="inputEmail" name="email" class="form-control" placeholder="Email address" required autofocus> |
6 |
|
7 |
<label for="inputUsername" class="sr-only">Username</label> |
8 |
<input type="text" id="inputUsername" name="username" class="form-control" placeholder="Username" required> |
9 |
|
10 |
<label for="inputPassword" class="sr-only">Password</label> |
11 |
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required> |
12 |
|
13 |
<label for="inputConfirmPassword" class="sr-only">Confirm Password</label> |
14 |
<input type="password" id="inputConfirmPassword" name="confirmationPassword" class="form-control" placeholder="Confirm Password" required> |
15 |
|
16 |
<br/>
|
17 |
|
18 |
<button class="btn btn-lg btn-default btn-block" type="submit">Sign up</button> |
19 |
</form>
|
Cuối cùng cho các view của bạn, đây là thanh điều hướng của bạn.
1 |
#partials/navbar.handlebars |
2 |
|
3 |
<div class="masthead"> |
4 |
<h3 class="text-muted">Site Authentication</h3> |
5 |
<nav>
|
6 |
<ul class="nav nav-justified"> |
7 |
<li class="active"><a href="/">Home</a></li> |
8 |
{{#if isAuthenticated}}
|
9 |
<li><a href="/users/dashboard">Dashboard</a></li> |
10 |
<li><a href="/users/logout">Logout</a></li> |
11 |
{{else}}
|
12 |
<li><a href="/users/register">Sign Up</a></li> |
13 |
<li><a href="/users/login">Sign In</a></li> |
14 |
{{/if}}
|
15 |
</ul>
|
16 |
</nav>
|
17 |
</div>
|
Đã xong phần đó, bạn có thể tiếp tục các phần khó hơn.
Xác nhận dữ liệu
Bạn sẽ cần một model User. Từ code của view ở trên, bạn có thể suy ra rằng các thuộc tính cần thiết cho model User là email, username và password. Tạo một thư mục models và một file trong đó với tên gọi là user.js.
1 |
#models/user.js |
2 |
|
3 |
// 1
|
4 |
const mongoose = require('mongoose') |
5 |
const Schema = mongoose.Schema |
6 |
const bcrypt = require('bcryptjs') |
7 |
|
8 |
// 2
|
9 |
const userSchema = new Schema({ |
10 |
email: String, |
11 |
username: String, |
12 |
password: String |
13 |
}, { |
14 |
|
15 |
// 3
|
16 |
timestamps: { |
17 |
createdAt: 'createdAt', |
18 |
updatedAt: 'updatedAt' |
19 |
}
|
20 |
})
|
21 |
|
22 |
// 4
|
23 |
const User = mongoose.model('user', userSchema) |
24 |
module.exports = User |
- Import các dependency và lưu chúng trong các biến.
- Schema mới được tạo ra. Đối với mỗi người dùng, bạn muốn lưu
email,usernamevàpasswordvào database. Schema cho thấy cách model được xây dựng cho mỗi tài liệu. Ở đây bạn muốn email, username và password thuộc kiểu dự liệu String. - Đối với người dùng được lưu vào database, bạn cũng muốn tạo
timestamp. Bạn sử dụng Mongoose để lấycreatedAtvàupdatedAt, và sau đó được lưu vào database. - Mô hình được định nghĩa và gán cho một biến được gọi là
User, sau đó được export một mô-đun để có thể được tiếp tục sử dụng trong các phần khác của ứng dụng.
Salting và Hashing mật khẩu
Bạn không muốn lưu trữ mật khẩu của người dùng dưới dạng văn bản thuần túy. Dưới đây là điều bạn muốn làm khi người dùng nhập mật khẩu thuần văn bản khi đăng ký. Mật khẩu văn bản thuần túy sẽ được hash bằng cách sử dụng một salt được tạo ra từ ứng dụng của bạn (sử dụng bcryptjs). Mật khẩu hash này sau đó được lưu trữ trong database.
Nghe thật hay, phải không? Hãy thực hiện điều đó trong file user.js.
1 |
#models/user.js |
2 |
|
3 |
module.exports.hashPassword = async (password) => { |
4 |
try { |
5 |
const salt = await bcrypt.genSalt(10) |
6 |
return await bcrypt.hash(password, salt) |
7 |
} catch(error) { |
8 |
throw new Error('Hashing failed', error) |
9 |
}
|
10 |
}
|
Bạn vừa tạo một phương thức sẽ được gọi trong các sự kiện đăng ký người dùng. Phương thức này sẽ nhận mật khẩu văn bản thuần túy mà người dùng đã nhập. Như tôi đã đề cập trước đó, mật khẩu thuần văn bản sẽ được băm bằng cách sử dụng một muối được tạo ra. Mật khẩu băm sẽ được trả về như mật khẩu cho người dùng.
Index và Route cho người dùng
Tạo một thư mục mới được gọi là routes. Trong thư mục mới này, tạo hai file mới: index.js và users.js.
File index.js rất đơn giản. Nó sẽ tham chiếu tới index của ứng dụng của bạn. Hãy nhớ rằng bạn thiết lập middleware cho các route trong file app.js của mình khi bạn làm điều này.
1 |
app.use('/', require('./routes/index')) |
2 |
app.use('/users', require('./routes/users')) |
Vì vậy, các route cho index của bạn đơn giản là hiển thị trang index, sẽ trông như sau.
1 |
#routes/index.js |
2 |
|
3 |
const express = require('express') |
4 |
const router = express.Router() |
5 |
|
6 |
router.get('/', (req, res) => { |
7 |
res.render('index') |
8 |
})
|
9 |
|
10 |
module.exports = router |
Bây giờ đến route cho user (người dùng). Hiện tại, file route này sẽ làm 4 nhiệm vụ.
- Yêu cầu các dependency. Bạn sẽ cần phải yêu cầu dependency bạn đã cài đặt bằng cách sử dụng NPM.
- Xác thực input (dữ liệu nhập) của người dùng. Bạn muốn đảm bảo rằng người dùng không gửi biểu mẫu trống. Tất cả input là bắt buộc và tất cả phải là kiểu String. Email có xác nhận đặc biệt có tên gọi là
.email(), đảm bảo rằng nội dung được nhập phù hợp với định dạng email, trong khi password được xác thực bằng cách sử dụng regular expression. Đối với pasword đã xác nhận, bạn muốn nó giống như mật khẩu đã được nhập. Joi sẽ xử lý các xác nhận này. - Thiết lập router của bạn. Yêu cầu GET hiển thị trang đăng ký, trong khi yêu cầu POST khởi động khi người dùng nhấn nút để submit của form.
- Router được export dưới dạng mô-đun.
Code trông giống thế này.
1 |
#routes/users.js |
2 |
|
3 |
const express = require('express'); |
4 |
const router = express.Router() |
5 |
const Joi = require('joi') |
6 |
const passport = require('passport') |
7 |
|
8 |
const User = require('../models/user') |
9 |
|
10 |
|
11 |
//validation schema
|
12 |
|
13 |
const userSchema = Joi.object().keys({ |
14 |
email: Joi.string().email().required(), |
15 |
username: Joi.string().required(), |
16 |
password: Joi.string().regex(/^[a-zA-Z0-9]{6,30}$/).required(), |
17 |
confirmationPassword: Joi.any().valid(Joi.ref('password')).required() |
18 |
})
|
19 |
|
20 |
router.route('/register') |
21 |
.get((req, res) => { |
22 |
res.render('register') |
23 |
})
|
24 |
.post(async (req, res, next) => { |
25 |
try { |
26 |
const result = Joi.validate(req.body, userSchema) |
27 |
if (result.error) { |
28 |
req.flash('error', 'Data entered is not valid. Please try again.') |
29 |
res.redirect('/users/register') |
30 |
return
|
31 |
}
|
32 |
|
33 |
const user = await User.findOne({ 'email': result.value.email }) |
34 |
if (user) { |
35 |
req.flash('error', 'Email is already in use.') |
36 |
res.redirect('/users/register') |
37 |
return
|
38 |
}
|
39 |
|
40 |
const hash = await User.hashPassword(result.value.password) |
41 |
|
42 |
delete result.value.confirmationPassword |
43 |
result.value.password = hash |
44 |
|
45 |
const newUser = await new User(result.value) |
46 |
await newUser.save() |
47 |
|
48 |
req.flash('success', 'Registration successfully, go ahead and login.') |
49 |
res.redirect('/users/login') |
50 |
|
51 |
} catch(error) { |
52 |
next(error) |
53 |
}
|
54 |
})
|
55 |
|
56 |
module.exports = router |
Hãy nhìn kỹ hơn điều gì đang xảy ra trong yêu cầu POST.
Các giá trị được nhập trong form đăng ký có thể truy cập qua req.body và các giá trị trông giống thế này.
1 |
value: |
2 |
{ email: 'chineduizuchkwu1@gmail.com', |
3 |
username: 'izu', |
4 |
password: 'chinedu', |
5 |
confirmationPassword: 'chinedu' }, |
Điều này được xác thực bằng cách sử dụng userSchema mà bạn đã tạo ở trên và các giá trị được người dùng nhập vào được gán cho một biến được gọi là result.
Nếu gặp lỗi do xác thực, một thông báo lỗi được hiển thị tới người dùng và xuất hiện sự chuyển hướng đến trang đăng ký.
Mặt khác, chúng tôi cố gắng tìm xem liệu người dùng với cùng email có đang tồn tại hay không, vì bạn không muốn có hai hoặc nhiều người dùng có cùng địa chỉ email. Nếu người dùng được tìm thấy, họ được thông báo rằng địa chỉ email đã được sử dụng.
Trong trường hợp chưa có người dùng đã đăng ký có địa chỉ email đó, tiếp theo là hash mật khẩu. Đây là lúc bạn gọi phương thức hashPassword được tạo trong file user.js của bạn. Mật khẩu vừa hash được gán cho một biến được gọi là hash.
Không cần phải lưu trữ các confirmationPassword trong database. Vì vậy, nó sẽ bị xóa. Mật khẩu có sẵn từ kết quả vẫn là mật khẩu thuần tuý. Vì bạn không muốn lưu trữ mật khẩu thuần tuý trong database của mình, nên điều quan trọng là gán lại giá trị mật khẩu cho hash được tạo ra. Điều này được thực hiện bằng một dòng code.
1 |
result.value.password = hash |
Đối tượng người dùng mới được lưu vào database. Hiển thị thông báo nhanh cho biết đã đăng ký thành công và người dùng được chuyển hướng đến trang login.
Khởi động server bằng terminal bằng cách chạy:
1 |
node app.js |
Mở trình duyệt của bạn với địa chỉ http://localhost:5000 và bạn sẽ thấy ứng dụng mới của mình.
Tổng kết
Bây giờ bạn đã biết làm sao để triển khai tính năng đăng ký trong ứng dụng web Node. Bạn đã biết được tầm quan trọng của việc xác thực dữ liệu đầu vào của người dùng và cách thực hiện điều đó bằng Joi. Bạn cũng đã sử dụng bcryptjs để salt và hash mật khẩu của bạn.
Tiếp theo, bạn sẽ thấy làm thế nào để triển khai tính năng đăng nhập cho người dùng đã đăng ký. Tôi tin rằng bạn rất thích!



