Authentication trong Angular

Authentication trong Angular là xác thực người dùng, nhằm xác định người đang dùng ứng dụng là ai để cấp quyền truy cập.

Liên quan đến authentication, có một số việc cần làm cơ bản sau đây: Xử lý đăng nhập của user, chức năng thoát, nhận thông tin profile của người dùng, bảo vệ ứng dụng qua các đường route và gọi hàm tại các vị trí cần thiết để kiểm tra.



Để thực hiện Authentication trong Angular, bạn cần biết các loại route guard  và cách tạo. Route guard là bảo vệ các route trong ứng dụng. User được phép vào 1 route nào đó hay không tùy thuộc vào sự đánh giá true/false của guard. Trong Angular có các loại route guard là: CanActivate,  CanActivateChild, CanDeactivate, CanLoad…

Guard được tạo ra là để bảo vệ các route, do đó khai báo guard ở từng route

1. Chuẩn bị

Đây là bài khá dài và cũng khá công phu cho việc triển khai các chức năng, cho nên chúng ta cần 1 số chuẩn bị trước khi vào chủ đề chính.

a. Chuẩn bị ứng dụng angular phía client

–  Tạo ứng dụng :Tạo ứng dụng angular để thực hiện authentication trong dự án. Tên ứng dụng sao cũng được

ng new authen --defaults

Tạo các component : Tạo các component để thực tập authentication

ng g c home
ng g c downLoad
ng g c dangKy
ng g c dangNhap
ng g c doiPass

– Tạo guard để bảo vệ các route: ng generate guard Baove

– Khai báo config để sử dụng http serviceL

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideClientHydration()
, importProvidersFrom(HttpClientModule)
, provideHttpClient(withFetch())

]
};

– Khai báo route cho các component : Mở app.routes.ts khai báo route cho các component

//app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { DangKyComponent } from './dang-ky/dang-ky.component';
import { DangNhapComponent } from './dang-nhap/dang-nhap.component';
import { DoiPassComponent } from './doi-pass/doi-pass.component';
import { DownLoadComponent } from './down-load/down-load.component';
import { baoveGuard } from './baove.guard';
export const routes: Routes = [
  { path:'', component:HomeComponent},
  { path:'dangnhap', component:DangNhapComponent},
  { path:'dangky', component:DangKyComponent},
  { path:'doipass', component:DoiPassComponent, 
    canActivate:[baoveGuard], },  
  { path:'download', component:DownLoadComponent, 
    canActivate:[baoveGuard], }, 
];

Tạo layout: Nhúng bootstrap và code tạo layout (có menu, router-outlet

<!--src/index.html-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- app/app.component.html-->
<div class="container">
  <header class="bg-info" style="height: 90px"></header>
  <nav>
  <nav class="navbar navbar-expand bg-warning">
  <ul class="navbar-nav">
    <li class="nav-item">
     <a class="nav-link" href="#" routerLink="/">Trang chủ</a> 
    </li>
    <li class="nav-item">
     <a class="nav-link" href="#" routerLink="dangnhap">Đăng nhập</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#" routerLink="dangky">Đăng ký</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#" routerLink="doipass">Đổi pass</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#" routerLink="download">Download</a>
    </li>
  </ul>
  </nav>
  </nav>
  <main class="d-flex" style="min-height: 300px;">
    <article class="col-md-9 bg-body-secondary"> 
        <router-outlet></router-outlet>
    </article>
    <aside class="col-md-3 bg-info-subtle"> </aside>
  </main>
</div>

– Tạo service auth: Chạy lệnh ng g s auth để tạo service có tên auth (hoặc tên gì cũng được) . Nơi đây bạn sẽ code các hàm authentication.

b. Chuẩn bị ứng dụng phía server

– Tạo folder bai7_ServerNodeJS

– Chuyển vào folder mới tạo và chạy lệnh npm init và gõ Enter chấp nhận tất cả các thông số mặc định.

– Chạy lệnh cài module express: npm install express

– Tạo file server.js sử dụng module express

const exp = require("express");
const app = exp();
const port = 3000;
app.get("/", (req, res) => {
     res.send("<h1>Đây là trang home</h1>");
});
app.listen(port, () =>{
     console.log(`Ung dung dang chay voi port ${port}`);
});

– Chạy ứng dụng: node server.js

– Xem trong trình duyệt: http://localhost:3000

Như vậy chúng ta đã chuẩn bị xong 2 ứng dụng. Ứng dụng angular hoạt động như client và ứng dụng NodeJS hoạt động như server phía backend. Dùng cả hai để thực hiện giải pháp authentication trong Angular.

2. Tạo chức năng đăng nhập

Tạo form đăng nhập

Để người dùng đăng nhập, bạn tạo form đăng nhập (trong angular) .Thực hiện như sau

– Import form module

//dang-nhap.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-dang-nhap', standalone: true,
imports: [FormsModule],
templateUrl: './dang-nhap.component.html',
styleUrl: './dang-nhap.component.css'
})
export class DangNhapComponent { }

