Uwierzytelnianie i autoryzacja z użyciem JWT, Angulara i Spring Boota [część 2]

Uwierzytelnianie i autoryzacja z użyciem JWT, Angulara i Spring Boota [część 2]

Ten wpis to kontynuacja wpisu https://javaleader.pl/2020/10/05/uwierzytelnianie-i-autoryzacja-z-uzyciem-jwt-angulara-i-spring-boota-czesc-1/. Zachęcam Cię do zapoznania się z pierwszą częścią gdzie napiliśmy API w Spring Boot które wykonuje autentykacje i autoryzacje użytkownika w oparciu o token JWT. Tutaj zobaczysz w jaki sposób wystawić aplikację frontendową która korzysta ze wspomnianego API. Do dzieła! Tworzymy niezbędne komponenty i serwisy Angulara:

ng g s _services/auth
ng g s _services/token-storage
ng g s _services/user
 
ng g c login
ng g c register
ng g c home
ng g c profile
ng g c board-admin
ng g c board-moderator
ng g c board-user

Plik app.module.ts – importujemy i deklarujemy moduły:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
 
import { AppRoutingModule } from './app-routing.module';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
 
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardUserComponent } from './board-user/board-user.component';
 
import { authInterceptorProviders } from './_helpers/auth.interceptor';
 
@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    RegisterComponent,
    HomeComponent,
    ProfileComponent,
    BoardAdminComponent,
    BoardModeratorComponent,
    BoardUserComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [authInterceptorProviders],
  bootstrap: [AppComponent]
})
export class AppModule { }

serwis – auth.service.ts – wysyłamy żądania HTTP metodą POST z danymi potrzebnymi do logowania lub z danymi potrzebnymi do rejestracji użytkownika:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
 
const AUTH_API = 'http://localhost:8080/api/auth/';
 
const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
 
@Injectable({
  providedIn: 'root'
})
export class AuthService {
 
  constructor(private http: HttpClient) { }
 
  login(credentials): Observable<any> {
    return this.http.post(AUTH_API + 'signin', {
      username: credentials.username,
      password: credentials.password
    }, httpOptions);
  }
 
  register(user): Observable<any> {
    return this.http.post(AUTH_API + 'signup', {
      username: user.username,
      email: user.email,
      password: user.password
    }, httpOptions);
  }
}

serwis token-storage.service.ts – przechowujemy dane użytkownika w sesji przeglądarki – (browser session storage):

import { Injectable } from '@angular/core';
 
const TOKEN_KEY = 'auth-token';
const USER_KEY = 'auth-user';
 
@Injectable({
  providedIn: 'root'
})
export class TokenStorageService {
 
  constructor() { }
 
  signOut(): void {
    window.sessionStorage.clear();
  }
 
  public saveToken(token: string): void {
    window.sessionStorage.removeItem(TOKEN_KEY);
    window.sessionStorage.setItem(TOKEN_KEY, token);
  }
 
  public getToken(): string {
    return sessionStorage.getItem(TOKEN_KEY);
  }
 
  public saveUser(user): void {
    window.sessionStorage.removeItem(USER_KEY);
    window.sessionStorage.setItem(USER_KEY, JSON.stringify(user));
  }
 
  public getUser(): any {
    return JSON.parse(sessionStorage.getItem(USER_KEY));
  }
}

serwis user.service.ts – dostęp do zasobów API:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
 
const API_URL = 'http://localhost:8080/api/test/';
 
@Injectable({
  providedIn: 'root'
})
export class UserService {
 
  constructor(private http: HttpClient) { }
 
  getPublicContent(): Observable<any> {
    return this.http.get(API_URL + 'all', { responseType: 'text' });
  }
 
  getUserBoard(): Observable<any> {
    return this.http.get(API_URL + 'user', { responseType: 'text' });
  }
 
  getModeratorBoard(): Observable<any> {
    return this.http.get(API_URL + 'mod', { responseType: 'text' });
  }
 
  getAdminBoard(): Observable<any> {
    return this.http.get(API_URL + 'admin', { responseType: 'text' });
  }
}

