HomeOur Team
Xây dựng ứng dụng Authentication sử dụng Laravel & Nextjs

Xây dựng ứng dụng Authentication sử dụng Laravel & Nextjs

By chung.nguyen1
Published in Solutions
October 20, 2022
4 min read

Trong bài viết này, chúng ta sẽ xây dựng ứng dụng đăng ký, đăng nhập đơn giản bằng Laravel và Nextjs

Cài đặt Laravel

Chạy lệnh git clone https://github.com/laravel/laravel next-auth-api

Cài đặt thư viện

Để cài đặt thư viện cho dự án chúng ta chạy câu lệnh

composer install

Tạo file .env

Chúng ta cần tạo file .env để lưu thông tin cấu hình của dự án. File .env sẽ được đặt ở thư mục gốc của dự án, ngang cấp với file .env.example, sau khi tạo xong thì copy nội dung file .env.example sang file .env như sau:

APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

Tạo secret key

Chạy lệnh sau để tạo secret key cho dự án:

php artisan key:generate

Cài đặt thư viện tymon/jwt-auth

Để xây dựng ứng dụng Authentication thông qua api chúng ta cần sử dụng thư viện tymon/jwt-auth. Để cài đặt thư viện này ta vào file composer.json vào thêm đoạn "tymon/jwt-auth": "^1.0.2" vào phần require như ảnh sau:

{
"name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.",
"keywords": ["framework", "laravel"],
"license": "MIT",
"require": {
"php": "^8.0.2",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^9.19",
"laravel/sanctum": "^3.0",
"laravel/tinker": "^2.7",
"tymon/jwt-auth": "^1.0.2"
}
...
}

Thêm service provider

Thêm service provider vào mảng providers trong file config/app.php:

'providers' => [
...
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]

Tạo config

Chạy lệnh sau để xuất bản file config:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Bạn sẽ có một file config/jwt.php cho phép bạn cấu hình dữ liệu của thư viện này.

Tạo secret key trong thư viện tymon/jwt-auth

Chạy lệnh sau để tạo secret key:

php artisan jwt:secret

Lệnh này sẽ thêm đoạn JWT_SECRET=foobar vào file .env

Config authenticate default guard

Mặc định Laravel sẽ lưu thông tin user đăng nhập vào hệ thống bằng session nhưng ứng dụng của chúng ta có 1 hệ thống frontend riêng biệt với hệ thống backend nên sẽ không thể truy xuất được thông tin session của user đã đăng nhập. Vì thế chúng ta cần khai báo 1 auth guard khác cho hệ thống.

Chúng ta mở file config/auth.php và chỉnh sửa default guard từ web thành api như sau:

...
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],

Tiếp đó chúng ta sẽ thêm guard api vào mảng guards:

...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],

Cài đặt thư viện prettus/l5-repository

Repository Pattern là lớp trung gian giữa tầng Data Access và Business Logic. Repository design pattern cho phép bạn sử dụng các đối tượng mà không cần phải biết các đối tượng này được duy trì như thế nào. Về cơ bản nó là một sự trừu tượng hóa của lớp dữ liệu. Để biết thêm thông tin các bạn có thể xem thêm ở bài viết này

Để cài đặt thư viện, chúng ta chạy câu lệnh sau:

composer require prettus/l5-repository

Thêm service provider

Thêm service provider vào mảng providers trong file config/app.php:

'providers' => [
...
Prettus\Repository\Providers\RepositoryServiceProvider::class,
]

Xuất bản file config:

Chạy lệnh sau để xuất bản file config:

php artisan vendor:publish --provider "Prettus\Repository\Providers\RepositoryServiceProvider"

Config database information

Sau khi cài đặt xong thư viện, chúng ta cần config thông tin database trong file .env:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=next_authentication
DB_USERNAME=root
DB_PASSWORD=

Khi config xong thì chạy lệnh php artisan migrate để tạo bảng cho database

Tạo register route

Để tạo đường dẫn api đăng ký, chúng ta vào file routes/api.php và thêm đoạn code sau

<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
Route::post('register', [AuthController::class, 'register'])->name('register');

Bên trong route cần file AuthController trong thư mục Api. Để tạo file này, chúng ta chạy lệnh:

php artisan make:controller Api/AuthController

Bên trong file AuthController.php chúng ta viết code cho hàm register:

<?php
namespace App\Http\Controllers\Api;
use App\Services\AuthService;
use App\Validators\AuthValidator;
use Illuminate\Http\Request;
class AuthController
{
protected $authService;
private $validator;
public function __construct(
AuthService $authService,
AuthValidator $validator
) {
$this->authService = $authService;
$this->validator = $validator;
}
public function register(Request $request)
{
$this->validator->isValid($request, 'REGISTER');
$user = $this->authService->register($request);
return response()->json(['data' => $user], 200);
}
}

Ở trên chúng ta sử dụng AuthService để lưu phần xử lý logic và AuthValidator để validate dữ liệu.

<?php
// app/Services/AuthService.php
namespace App\Services;
use App\Repositories\UserRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthService
{
public function __construct(
UserRepository $repository
) {
$this->repository = $repository;
}
public function register(Request $request)
{
if (!$this->repository->findByField('email', $request->get('email'))->isEmpty()) {
return response()->json(['error' => 'Email đã tồn tại'], 500);
}
$data = $request->all();
$user = $this->repository->create($data);
$user->password = Hash::make($data['password']);
$user->save();
return $user;
}
}

Bên trong file AuthService ta thấy có sử dụng UserRepository nên ta cần tạo filee UserRepository với nội dung sau:

<?php
// app/Repositories/UserRepository.php
namespace App\Repositories;
use App\Models\User;
use Prettus\Repository\Criteria\RequestCriteria;
use Prettus\Repository\Eloquent\BaseRepository;
/**
* Class UserRepository.
*
* @package namespace App\Repositories;
*/
class UserRepository extends BaseRepository
{
/**
* Specify Model class name
*
* @return string
*/
public function model()
{
return User::class;
}
/**
* Boot up the repository, pushing criteria
*/
public function boot()
{
$this->pushCriteria(app(RequestCriteria::class));
}
}

Xây dựng Validation

Vì dự án sử dụng api để trả về dữ liệu cho frontend nên chúng ta sẽ xây dựng 1 hệ thống Validation để trả về dữ liệu json cho phía frontend.

<?php
// app/Validators/AuthValidator.php
namespace App\Validators;
use App\Validators\AbstractValidator;
/**
* Class AuthValidator.
*
* @package namespace App\Validators;
*/
class AuthValidator extends AbstractValidator
{
protected $rules = [
'REGISTER' => [
'email' => ['required', 'email'],
'password' => ['required', 'min:4'],
],
];
}
<?php
// app/Validators/AbstractValidator.php
namespace App\Validators;
use App\Validators\ValidatorInterface;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
abstract class AbstractValidator implements ValidatorInterface
{
/**
* Get rule for validation by action ValidatorInterface::RULE_CREATE or ValidatorInterface::RULE_UPDATE
*
* Default rule: ValidatorInterface::RULE_CREATE
*
* @param null $action
* @return array
*/
public function getRules($action = null)
{
$rules = $this->rules;
if (isset($this->rules[$action])) {
$rules = $this->rules[$action];
}
return $rules;
}
public function isValid($data, $action)
{
if ($data instanceof Request) {
$data = $data->all();
}
$validator = Validator::make($data, $this->getRules($action));
if ($validator->fails()) {
throw new Exception($validator->errors(), 1000);
}
return true;
}
}
<?php
// app/Validators/ValidatorInterface.php
<?php
namespace App\Validators;
interface ValidatorInterface
{
public function isValid($data, $action);
}

Vậy là chúng ta đã xây dựng xong api register, tiếp theo chúng ta sẽ xây dựng api login cho ứng dụng.

Tạo login route

Để tạo đường dẫn api login, chúng ta vào file routes/api.php và thêm đoạn code sau:

<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
...
Route::post('login', [AuthController::class, 'login'])->name('login');

Tiếp theo ta sẽ thêm hàm login cho AuthController:

<?php
// app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
class AuthController
{
...
public function login(Request $request)
{
$this->validator->isValid($request, 'LOGIN');
$token = $this->authService->login($request);
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60,
], 200);
}
}