– Tạo form trong view

<!-- dang-nhap.component.html -->
<form #frm1="ngForm" class="col-10 m-auto p-2 border border-primary" 
(ngSubmit)="xulyDN(frm1.value)" >
<h4>THÀNH VIÊN ĐĂNG NHẬP</h4>
<p>Username
  <input name="un" ngModel class="form-control border-primary" type="text"> 
</p>
<p> Password
  <input name="pw" ngModel class="form-control border-primary" type="password"> 
</p>
<p>
 <button type="submit" class="btn btn-success">Đăng nhập </button>
</p>
</form>

Xử lý đăng nhập trong form

Import và tạo Router, authService, định nghĩa hàm xulyDN để gọi hàm login trong service

//dang-nhap.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
@Component({
  selector: 'app-dang-nhap',
  templateUrl: './dang-nhap.component.html',
  styleUrls: ['./dang-nhap.component.css']
})
export class DangNhapComponent implements OnInit {
  constructor( 
    private auth:AuthService, 
    private router: Router
  ) { }
  xulyDN(data:any){
    this.auth.login( data.un, data.pw).subscribe( ()=>{ 
         console.log("Đăng nhập thành công");
         this.router.navigateByUrl('/');
      }
    )
  }
}

Định nghĩa hàm login trong service để gọi lên server

Trong auth service , sử dụng http service để submit username và password lên cho server kiểm tra.

//auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Injectable({  providedIn: 'root'})
export class AuthService {
  constructor( private _http:HttpClient) { }
  login(username:string='', password:string=''){    
    const userInfo = { un:username, pw:password }
    const headers = new HttpHeaders().set('Content-Type', 'application/json') ;
    return this._http.post('http://localhost:3000/login'
    , JSON.stringify(userInfo) 
    , {headers:headers, responseType: 'text'}
  ) 
  }//login
}

3. Code phía server tạo Json Web Token session

Phía server sẽ đón nhận request post chứa thông tin người dùng từ form login gửi lên và kiểm tra thông tin hợp lệ không. Nếu OK thì 1 token sẽ được tạo ra và gửi cho phía client (tức trình duyệt) sau khi tạo để dùng cho liên lạc trong các request sau đó.

Cài SSL

Vào https://kb.firedaemon.com/support/solutions/articles/4000121705-openssl-3-0-and-1-1-1-binary-distributions-for-microsoft-windows để download OpenSSL. Sau đó giải nén file download sẽ có source OpenSSL để dùng (trong folder x64\bin)

Mở command line rồi vào folder x64\bin chạy các lệnh sau để tạo provite key, public key:

– Tạo private key:

openssl genrsa -out private-key.txt

– Tạo public key:

openssl rsa -in private-key.txt -pubout -out public-key.txt

Chép 2 file mới tạo private-key.txt public-key.txt sang folder b7_ServerNodeJS để dùng

Cài các module cho NodeJS

Cài module cors để cho phép http request cross domain, module moment để trợ giúp tính ngày giờ (để xem session user hết hạn chưa), module node-jsonwebtoken để tạo token gửi về cho client