serwis – auth.interceptor.ts – (rejestrowany w pliku app.module.ts w tablicy providers). Interceptory służą do przechwytywania żądań. Pozwala to na przechwycenie żądania, modyfikacje oraz przekazanie go dalej. Jeśli token istnieje w sesji przeglądarki to dodawany jest do nagłówka HTTP. W module w którym chcemy używać interceptora, musimy go dodać do sekcji providers. Informujemy wtedy że jest to interceptor, podajemy naszą implementację i opcjonalnie jeżeli chcielibyśmy stosować kilka interceptorów podajemy wartość parametru multi na true. W naszym przypadku interceptor został dodany tutaj:

providers: [authInterceptorProviders]
import { HTTP_INTERCEPTORS, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
 
import { TokenStorageService } from '../_services/token-storage.service';
import { Observable } from 'rxjs';
 
const TOKEN_HEADER_KEY = 'Authorization';       // for Spring Boot back-end
// const TOKEN_HEADER_KEY = 'x-access-token';   // for Node.js Express back-end
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private token: TokenStorageService) { }
 
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let authReq = req;
    const token = this.token.getToken();
    if (token != null) {
      // for Spring Boot back-end
      authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
    }
    return next.handle(authReq);
  }
}
 
export const authInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];

Komponent register.component.ts – rejestrujemy nowego użytkownika wywołując odpowiedni endpoint z API:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../_services/auth.service';
 
@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
 
  form: any = {};
  isSuccessful = false;
  isSignUpFailed = false;
  errorMessage = '';
 
  constructor(private authService: AuthService) { }
 
  ngOnInit(): void {
  }
 
  onSubmit(): void {
    this.authService.register(this.form).subscribe(
      data => {
        console.log(data);
        this.isSuccessful = true;
        this.isSignUpFailed = false;
      },
      err => {
        this.errorMessage = err.error.message;
        this.isSignUpFailed = true;
      }
    );
  }
}

Szablon widoku formularza rejestracyjnego – register.component.html – dane z formularza trafiają do tablicy form:

<div class="col-md-12">
  <div class="card card-container">
    <img
      id="profile-img"
      src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
      class="profile-img-card"
    />
    <form
      *ngIf="!isSuccessful"
      name="form"
      (ngSubmit)="f.form.valid && onSubmit()"
      #f="ngForm"
      novalidate
    >
      <div class="form-group">
        <label for="username">Username</label>
        <input
          type="text"
          class="form-control"
          name="username"
          [(ngModel)]="form.username"
          required
          minlength="3"
          maxlength="20"
          #username="ngModel"
        />
        <div class="alert-danger" *ngIf="f.submitted && username.invalid">
          <div *ngIf="username.errors.required">Username is required</div>
          <div *ngIf="username.errors.minlength">
            Username must be at least 3 characters
          </div>
          <div *ngIf="username.errors.maxlength">
            Username must be at most 20 characters
          </div>
        </div>
      </div>
      <div class="form-group">
        <label for="email">Email</label>
        <input
          type="email"
          class="form-control"
          name="email"
          [(ngModel)]="form.email"
          required
          email
          #email="ngModel"
        />
        <div class="alert-danger" *ngIf="f.submitted && email.invalid">
          <div *ngIf="email.errors.required">Email is required</div>
          <div *ngIf="email.errors.email">
            Email must be a valid email address
          </div>
        </div>
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input
          type="password"
          class="form-control"
          name="password"
          [(ngModel)]="form.password"
          required
          minlength="6"
          #password="ngModel"
        />
        <div class="alert-danger" *ngIf="f.submitted && password.invalid">
          <div *ngIf="password.errors.required">Password is required</div>
          <div *ngIf="password.errors.minlength">
            Password must be at least 6 characters
          </div>
        </div>
      </div>
      <div class="form-group">
        <button class="btn btn-primary btn-block">Sign Up</button>
      </div>
 
      <div class="alert alert-warning" *ngIf="f.submitted && isSignUpFailed">
        Signup failed!<br />{{ errorMessage }}
      </div>
    </form>
 
    <div class="alert alert-success" *ngIf="isSuccessful">
      Your registration is successful!
    </div>
  </div>
</div>

