Tiếp theo bài Xác thực và cấp quyền truy cập trong Angular, trong bài này chúng ta sẽ thực hành viết ứng dụng xác thực người dùng với JWT token (JWT được tạo bởi REST API trong bài Tạo JWT và xác thực quyền truy cập trong Django Rest Framework).
Trước hết chúng ta tiến hành cài đặt jwt-decode để decode token nhận được từ server sử dụng dòng lệnh sau:
1 |
npm install jwt-decode |
Tiếp theo ta tiến hành viết lại shared/auth.service.ts như sau:
1. Hàm login()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
login(log_uname, log_pw){ this.httpClient.post('http://127.0.0.1:8000/get-token/',{username: log_uname, password:log_pw}).subscribe( response => { // console.log(response) this.refresh_token=response['refresh']; this.access_token=response['access'] // Decode received token var decoded_access = jwt_decode(this.access_token); var decoded_refresh = jwt_decode(this.refresh_token); this.role=decoded_access['role'] ; // Store access token and its expired time localStorage.setItem('access_token', this.access_token); localStorage.setItem('access_exp', decoded_access['exp']); // Store refresh token and its expired time localStorage.setItem('refresh_token', this.refresh_token); localStorage.setItem('refresh_exp', decoded_refresh['exp']); // Redirect to dashboard (AuthGuard will check with canActivate() method ) this.router.navigate(['/dashboard']); } ) } |
Ta gửi POST request tới REST API http://127.0.0.1:8000/get-token/ (xem thêm tại bài Tạo JWT và xác thực quyền truy cập trong Django Rest Framework) để xác thực người dùng. Khi người dùng nhập đúng username và password thì server sẽ trả về token của người dùng (bao gồm refresh token và access token). Ta tiến hành decode token nhận được sử dụng jwt-decode rồi lưu 4 thông tin gồm access token, access token expire (thời hạn sử dụng của access token), refresh token và refresh token expire vào localStorage. Sau đó ta chuyển hướng người dùng đến Dashboard để AuthGuard thực hiện xác thực quyền truy cập dựa trên role của người dùng
2. Hàm Logout()
Khi user logout chúng ta sẽ thực hiện xóa các thông tin đã lưu trong localStorage và chuyển hướng người dùng đến trang Login như sau:
1 2 3 4 5 6 7 8 9 10 |
// Logout function logout(){ // Remove stored information localStorage.removeItem('access_token') localStorage.removeItem('access_exp') localStorage.removeItem('refresh_token') localStorage.removeItem('refresh_exp') // Redirect to login page this.router.navigate(['/login']); } |
3. Hàm isAuthenticated()
Khi người dùng được chuyển hướng đến Dashboard (hoặc khi người dùng truy cập một path bất kỳ) AuthGuard sẽ sử dụng hàm isAuthenticated() để kiểm tra người dùng có quyền truy cập hay không.
1 2 3 4 5 6 7 8 9 10 11 12 |
isAuthenticated(){ let refresh_token=localStorage.getItem('refresh_token') // Check if refresh token exsist if(refresh_token){ var refreshTime= parseInt(localStorage.getItem('refresh_exp') ) - Math.floor(Date.now() / 1000) // If the refresh token valid more than 1 min if(refreshTime>60){ // Get user role this.role= jwt_decode(refresh_token)['role'] return true } } |
Ta sử dụng hàm localStorage.getItem() để kiểm tra sự tồn tại của refresh token và kiểm tra xem refresh token có còn thời hạn sử dụng hay không. Nếu người dùng có refresh token trong thời hạn sử dụng tức là người dùng sẽ có quyền truy cập ứng dụng
4. Các hàm kiểm tra access token
Để đảm bảo access token của người dùng luôn còn thời gian sử dụng, ta viết hàm tokenChecking() để kiểm tra nếu thời hạn sử dụng của access token còn không quá 1 phút thì sẽ gửi POST request tới http://127.0.0.1:8000/refresh-token/ sử dụng refresh token để lấy access token mới.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
tokenChecking(){ let access_token=localStorage.getItem('access_token') // Check if access token exsist if(access_token){ // Remain valid time of access token var time2refresh= parseInt(localStorage.getItem('access_exp')) - Math.floor(Date.now() / 1000) // If the token valid less than 1 mins if(time2refresh< 60){ // Check if refresh token is still valid var refreshTime= parseInt(localStorage.getItem('refresh_exp')) - Math.floor(Date.now() / 1000) // If the refresh token valid more than 1 min if(refreshTime>60){ // Get new access token using refresh token this.refresh2token() console.log('Get new access token successfully') } } } } // Get new access token using refresh token refresh2token(){ console.log('Get new token using refresh token') this.httpClient.post('http://127.0.0.1:8000/refresh-token/',{refresh: localStorage.getItem('refresh_token')}).subscribe( response => { // Extract response data this.access_token=response['access'] var decoded_access = jwt_decode(this.access_token); this.role=decoded_access['role']; // Store access token and its expired time localStorage.setItem('access_token', this.access_token); localStorage.setItem('access_exp', decoded_access['exp']); } ) } |
Hàm tokenChecking() sẽ được khởi chạy khi ta bắt đầu khởi chạy ứng dụng và sau đó sẽ được chạy mỗi phút một lần để kiểm tra access token bằng cách sử dụng setInterval() như sau:
1 2 3 4 5 6 7 8 9 10 11 12 |
constructor(private router: Router, private httpClient: HttpClient) { // Check the access token this.tokenChecking() // Check token every 1 min to make sure user always get update token this.tokenInterval() } // Check access token every 1 min to see if it is still valid tokenInterval() { this.token_interval = setInterval(() => { this.tokenChecking(); },60*1000) } |
Tiếp theo, ta viết JWTInterceptorService để thêm header với thông tin về access token vào các request gửi đến server như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root' }) export class JWTInterceptorService implements HttpInterceptor{ constructor(private auth_service: AuthService) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // Get access_token from localStorage const access_token = localStorage.getItem('access_token'); // Intercept every http request if the token exists if (access_token) { const cloned = req.clone({ // Add token to header of http requst headers: req.headers.set('Authorization', 'Bearer '.concat(access_token)) }); return next.handle(cloned); } else { return next.handle(req); } } } |
Để đảm bảo WTInterceptorService có thể thêm thông tin vào các Http request, ta tiến hành cấu hình provider trong app.module.ts như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { LoginComponent } from './login/login.component'; import {MatCardModule} from '@angular/material/card'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { DashboardModule } from './dashboard/dashboard.module'; import { JWTInterceptorService } from './shared/jwtinterceptor.service'; @NgModule({ declarations: [ AppComponent, LoginComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, MatCardModule, FormsModule, RouterModule, HttpClientModule, DashboardModule ], // Configue JWTInterceptor providers: [ { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptorService, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptorService, multi: true }, ], bootstrap: [AppComponent] }) export class AppModule { } |
Trong bài Tạo JWT và xác thực quyền truy cập trong Django Rest Framework, ta đã viết REST API http://127.0.0.1:8000/stockApp/getInfo/ cho phép người dùng (đã được xác thực) truy cập thông tin về các mã chứng khoán. Do đó, trong bài này để kiểm tra hoạt động của Inceptor ta tạo thêm StockInfoComponent để hiển thị dữ liệu nhận được từ getInfo API như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- stock-info.component.html --> <h2> Stock Information </h2> <table class="example-table"> <!-- table header row --> <thead > <th>No</th> <th>Name</th> <th>ROE</th> <th>PB</th> <th>Company</th> </thead> <!-- table body --> <tbody> <!-- table row --> <tr *ngFor="let stock of stock_list; index as i"> <td>{{i+1}}</td> <td>{{stock.name}}</td> <td>{{stock.roe}}</td> <td>{{stock.pb}}</td> <td>{{stock.company}}</td> </tr> </tbody> </table> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// stock-info.component.ts import { Component, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; // Define Post interface for each post export interface StockInfo{ name: string; roe: number; pb: number; company: string; } @Component({ selector: 'app-stock-info', templateUrl: './stock-info.component.html', styleUrls: ['./stock-info.component.css'] }) export class StockInfoComponent implements OnInit { stock_list: StockInfo[]; constructor(private httpClient: HttpClient) { } ngOnInit(): void { this.httpClient.get<StockInfo[]>('http://127.0.0.1:8000/stockApp/getInfo/').subscribe( response =>{ this.stock_list=response } ) } } |
Khởi chạy ứng dụng với lệnh ng serve và đăng nhập ứng dụng tại http://localhost:4200/login; Click vào Stock Info ta được kết quả hiển thị dữ liệu chứng khoán như sau:Mở một tab mới và truy cập http://localhost:4200/dashboard/contact, ứng dụng cho phép ta truy cập Contact component vì access token với quyền Admin đã được lưu trong localStorage:
Click Logout và truy cập lại địa chỉ http://localhost:4200/dashboard, ứng dụng chuyển hướng người dùng đến trang Login vì thông tin về token đã bị xóa khi người dùng Logout. Do đó, ứng dụng yêu cầu người dùng phải thực hiện đăng nhập lại.
Lưu ý: Khi gửi request tới Django server mà gặp lỗi “Access to XMLHttpRequest at ‘url’’ from origin has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource”: chúng ta tiến hành cài đặt django-cors-headers bằng lệnh pip install django-cors-headers và thực hiện cấu hình file settings.py của Django project như sau:
1 2 3 4 5 6 7 8 9 |
INSTALLED_APPS = [ ..... 'corsheaders', ] MIDDLEWARE = [ ..... 'corsheaders.middleware.CorsMiddleware', ] |
Với ALLOWED_HOSTS ta có thể sử dụng một trong hai cách sau:
1 2 |
ALLOWED_HOSTS=['*'] CORS_ORIGIN_ALLOW_ALL = True |
Hoặc
1 2 3 4 5 |
ALLOWED_HOSTS=['http://localhost:4200'] CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = ( 'http://localhost:4200', ) |
Như vậy, với việc kết hợp sử dụng JWT được tạo bởi REST API trong bài Tạo JWT và xác thực quyền truy cập trong Django Rest Framework, chúng ta đã hoàn thành việc viết ứng dụng xác thực người dùng với JWT token. Các bạn có thể tham khảo toàn bộ code của bài này trong trang Github của Itech Seeker tại đây.