npm i cors
npm i moment
npm i node-jsonwebtoken

Code xử lý tạo và trả về token

Code trong server.js (phía server) thực hiện các việc sau:

  1. Tạo path có tên login với method post để đón post request gửi lên từ client.
  2. Nhận username và password trong body của request rồi gọi hàm check userpass xem có đúng không (trả về true/false). Nếu sai trả về status 401 , nếu OK thì tạo token rồi send lại cho client
  3. Định nghĩa hàm check userpass để kiểm tra user pass có ok không. Code sau là demo nhẹ cho dễ hiểu, sau này bạn code kết nối vào db thêm nhé
  4. Định nghĩa hàm lấy thông tin user để lấy đầy đủ thông tin của user theo username.
//server.js
const exp = require("express");
const fs = require('fs');
const bodyParser = require("body-parser");
const jwt = require('jsonwebtoken');
var cors = require('cors')
const app = exp();
const port = 3000;
const PRIVATE_KEY = fs.readFileSync('private-key.txt');
app.use(bodyParser.json()); 
app.use(cors());
app.get("/", (req, res) => { res.send("<h1>Đây là trang home</h1>");});
app.post('/login', (req, res) => {
     const un = req.body.un;
     const pw = req.body.pw;
     if (checkUserPass(un, pw)) {
          const userInfo = getUserInfo(un);   
          const jwtBearerToken = jwt.sign({}, PRIVATE_KEY, {
             algorithm: 'RS256',  
             expiresIn: 120, 
             subject: userInfo.id
          })          
          //res.cookie("SESSIONID", jwtBearerToken, {httpOnly:true, secure:false});
          res.status(200).json({ idToken: jwtBearerToken, expiresIn: 120 });
       }
       else res.sendStatus(401); // send status 401 Unauthorized  
})
checkUserPass = (un, pw) => {  
     if (un=='aa' && pw=='123') { return true}
     if (un=='bb' && pw=='321') { return true}
     return false; 
}
getUserInfo = (username) =>{  
     if (username=='aa') return { "id":"1", hoten:"Nguyễn Văn Tèo" }  
     if (username=='bb')  return { "id":"2", hoten:"Nguyễn Thị Lượm" }  
     return {"id":"-1", "hoten":""}
}
app.listen(port, () =>{   
   console.log(`Ung dung dang chay voi port ${port}`); 
});

4. Lưu trữ và sử dụng JWT ở client side

Token có thể lưu trong cookie hoặc localstorage để dùng sau (cho các chức năng cần check đăng nhập)

– Trong project angular, cài đặt module moment : npm i moment

– Code trong component đăng nhập

//dang-nhap.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AuthService } from '../auth.service';
import moment  from 'moment';
import { Router } from '@angular/router';

@Component({
  selector: 'app-dang-nhap', standalone: true,
  imports: [FormsModule],
  templateUrl: './dang-nhap.component.html',
  styleUrl: './dang-nhap.component.css'
})
export class DangNhapComponent {
  constructor( 
    private auth:AuthService, 
    private router: Router) { }
  xulyDN(data:any){    
    console.log(data, data.un , data.pw);
    this.auth.login( data.un, data.pw).subscribe( 
      res =>{          
          var d = JSON.parse(res);
          console.log("Đăng nhập thành công ", res);          
          const expiresAt = moment().add(d.expiresIn,'second');
           localStorage.setItem('id_token', d.idToken);
           localStorage.setItem("expires_at", 
               JSON.stringify(expiresAt.valueOf()) );
           this.router.navigateByUrl('/');
      },
      error => {
        console.log('oops', error);
        this.router.navigateByUrl('/dangnhap');
      }
    )
   } //xulyDN
}

5. Chức năng thoát

Đơn giản là xóa tất cả các biến đã lưu trong local storage lúc đăng nhập thành công. Thực bằng cách tạo link thoát và gọi hàm thoát khi user click.

Định nghĩa hàm thoát trong service

//auth.service.ts
thoat() {
    localStorage.removeItem("id_token");
    localStorage.removeItem("expires_at");
    localStorage.removeItem("username");
}

