
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