Và cập nhật lại các file sau AuthService, AuthValidator:

<?php
namespace App\Services;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthService
{
...
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
try {
$user = $this->repository->findByField('email', $credentials['email'])->first();
if (!$user) {
return response()->json(['error' => 'Email đã tồn tại'], 500);
}
if (!Hash::check($credentials['password'], $user->password)) {
return response()->json(['error' => 'Mật khẩu không đúng'], 500);
}
if (!$token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Không tạo được token'], 500);
}
return $token;
} catch (JWTException $e) {
return response()->json(['error' => 'Không tạo được token'], 500);
}
}
}
<?php
namespace App\Validators;
use App\Validators\AbstractValidator;
/**
* Class AuthValidator.
*
* @package namespace App\Validators;
*/
class AuthValidator extends AbstractValidator
{
protected $rules = [
...
'LOGIN' => [
'email' => ['required', 'email'],
'password' => ['required', 'min:4'],
],
];
}

Cuối cùng, ta sẽ tạo 1 route me để lấy ra thông tin người dùng đã đăng nhập.

// routes/api.php
<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Support\Facades\Route;
...
Route::get('me', [AuthController::class, 'me'])->name('me');

Ta thêm hàm me vào AuthController như sau:

<?php
// app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
class AuthController
{
...
public function me(Request $request)
{
$user = auth()->user();
if (empty($user)) {
return response()->json(['error' => 'Không tồn tại người dùng'], 500);
}
return response()->json(['data' => $user], 200);
}
}

Cài đặt Nextjs

Sau khi đã xây dựng xong phần Backend, tiếp theo chúng ta sẽ xây dựng Frontend bằng Nextjs. Để tạo ứng dụng Nextjs chúng ta cần cài đặt Nodejsnpx, sau đó chạy câu lệnh sau:

npx create-next-app next-auth --ts

Tiếp theo chúng ta cd next-auth để truy cập thư mục của dự án, sau đó gõ lệnh npm run dev để chạy dự án.

Mở trình duyệt và gõ http://localhost:3000 để xem giao diện của dự án.

Cài đặt bootstrap

Để giao diện nhìn đẹp hơn thì chúng ta sẽ thêm thư viện bootstrap vào dự án bằng cách mở file pages/index.tsx và thêm đoạn code sau:

import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
<>
<Head>
<title>Next Auth</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi"
crossorigin="anonymous"
/>
</Head>
...
</>
)
}
export default Home

Tiếp theo chúng ta sẽ chia layout cho từng trang riêng biệt. Chúng ta sẽ tạo 1 file layouts/Layout.tsx để chứa layout chung cho toàn bộ hệ thống:

import React from "react";
import Head from "next/head";
import Link from "next/link";
const Layout = ({ children }) => {
return (
<>
<Head>
<title>Next Auth</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi"
crossorigin="anonymous"
/>
</Head>
<nav className="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<div className="container-fluid">
<Link href="/">
<a className="navbar-brand">Home</a>
</Link>
<div>
<ul className="navbar-nav me-auto mb-2 mb-md-0">
<li className="nav-item">
<Link href="/login">
<a className="nav-link active">Login</a>
</Link>
</li>
<li className="nav-item">
<Link href="/register">
<a className="nav-link active">Register</a>
</Link>
</li>
</ul>
</div>
</div>
</nav>
<main className="form-signin w-100 m-auto">{children}</main>
</>
);
};
export default Layout;

Ở đây, chúng ta sẽ sử dụng chung phần header cho các trang và mỗi trang sẽ khác nhau ở phần main. Nên ở thẻ main chúng ta có đoạn {children} để hiển thị nội dung của các trang con.

Xây dựng trang đăng ký

Tiếp theo chúng ta tạo file pages/register.jsx và xây dựng form đăng ký như sau:

import React from "react";
import Layout from "../layouts/Layout";
const Register = () => {
return (
<Layout>
<form>
<h1 className="h3 mb-3 fw-normal">Đăng ký</h1>
<div className="form-floating">
<input
type="text"
className="form-control"
placeholder="Tên người dùng"
required
/>
</div>
<div className="form-floating">
<input
type="email"
className="form-control"
placeholder="Email"
required
/>
</div>
<div className="form-floating">
<input
type="password"
className="form-control"
placeholder="Mật khẩu"
required
/>
</div>
<button className="w-100 btn btn-lg btn-primary" type="submit">
Đăng ký
</button>
</form>
</Layout>
);
};
export default Register;