Komponent – login.component.ts – zapisujemy token użytkownika w sesji przeglądarki:

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../_services/auth.service';
import { TokenStorageService } from '../_services/token-storage.service';
 
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
 
  form: any = {};
  isLoggedIn = false;
  isLoginFailed = false;
  errorMessage = '';
  roles: string[] = [];
 
  constructor(private authService: AuthService, private tokenStorage: TokenStorageService) { }
 
  ngOnInit(): void {
    if (this.tokenStorage.getToken()) {
      this.isLoggedIn = true;
      this.roles = this.tokenStorage.getUser().roles;
    }
  }
 
  onSubmit(): void {
 
    this.authService.login(this.form).subscribe(
      (data) => {
        this.tokenStorage.saveToken(data.token);
        this.tokenStorage.saveUser(data);
        this.isLoginFailed = false;
        this.isLoggedIn = true;
        this.roles = this.tokenStorage.getUser().roles;
        this.reloadPage();
      },
      err => {
        this.errorMessage = err.error.message;
        this.isLoginFailed = true;
      }
    );
  }
  reloadPage(): void {
    window.location.reload();
  }
};

Formularz logowania – login.component.html:

<div class="col-md-12">
  <div class="card card-container">
    <img
      id="profile-img"
      src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
      class="profile-img-card"
    />
    <form
      *ngIf="!isLoggedIn"
      name="form"
      (ngSubmit)="f.form.valid && onSubmit()"
      #f="ngForm"
      novalidate
    >
      <div class="form-group">
        <label for="username">Username</label>
        <input
          type="text"
          class="form-control"
          name="username"
          [(ngModel)]="form.username"
          required
          #username="ngModel"
        />
        <div
          class="alert alert-danger"
          role="alert"
          *ngIf="f.submitted && username.invalid"
        >
          Username is required!
        </div>
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input
          type="password"
          class="form-control"
          name="password"
          [(ngModel)]="form.password"
          required
          minlength="6"
          #password="ngModel"
        />
        <div
          class="alert alert-danger"
          role="alert"
          *ngIf="f.submitted && password.invalid"
        >
          <div *ngIf="password.errors.required">Password is required</div>
          <div *ngIf="password.errors.minlength">
            Password must be at least 6 characters
          </div>
        </div>
      </div>
      <div class="form-group">
        <button class="btn btn-primary btn-block">
          Login
        </button>
      </div>
      <div class="form-group">
        <div
          class="alert alert-danger"
          role="alert"
          *ngIf="f.submitted && isLoginFailed"
        >
          Login failed: {{ errorMessage }}
        </div>
      </div>
    </form>
 
    <div class="alert alert-success" *ngIf="isLoggedIn">
      Logged in as {{ roles }}.
    </div>
  </div>
</div>

Komponent dla profilu – profile.component.ts – użytkownik pobierany jest z sessionStorage:

import { Component, OnInit } from '@angular/core';
import { TokenStorageService } from '../_services/token-storage.service';
 
@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
 
  currentUser: any;
 
  constructor(private token: TokenStorageService) { }
 
  ngOnInit(): void {
    this.currentUser = this.token.getUser();
  }
 
}

plik widoku profile.component.html:

<div class="container" *ngIf="currentUser; else loggedOut">
  <header class="jumbotron">
    <h3>
      <strong>{{ currentUser.username }}</strong> Profile
    </h3>
  </header>
  <p>
    <strong>Token:</strong>
    {{ currentUser.token.substring(0, 20) }} ...
    {{ currentUser.token.substr(currentUser.token.length - 20) }}
  </p>
  <p>
    <strong>Email:</strong>
    {{ currentUser.email }}
  </p>
  <strong>Roles:</strong>
  <ul>
    <li *ngFor="let role of currentUser.roles">
      {{ role }}
    </li>
  </ul>
</div>
 
<ng-template #loggedOut>
  Please login.
</ng-template>

Komponent home.component.ts – pobieramy treść dla zasobów publicznych:

import { Component, OnInit } from '@angular/core';
import { UserService } from '../_services/user.service';
 
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
 
  content: string;
 
  constructor(private userService: UserService) { }
 
  ngOnInit(): void {
    this.userService.getPublicContent().subscribe(
      data => {
        this.content = data;
      },
      err => {
        this.content = JSON.parse(err.error).message;
      }
    );
  }
}

Plik widoku home.component.html:

<div class="container">
  <header class="jumbotron">
    <p>{{ content }}</p>
  </header>
</div>

Komponent board-admin.component.ts – pobieramy treść dla zasobów chronionych:

import { Component, OnInit } from '@angular/core';
import { UserService } from '../_services/user.service';
 
@Component({
  selector: 'app-board-admin',
  templateUrl: './board-admin.component.html',
  styleUrls: ['./board-admin.component.css']
})
export class BoardAdminComponent implements OnInit {
 
