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
Chạy lệnh git clone https://github.com/laravel/laravel next-auth-api
Để cài đặt thư viện cho dự án chúng ta chạy câu lệnh
composer install
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=LaravelAPP_ENV=localAPP_KEY=APP_DEBUG=trueAPP_URL=http://localhostLOG_CHANNEL=stackLOG_DEPRECATIONS_CHANNEL=nullLOG_LEVEL=debugDB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=laravelDB_USERNAME=rootDB_PASSWORD=BROADCAST_DRIVER=logCACHE_DRIVER=fileFILESYSTEM_DISK=localQUEUE_CONNECTION=syncSESSION_DRIVER=fileSESSION_LIFETIME=120MEMCACHED_HOST=127.0.0.1REDIS_HOST=127.0.0.1REDIS_PASSWORD=nullREDIS_PORT=6379MAIL_MAILER=smtpMAIL_HOST=mailhogMAIL_PORT=1025MAIL_USERNAME=nullMAIL_PASSWORD=nullMAIL_ENCRYPTION=nullMAIL_FROM_ADDRESS="hello@example.com"MAIL_FROM_NAME="${APP_NAME}"AWS_ACCESS_KEY_ID=AWS_SECRET_ACCESS_KEY=AWS_DEFAULT_REGION=us-east-1AWS_BUCKET=AWS_USE_PATH_STYLE_ENDPOINT=falsePUSHER_APP_ID=PUSHER_APP_KEY=PUSHER_APP_SECRET=PUSHER_HOST=PUSHER_PORT=443PUSHER_SCHEME=httpsPUSHER_APP_CLUSTER=mt1VITE_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}"
Chạy lệnh sau để tạo secret key cho dự án:
php artisan key:generate
Để 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 vào mảng providers trong file config/app.php
:
'providers' => [...Tymon\JWTAuth\Providers\LaravelServiceProvider::class,]
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.
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
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',],],
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 vào mảng providers trong file config/app.php
:
'providers' => [...Prettus\Repository\Providers\RepositoryServiceProvider::class,]
Chạy lệnh sau để xuất bản file config:
php artisan vendor:publish --provider "Prettus\Repository\Providers\RepositoryServiceProvider"
Sau khi cài đặt xong thư viện, chúng ta cần config thông tin database trong file .env
:
DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=next_authenticationDB_USERNAME=rootDB_PASSWORD=
Khi config xong thì chạy lệnh php artisan migrate
để tạo bảng cho database
Để tạo đường dẫn api đăng ký, chúng ta vào file routes/api.php
và thêm đoạn code sau
<?phpuse 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
:
<?phpnamespace 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.phpnamespace 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.phpnamespace 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));}}
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.phpnamespace 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.phpnamespace 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<?phpnamespace 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 đường dẫn api login, chúng ta vào file routes/api.php
và thêm đoạn code sau:
<?phpuse 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.phpnamespace 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
:
<?phpnamespace 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);}}}
<?phpnamespace 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<?phpuse 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.phpnamespace 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);}}
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 Nodejs và npx
, 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.
Để 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" /><linkhref="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" /><linkhref="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.
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"><inputtype="text"className="form-control"placeholder="Tên người dùng"required/></div><div className="form-floating"><inputtype="email"className="form-control"placeholder="Email"required/></div><div className="form-floating"><inputtype="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"><inputtype="text"className="form-control"placeholder="Tên người dùng"requiredonChange={(e) => setName(e.target.value)}/></div><div className="form-floating"><inputtype="email"className="form-control"placeholder="Email"requiredonChange={(e) => setEmail(e.target.value)}/></div><div className="form-floating"><inputtype="password"className="form-control"placeholder="Mật khẩu"requiredonChange={(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.
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"><inputtype="email"className="form-control"placeholder="Email"requiredonChange={(e) => setEmail(e.target.value)}/></div><div className="form-floating"><inputtype="password"className="form-control"placeholder="Mật khẩu"requiredonChange={(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.
https://github.com/chung-nguyen-tda/next-auth-api
https://github.com/chung-nguyen-tda/next-auth