Đặt ở đâu tùy bạn, code  dưới đây định nghĩa trong app.component.html

<aside class="col-md-3 bg-info-subtle"> 
<a href="#" (click)="thoat()">Thoát</a>
</aside>

Trong component app, code gọi hàm thoat trong service auth

//app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { RouterLink } from '@angular/router';
import { AuthService } from './auth.service';
import { CommonModule } from '@angular/common';
@Component({
  selector: 'app-root', standalone: true,
  imports: [RouterOutlet, RouterLink , CommonModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'Authentication trong Angular';
  constructor( private auth:AuthService){}
  thoat(){ this.auth.thoat();  }
}

6. Kiểm tra đăng nhập

Để kiểm tra user đã đăng nhập chưa, bạn định nghĩa hàm trong service để kiểm tra token quá hạn chưa, nếu chưa thì trả về true còn quá hạn thì trả về false

//auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import moment from 'moment';
import { DOCUMENT } from '@angular/common';
import { Inject } from '@angular/core';
@Injectable({  providedIn: 'root'})
export class AuthService {
  constructor( 
    private _http:HttpClient ,
    @Inject(DOCUMENT)  private document: Document
  ) { }
  login(username:string='', password:string=''){    
    const userInfo = { un:username, pw:password }
    const headers = new HttpHeaders().set('Content-Type', 'application/json') ;
    return this._http.post('http://localhost:3000/login'
    , JSON.stringify(userInfo) 
    , {headers:headers, responseType: 'text'}
  ) 
  }//login  
  daDangNhap() {
   let localStorage = this.document.defaultView?.localStorage
    if (!localStorage) return false;
    const str = localStorage.getItem("expires_at") || "";
    if (str=="") return false; //chưa dn    
    const expiresAt = JSON.parse(str);    
    return moment().isBefore(moment(expiresAt));
  } //daDangNhap
  thoat() {
    localStorage.removeItem("id_token");
    localStorage.removeItem("expires_at");
    localStorage.removeItem("username");
  }
}

Định nghĩa hàm daDangNhap trong app component

 //app.component.ts
...
daDangNhap() { return this.auth.daDangNhap()}

Gọi hàm daDangNhap trong view:

<!-- app.component.html-->
<aside class="col-md-3 bg-info-subtle"> 
   <a href="#" (click)="thoat()">Thoát</a>
   <p> Tình trạng đăng nhập: {{ daDangNhap() }}</p>
</aside>

Test : thử thoát và đăng nhập, sẽ thấy giá trị true/false hiện ra

Ẩn hiện tag theo tình trạng đăng nhập

<!-- app.component.html-->
<li *ngIf="daDangNhap()" class="nav-item">
    <a class="nav-link" routerLink="doipass" href="#"> Đổi pass</a>
</li>

Code trong guard bảo vệ route

//baove.guard.ts
import { CanActivateFn } from '@angular/router';

import { Router } from '@angular/router';
import moment  from 'moment';
export const baoveGuard: CanActivateFn = (route, state) => {
  const str = localStorage.getItem("expires_at") || "";
  if (str=="") return false; //chưa dn    
  const expiresAt = JSON.parse(str);    
  const daDangNhap =  moment().isBefore(moment(expiresAt));
  return daDangNhap
};

Test thử : khi chưa đăng nhập, nhắp vào link Download trên menu bị chuyển sang trang đăng nhập, nếu đã đăng nhập thì sẽ xem được view download.

Mời bạn đọc các link sau để tham khảo thêm:


Mời bạn thực tập triển khai thêm

  • Tạo nội dung cho view download
  • Chức năng đổi pass
  • Cải thiện đăng nhập phía server (hàm checkUserPass) kết nối vào database để check
  • Cải thiện đăng nhập phía server (hàm getUseInfo) kết nối vào database để lấy dữ liệu
  • Cải thiện đăng nhập phía client: bổ sung thêm để quay lại trang cũ (hiện chỉ quay lại đúng trang chủ)