  content: string;
 
  constructor(private userService: UserService) { }
 
  ngOnInit(): void {
    this.userService.getAdminBoard().subscribe(
      data => {
        this.content = data;
      },
      err => {
        this.content = JSON.parse(err.error).message;
      }
    );
  }
}

Plik widoku – board-admin.component.html – jeśli autoryzacja przebiegła pomyślnie zostanie wyświetlona treść chroniona w przeciwnym wypadku będzie błąd autoryzacji:

<div class="container">
  <header class="jumbotron">
    <p>{{ content }}</p>
  </header>
</div>

Deklarujemy routing – czyli pod jakimi adresami dostępne są komponenty Angulara:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
 
import { RegisterComponent } from './register/register.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
 
const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'profile', component: ProfileComponent },
  { path: 'user', component: BoardUserComponent },
  { path: 'mod', component: BoardModeratorComponent },
  { path: 'admin', component: BoardAdminComponent },
  { path: '', redirectTo: 'home', pathMatch: 'full' }
];
 
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Komponent app.component.tsgłówny komponetnt aplikacji Angulara – zakładki wyświetlane są w zalezności od roli jaką posiada dany użytkownik:

import { Component, OnInit } from '@angular/core';
import { TokenStorageService } from './_services/token-storage.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private roles: string[];
  isLoggedIn = false;
  showAdminBoard = false;
  showModeratorBoard = false;
  username: string;
 
  constructor(private tokenStorageService: TokenStorageService) { }
 
  ngOnInit(): void {
    this.isLoggedIn = !!this.tokenStorageService.getToken();
 
    if (this.isLoggedIn) {
      const user = this.tokenStorageService.getUser();
      this.roles = user.roles;
 
      this.showAdminBoard = this.roles.includes('ROLE_ADMIN');
      this.showModeratorBoard = this.roles.includes('ROLE_MODERATOR');
 
      this.username = user.username;
    }
  }
 
  logout(): void {
    this.tokenStorageService.signOut();
    window.location.reload();
  }
}

!! – oznacza podwójną negację w JavaScript z rzutowaniem na typ logiczny boolean. Widok przedstawia się następująco:

Plik app.component.html:

<div id="app">
  <nav class="navbar navbar-expand navbar-dark bg-dark">
    <a href="#" class="navbar-brand">JavaLeader.pl</a>
    <ul class="navbar-nav mr-auto" routerLinkActive="active">
      <li class="nav-item">
        <a href="/home" class="nav-link" routerLink="home">Home </a>
      </li>
      <li class="nav-item" *ngIf="showAdminBoard">
        <a href="/admin" class="nav-link" routerLink="admin">Admin Board</a>
      </li>
      <li class="nav-item" *ngIf="showModeratorBoard">
        <a href="/mod" class="nav-link" routerLink="mod">Moderator Board</a>
      </li>
      <li class="nav-item">
        <a href="/user" class="nav-link" *ngIf="isLoggedIn" routerLink="user">User</a>
      </li>
    </ul>
 
    <ul class="navbar-nav ml-auto" *ngIf="!isLoggedIn">
      <li class="nav-item">
        <a href="/register" class="nav-link" routerLink="register">Sign Up</a>
      </li>
      <li class="nav-item">
        <a href="/login" class="nav-link" routerLink="login">Login</a>
      </li>
    </ul>
 
    <ul class="navbar-nav ml-auto" *ngIf="isLoggedIn">
      <li class="nav-item">
        <a href="/profile" class="nav-link" routerLink="profile">{{ username }}</a>
      </li>
      <li class="nav-item">
        <a href class="nav-link" (click)="logout()">LogOut</a>
      </li>
    </ul>
  </nav>
 
  <div class="container">
    <router-outlet></router-outlet>
  </div>
</div>

w tagach:

<router-outlet></router-outlet>

wyświetlana jest zawartość zdefiniowanych w aplikacji komponentów. W pliku index.html definiujemy tagi:

<app-root></app-root>

które odpowiadają za wyświetlanie głównego komponentu aplikacji – app.component.ts.

Plik index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
      integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

Aplikację uruchamiamy poleceniem na porcie 8081:

ng serve --port 8081

Zachęcam do lektury oryginalnego artykułu który znajduje się tutaj – https://bezkoder.com/angular-10-spring-boot-jwt-auth/.

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

Leave a comment

Your email address will not be published.


*