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.ts – głó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/.
Leave a comment