Tiếp theo chúng ta sẽ lưu giá trị các input trong form, và xử lý sự kiện form submit:

import { useRouter } from "next/router";
import React, { SyntheticEvent, useState } from "react";
import Layout from "../layouts/Layout";
const Register = () => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const submit = async (e: SyntheticEvent) => {
e.preventDefault();
const response = await fetch("http://localhost:8000/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
email,
password,
}),
});
if (response.status === 200) {
await router.push("/login");
}
};
return (
<Layout>
<form onSubmit={submit}>
<h1 className="h3 mb-3 fw-normal">Đăng ký</h1>
<div className="form-floating">
<input
type="text"
className="form-control"
placeholder="Tên người dùng"
required
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="form-floating">
<input
type="email"
className="form-control"
placeholder="Email"
required
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="form-floating">
<input
type="password"
className="form-control"
placeholder="Mật khẩu"
required
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button className="w-100 btn btn-lg btn-primary" type="submit">
Đăng ký
</button>
</form>
</Layout>
);
};
export default Register;

useState là một Hook của ReactJS nhận vào 2 tham số. Tham số thứ nhất là giá trị ban đầu và tham số thứ 2 là một function để cập nhật giá trị cho tham số thứ nhất. Bạn có thể xem thêm ví dụ về useState tại đây

Ở trang register chúng ta sử dụng hàm onChange để cập nhật dữ liệu cho các input của form. Và khai báo hàm submit khi nhập đủ thông tin và ấn submit form.

Trong hàm submit chúng ta dùng hàm fetch để gửi dữ liệu của form lên api register với đường dẫn http://localhost:8000/api/register mà chúng ta đã xây dựng ở bên trên. Khi server trả về status = 200 tức là thêm mới thành công và ta chuyển đến trang login để đăng nhập.

Xây dựng trang đăng nhập

Tương tự trang đăng ký, để xây dựng trang đăng nhập chúng ta tạo file pages/login.jsx và xây dựng form như sau:

import { useRouter } from "next/router";
import React, { SyntheticEvent, useState } from "react";
import Layout from "../layouts/Layout";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const submit = async (e: SyntheticEvent) => {
e.preventDefault();
await fetch("http://localhost:8000/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(async (data) => {
localStorage.setItem("jwt_token_key", data.access_token);
await router.push("/");
});
};
return (
<Layout>
<form onSubmit={submit}>
<h1 className="h3 mb-3 fw-normal">Đăng nhập</h1>
<div className="form-floating">
<input
type="email"
className="form-control"
placeholder="Email"
required
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="form-floating">
<input
type="password"
className="form-control"
placeholder="Mật khẩu"
required
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button className="w-100 btn btn-lg btn-primary" type="submit">
Đăng nhập
</button>
</form>
</Layout>
);
};
export default Login;

Ở trang Login chúng ta cũng sử dụng hàm onChange để cập nhật dữ liệu cho các input của form. Và khai báo hàm submit khi nhập đủ thông tin và ấn submit form.

Trong hàm submit chúng ta dùng hàm fetch để gửi dữ liệu của form lên api login với đường dẫn http://localhost:8000/api/login mà chúng ta đã xây dựng ở bên trên. Khi server trả về status = 200 tức là đăng nhập thành công và ta chuyển đến trang homepage.

Source code

https://github.com/chung-nguyen-tda/next-auth-api

https://github.com/chung-nguyen-tda/next-auth


Tags

authenticationlaravelnextjs

Share

chung.nguyen1

chung.nguyen1

Developer

Expertise

Related Posts

Tìm hiểu Facade Design Pattern trong Laravel
Solutions
Tìm hiểu Facade Design Pattern trong Laravel
December 21, 2022
3 min
Xây dựng ứng dụng social login với Nextjs
Others
Xây dựng ứng dụng social login với Nextjs
November 20, 2022
2 min
© 2023, All Rights Reserved.
Powered By

Quick Links

HomeOur Team

Social Media