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

Tạo guard

Để tạo guard, dùng lệnh như sau ng generate guard baove , trong đó baove là tên guard cần tạo.

Guard được tạo ra là để bảo vệ các route, do đó khai báo guard ở từng route trong app-routing.module.ts Ví dụ

{path:'doipass',component:DoiPassComponent,canActivate:[BaoveGuard]},  

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 bai7_ClientAngular --routing  --defaults

Nhúng các module cần thiết : Do ứng dụng sẽ có tạo form và sẽ có request lên server nên cần dùng đến FormsModule và HttpClientModule

//app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule} from '@angular/common/http';
@NgModule({
  declarations: [AppComponent, ],
  imports: [ BrowserModule, AppRoutingModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

– Tạo 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 route cho các component: Trong app-routing.ts: khai báo route cho các component

//app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { DangNhapComponent } from './dang-nhap/dang-nhap.component';
import { DangKyComponent } from './dang-ky/dang-ky.component';
import { DoiPassComponent } from './doi-pass/doi-pass.component';
import { DownLoadComponent } from './down-load/down-load.component';
import { BaoveGuard } from './baove.guard';
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], }, 
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

– 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">
  <nav>
  <nav class="navbar navbar-expand navbar-dark bg-dark">
  <ul class="navbar-nav">
  <li class="nav-item"><a class="nav-link" routerLink="" href="#">Trang chủ</a> </li>
  <li class="nav-item"><a class="nav-link" routerLink="download" href="#">Download</a> </li>
  <li class="nav-item"><a class="nav-link" routerLink="dangky" href="#">Đăng ký</a> </li>
  <li class="nav-item"><a class="nav-link" routerLink="dangnhap" href="#">Đăng nhập</a> </li>
  <li class="nav-item"><a class="nav-link" routerLink="doipass" href="#">Đổi pass</a> </li>
  </ul>
  </nav>
  </nav>
  <main class="d-flex" style="min-height: 400px;">
    <article class="col-md-9 bg-light"> 
        <router-outlet></router-outlet>
    </article>
    <aside class="col-md-3 bg-secondary"> </aside>
  </main>
</div>

– Tạo service: Chạy lệnh sau để 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.

ng g s auth

– Chạy ứng dụng Angular: ng serve –o

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. Login form

Tạo login form để người dùng thực hiện đăng nhập. Thực hiện trong ứng dụng Angular:

Code html tạo form

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

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

//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
  ) { }
  ngOnInit(): void { }
  xulyDN(data:any){
    this.auth.login( data.un, data.pw).subscribe( ()=>{ 
         console.log("Đăng nhập thành công");
         this.router.navigateByUrl('/');
      }
    )
  }
}

Code trong service auth để 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 nhúng moment trong app.modules.ts:

import { Moment } from 'moment';

– Code trong component đăng nhập

//dang-nhap.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
import * as moment  from 'moment';
@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) { }
  ngOnInit(): void { }
  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ạo link thoát

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

//app.component.ts
import { Component } from '@angular/core';
import { AuthService } from './auth.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor( private auth:AuthService){}
  thoat(){ this.auth.thoat();  }
}

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

Để kiểm tra đăng nhập 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 * as moment  from 'moment';
public daDangNhap() {
  const str = localStorage.getItem("expires_at") || "";
  if (str=="") return false; //chưa dn    
  const expiresAt = JSON.parse(str);    
  return moment().isBefore(moment(expiresAt));
}
 //app.component.ts
daDangNhap() { return this.auth.daDangNhap()}
<!-- app.component.html-->
<p class="text-white"> Tình trạng đăng nhập: {{ daDangNhap() }}</p>

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

Ẩn hiện tag trong view 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 { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({ providedIn: 'root'})
export class BaoveGuard implements CanActivate {
  constructor(public auth: AuthService, public router: Router) {}
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
      //logic xét duyệt để trả về true/false ở đây
      if (this.auth.daDangNhap()==false) {
        this.router.navigate(['dangnhap']);        
        return false;
      }
      else return true;
  }
}

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ủ)