DevGang
Авторизоваться

Учебное пособие по авторизации на основе ролей Angular 14 с примером

В этом руководстве мы рассмотрим пример того, как реализовать авторизацию/управление доступом на основе ролей в Angular 14. Пример основан на предыдущем опубликованном мной руководстве, посвященном JWT authentication, этот пример был расширен и теперь включает управление доступом на основе ролей. поверх авторизации JWT.

Пример приложения Angular 14

Учебный пример довольно минимален и содержит всего 3 страницы для демонстрации авторизации на основе ролей в Angular 14:

  • /login - общедоступная страница входа с полями имени пользователя и пароля, при отправке страница отправляет запрос POST в API для аутентификации учетных данных пользователя, в случае успеха API возвращает токен JWT для выполнения аутентифицированных запросов к безопасным маршрутам API.
  • / - безопасная домашняя страница, доступная для всех ролей (User и Admin), на ней отображается приветственное сообщение, текущая роль пользователя и текущие сведения о пользователе, полученные из защищенной конечной точки API.
  • /admin - безопасная страница администратора, доступная только для роли Admin, она отображает список всех пользователей, полученных из защищенной конечной точки API, которая также доступна только для пользователей-администраторов.

Форма входа с Angular Reactive Forms

Форма входа в этом примере создана с помощью библиотеки Reactive Forms, которая входит в состав фреймворка Angular (в @angular/forms). Он использует подход, основанный на модели, для создания, проверки и обработки форм в Angular. Более подробное руководство по реактивным формам см. в разделе Angular 14 — пример проверки реактивных форм.

Общее состояние с RxJS

RxJS Subjects и Observables используются для управления общим состоянием в приложении Angular. Например, текущий вошедший в систему пользователь управляется службой аутентификации с помощью RxJS BehaviorSubject и предоставляется остальной части приложения с помощью RxJS Observable, на который может подписаться любой компонент Angular и получать уведомления, когда пользователь входит в систему или выходит из нее.

Фальшивый серверный API

Приложение Angular по умолчанию работает с фальшивым бэкендом, чтобы он мог полностью работать в браузере без реального бэкэнд-API (без бэкэнда), чтобы переключиться на настоящий API, просто удалите или закомментируйте строку ниже используемого провайдера комментария // provider used to create fake backend, расположенного в модуле приложения (/src/app/app.module.ts).

Вы можете создать свой собственный API или подключить его к .NET API или Node.js API, доступным ниже.

Стилизовано с помощью Bootstrap 5

Пример приложения для входа оформлен с использованием CSS из Bootstrap 5.2, для получения дополнительной информации о Bootstrap см. https://getbootstrap.com/docs/5.2/getting-started/introduction/.

Код на GitHub

Учебный код доступен на GitHub по адресу https://github.com/cornflourblue/angular-14-role-based-authorization-example.

Вот он в действии:

См. на StackBlitz https://stackblitz.com/edit/angular-14-role-based-authorization-example
См. на StackBlitz https://stackblitz.com/edit/angular-14-role-based-authorization-example

Запустите пример авторизации на основе ролей Angular 14 локально

  1. Установите Node.js и npm из https://nodejs.org.
  2. Загрузите или клонируйте исходный код учебного проекта из https://github.com/cornflourblue/angular-14-role-based-authorization-example
  3. Установите все необходимые пакеты npm, запустив npm install или npm i из командной строки в корневой папке проекта (где находится package.json).
  4. Запустите приложение, запустив npm start из командной строки в корневой папке проекта, это создаст приложение и автоматически запустит его в браузере по URL-адресу http://localhost:4200.
Вы также можете запустить приложение с помощью команды Angular CLI ng serve --open. Для этого сначала установите Angular CLI глобально в вашей системе с помощью команды npm install -g @angular/cli.

Подключите приложение Angular к API .NET 6.0.

Чтобы быстро приступить к работе, просто выполните следующие шаги.

  1. Установите пакет SDK для .NET из https://dotnet.microsoft.com/download.
  2. Загрузите или клонируйте исходный код проекта с https://github.com/cornflourblue/dotnet-6-role-based-authorization-api
  3. Запустите API, запустив dotnet run из командной строки в корневой папке проекта (где находится файл WebApi.csproj). Вы должны увидеть сообщение Now listening on: http://localhost:4000.
  4. Вернувшись в приложение Angular, удалите или закомментируйте строку под комментарием // provider used to create fake backend, расположенного в файле /src/app/app.module.ts. Затем запустите приложение Angular, и теперь оно должно быть подключено к .NET API.

Подключите приложение Angular к API Node.js

Для того, чтобы быстро приступить к работе, просто выполните следующие шаги.

  1. Install NodeJS and NPM from https://nodejs.org.
  2. Загрузите или клонируйте исходный код проекта с https://github.com/cornflourblue/node-role-based-authorization-api
  3. Запустите API, запустив npm start из командной строки в корневой папке проекта, вы должны увидеть сообщение Server listening on port 4000.
  4. Вернувшись в приложение Angular, удалите или закомментируйте строку под комментарием // provider used to create fake backend, расположенного в файле /src/app/app.module.ts, затем запустите приложение Angular, и теперь оно должно быть подключено. с API аутентификации на основе ролей Node.js.

Структура проекта Angular 14

Angular CLI использовался для создания базовой структуры проекта с помощью команды ng new <project name>, CLI также используется для сборки и обслуживания приложения. Для получения дополнительной информации об Angular CLI см. https://angular.io/cli.

Структура приложения и кода учебника в основном соответствует рекомендациям из официального руководства по стилю Angular, с некоторыми моими собственными изменениями здесь и там.

Структура папок

Каждая функция имеет свою собственную папку (home, admin & login), другой общий код, такой как сервисы, модели, помощники и т.д., помещается в папки с префиксом подчеркивания _, чтобы легко различать их и группировать в верхней части структуры папок.

Бочкообразные файлы

Файлы index.ts в каждой папке представляют собой бочкообразные файлы, которые группируют экспортированные модули из папки вместе, чтобы их можно было импортировать, используя путь к папке, а не полный путь к модулю, и чтобы можно было импортировать несколько модулей в одном импорте (например, import { AuthenticationService, UserService } from '../_services').

Псевдонимы пути TypeScript

Псевдонимы путей @app и @environments были настроены в tsconfig.json, которые сопоставляются с каталогами /src/app и /src/environments. Это позволяет выполнять импорт относительно папок приложений и сред, добавляя к путям импорта префикс псевдонимов вместо использования длинных относительных путей (например, import MyComponent from '../../../MyComponent').

Вот основные файлы проекта, которые содержат логику приложения, мы пропустили некоторые файлы, которые были сгенерированы новой командой Angular CLI, которые мы не изменили.

  1. srcapp_helpersauth.guard.tserror.interceptor.tsfake-backend.tsjwt.interceptor.tsindex.ts_modelsrole.tsuser.tsindex.ts_servicesauthentication.service.tsuser.service.tsindex.tsadminadmin.component.htmladmin.component.tsindex.tshomehome.component.htmlhome.component.tsindex.tsloginlogin.component.htmllogin.component.tsindex.tsapp-routing.module.tsapp.component.htmlapp.component.tsapp.module.tsenvironmentsenvironment.prod.tsenvironment.tsindex.htmlmain.tspolyfills.ts
  2. package.json
  3. tsconfig.json

Защита авторизации

Путь: /src/app/_helpers/auth.guard.ts

Защита авторизации — это угловая защита маршрута, которая используется для предотвращения доступа неаутентифицированных или неавторизованных пользователей к ограниченным маршрутам. Она делает это путем реализации интерфейса CanActivate, который позволяет защите решать, можно ли активировать маршрут с помощью метода canActivate(). Если метод возвращает true, маршрут активируется (разрешается продолжение), в противном случае, если метод возвращает false, маршрут блокируется.

Защита авторизации использует службу аутентификации, чтобы проверить, вошел ли пользователь в систему, и если они вошли в систему, он проверяет, авторизована ли их роль для доступа к запрошенному маршруту. Если они вошли в систему и авторизованы, метод canActivate() возвращает true, в противном случае он возвращает false и перенаправляет пользователя на страницу входа.

Защита маршрута Angular прикреплена к маршрутам в конфигурации маршрутизатора, эта защита авторизации используется в app-routing.module.ts для защиты маршрутов домашней страницы и страницы администратора.

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AuthenticationService } from '@app/_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const user = this.authenticationService.userValue;
        if (user) {
            // check if route is restricted by role
            const { roles } = route.data;
            if (roles && !roles.includes(user.role)) {
                // role not authorized so redirect to home page
                this.router.navigate(['/']);
                return false;
            }

            // authorized so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}

Перехватчик ошибок

Путь: /src/app/_helpers/error.interceptor.ts

Перехватчик ошибок перехватывает ответы http от API, чтобы проверить, не было ли ошибок. Если есть ответ 401 Unauthorized или 403 Forbidden, пользователь автоматически выходит из приложения, все другие ошибки повторно выдаются вызывающей службе для обработки.

Он реализован с использованием интерфейса Angular HttpInterceptor, включенного в HttpClientModule. Благодаря реализации интерфейса HttpInterceptor вы можете создать собственный перехватчик для перехвата всех ответов об ошибках от API в одном месте.

Перехватчики HTTP добавляются в конвейер запросов в разделе provider файла app.module.ts.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AuthenticationService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if ([401, 403].includes(err.status) && this.authenticationService.userValue) {
                // auto logout if 401 Unauthorized or 403 Forbidden response returned from api
                this.authenticationService.logout();
            }

            const error = err.error.message || err.statusText;
            return throwError(() => error);
        }))
    }
}

Поддельный серверный провайдер

Путь: /src/app/_helpers/fake-backend.ts

Чтобы запустить и протестировать приложение Angular без реального бэкэнд-API, в примере используется фальшивый бэкэнд, который перехватывает HTTP-запросы из приложения Angular и отправляет обратно «фальшивые» ответы. Это делается классом, который реализует интерфейс Angular HttpInterceptor.

Поддельный бэкенд-провайдер содержит жестко закодированную коллекцию пользователей и предоставляет поддельные реализации для конечных точек API «аутентификация», «получение пользователя по идентификатору» и «получение всех пользователей», которые будут обрабатываться реальным API и базой данных в рабочем приложении. .

Маршрут «аутентификация» используется для входа в приложение и общедоступен, маршрут «получить пользователя по идентификатору» ограничен аутентифицированными пользователями в любой роли, однако обычные пользователи могут получить доступ только к своей собственной записи пользователя, в то время как пользователи с правами администратора могут получить доступ любая пользовательская запись. Маршрут «получить всех пользователей» ограничен только пользователями-администраторами.

Если запрос не соответствует ни одному из поддельных маршрутов, он передается как настоящий HTTP-запрос к серверному API.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, materialize, dematerialize } from 'rxjs/operators';

import { Role } from '@app/_models';

const users = [
    { id: 1, username: 'admin', password: 'admin', firstName: 'Admin', lastName: 'User', role: Role.Admin },
    { id: 2, username: 'user', password: 'user', firstName: 'Normal', lastName: 'User', role: Role.User }
];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        return handleRoute();

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/users') && method === 'GET':
                    return getUsers();
                case url.match(/\/users\/\d+$/) && method === 'GET':
                    return getUserById();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }

        }

        // route functions

        function authenticate() {
            const { username, password } = body;
            const user = users.find(x => x.username === username && x.password === password);
            if (!user) return error('Username or password is incorrect');
            return ok({
                id: user.id,
                username: user.username,
                firstName: user.firstName,
                lastName: user.lastName,
                role: user.role,
                token: `fake-jwt-token.${user.id}`
            });
        }

        function getUsers() {
            if (!isAdmin()) return unauthorized();
            return ok(users);
        }

        function getUserById() {
            if (!isLoggedIn()) return unauthorized();

            // only admins can access other user records
            if (!isAdmin() && currentUser()?.id !== idFromUrl()) return unauthorized();

            const user = users.find(x => x.id === idFromUrl());
            return ok(user);
        }

        // helper functions

        function ok(body: any) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500)); // delay observable to simulate server api call
        }

        function unauthorized() {
            return throwError(() => ({ status: 401, error: { message: 'unauthorized' } }))
                .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
        }

        function error(message: string) {
            return throwError(() => ({ status: 400, error: { message } }))
                .pipe(materialize(), delay(500), dematerialize());
        }

        function isLoggedIn() {
            const authHeader = headers.get('Authorization') || '';
            return authHeader.startsWith('Bearer fake-jwt-token');
        }

        function isAdmin() {
            return currentUser()?.role === Role.Admin;
        }

        function currentUser() {
            if (!isLoggedIn()) return;
            const id = parseInt(headers.get('Authorization')!.split('.')[1]);
            return users.find(x => x.id === id);
        }

        function idFromUrl() {
            const urlParts = url.split('/');
            return parseInt(urlParts[urlParts.length - 1]);
        }
    }
}

export const fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};

JWT-перехватчик

Путь: /src/app/_helpers/jwt.interceptor.ts

Перехватчик JWT перехватывает HTTP-запросы от приложения, чтобы добавить токен аутентификации JWT в заголовок авторизации, если пользователь вошел в систему и запрос относится к URL-адресу API приложения (environment.apiUrl).

Он реализован с использованием интерфейса HttpInterceptor, включенного в HttpClientModule. Благодаря реализации интерфейса HttpInterceptor вы можете создать собственный перехватчик для изменения HTTP-запросов до их отправки на сервер.

Перехватчики HTTP добавляются в конвейер запросов в разделе provider файла app.module.ts.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { environment } from '@environments/environment';
import { AuthenticationService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add auth header with jwt if user is logged in and request is to api url
        const user = this.authenticationService.userValue;
        const isLoggedIn = user?.token;
        const isApiUrl = request.url.startsWith(environment.apiUrl);
        if (isLoggedIn && isApiUrl) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${user.token}`
                }
            });
        }

        return next.handle(request);
    }
}

Модель Role

Путь: /src/app/_models/role.ts

Модель Role содержит перечисление, определяющее роли, поддерживаемые приложением.

export enum Role {
    User = 'User',
    Admin = 'Admin'
}

Модель User

Путь: /src/app/_models/user.ts

Модель пользователя — это небольшой интерфейс, который определяет свойства пользователя, включая его role и token? авторизации jwt. (знак вопроса в конце делает свойство необязательным в TypeScript).

import { Role } from "./role";

export interface User {
    id: number;
    firstName: string;
    lastName: string;
    username: string;
    role: Role;
    token?: string;
}

Служба аутентификации

Путь: /src/app/_services/authentication.service.ts

Служба аутентификации используется для входа и выхода из приложения Angular, она уведомляет другие компоненты, когда пользователь входит в систему и выходит из нее, и разрешает доступ пользователю, вошедшему в систему в данный момент.

Как сервис использует RxJS

RxJS Subjects и Observables используются для хранения текущего объекта пользователя и уведомления других компонентов, когда пользователь входит в приложение и выходит из него. Компоненты Angular могут subscribe() на общедоступного user: Observable будет уведомляться об изменениях, а уведомления отправляются, когда метод this.userSubject.next() вызывается в методах login() и logout(), передавая аргумент в каждого подписчика.

RxJS BehaviorSubject — это особый тип Subject, который удерживает текущее значение и передает его любым новым подписчикам, как только они подписываются, в то время как обычные Subjects не хранят текущее значение и выдают только те значения, которые публикуются после подписка создана.

Методы и свойства службы аутентификации

Метод login() отправляет учетные данные пользователя в API через HTTP-запрос POST для аутентификации. В случае успеха пользовательский объект, включая токен аутентификации JWT, сохраняется в localStorage, чтобы пользователь оставался в системе между обновлениями страницы. Затем пользовательский объект публикуется для всех подписчиков с вызовом this.userSubject.next(user);.

Метод logout() удаляет текущий объект пользователя из локального хранилища, публикует null в userSubject, чтобы уведомить всех подписчиков о том, что пользователь вышел из системы, и перенаправляет пользователя на страницу входа.

constructor() службы инициализирует userSubject объектом пользователя из localStorage, что позволяет пользователю оставаться в системе между обновлениями страницы или после закрытия браузера. Затем общедоступному user свойству присваивается значение this.userSubject.asObservable(); который позволяет другим компонентам подписываться на пользователя Observable, но не позволяет им публиковаться в userSubject, поэтому вход в приложение и выход из него можно выполнять только через службу аутентификации.

Геттер userValue позволяет другим компонентам легко получить значение текущего пользователя, вошедшего в систему, без необходимости подписываться на user Observable.

Если вам не нравится идея хранения текущих сведений о пользователе в локальном хранилище, все, что вам нужно сделать, это изменить 3 ссылки на localStorage в этом файле. Другими вариантами являются хранение сеанса, файлы cookie, или вы можете просто не сохранять данные пользователя в браузере, хотя имейте в виду, что с этим последним вариантом пользователь автоматически выйдет из системы, если обновит страницу.
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { User } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class AuthenticationService {
    private userSubject: BehaviorSubject<User | null>;
    public user: Observable<User | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
        this.user = this.userSubject.asObservable();
    }

    public get userValue() {
        return this.userSubject.value;
    }

    login(username: string, password: string) {
        return this.http.post<any>(`${environment.apiUrl}/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('user', JSON.stringify(user));
                this.userSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage to log user out
        localStorage.removeItem('user');
        this.userSubject.next(null);
        this.router.navigate(['/login']);
    }
}

Служба User

Путь: /src/app/_services/user.service.ts

Сервис user содержит всего пару методов для получения пользовательских данных из API, он действует как интерфейс между приложением Angular и внутренним API.

Мы включили службу user, чтобы продемонстрировать доступ к безопасным конечным точкам API с заголовком авторизации http, установленным после входа в приложение, заголовок аутентификации устанавливается с токеном JWT в перехватчике JWT. Защищенные конечные точки в примере — это фиктивные маршруты, реализованные в фальшивом бэкэнд-провайдере.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { environment } from '@environments/environment';
import { User } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class UserService {
    constructor(private http: HttpClient) { }

    getAll() {
        return this.http.get<User[]>(`${environment.apiUrl}/users`);
    }

    getById(id: number) {
        return this.http.get<User>(`${environment.apiUrl}/users/${id}`);
    }
}

Шаблон компонента Admin

Путь: /src/app/admin/admin.component.html

Шаблон компонента admin содержит html и синтаксис шаблона angular 14 для отображения списка всех пользователей, полученных из защищенной конечной точки API.

<div class="card mt-4">
    <h4 class="card-header">Admin</h4>
    <div class="card-body">
        <p>This page can be accessed <u>only by administrators</u>.</p>
        <p class="mb-1">All users from secure (admin only) api end point:</p>
        <div *ngIf="loading" class="spinner-border spinner-border-sm"></div>
        <ul *ngIf="users">
            <li *ngFor="let user of users">{{user.firstName}} {{user.lastName}}</li>
        </ul>
    </div>
</div>

Компонент Admin

Путь: /src/app/admin/admin.component.ts

Компонент admin вызывает службу пользователей, чтобы получить всех пользователей из защищенной конечной точки API и назначить их свойству локальных users, которое доступно из шаблона компонента admin выше.

import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';

import { User } from '@app/_models';
import { UserService } from '@app/_services';

@Component({ templateUrl: 'admin.component.html' })
export class AdminComponent implements OnInit {
    loading = false;
    users: User[] = [];

    constructor(private userService: UserService) { }

    ngOnInit() {
        this.loading = true;
        this.userService.getAll().pipe(first()).subscribe(users => {
            this.loading = false;
            this.users = users;
        });
    }
}

Шаблон компонента Home

Путь: /src/app/home/home.component.html

Шаблон компонента home содержит синтаксис шаблона html и angular 14 для отображения простого приветственного сообщения и текущей записи пользователя, полученной из защищенной конечной точки API.

<div class="card mt-4">
    <h4 class="card-header">Home</h4>
    <div class="card-body">
        <p>You're logged in with Angular 14 & JWT!!</p>
        <p>Your role is: <strong>{{user.role}}</strong>.</p>
        <p>This page can be accessed by <u>all authenticated users</u>.</p>
        <p class="mb-1">Current user from secure api end point:</p>
        <div *ngIf="loading" class="spinner-border spinner-border-sm"></div>
        <ul *ngIf="userFromApi">
            <li>{{userFromApi.firstName}} {{userFromApi.lastName}}</li>
        </ul>
    </div>
</div>

Компонент Home

Путь: /src/app/home/home.component.ts

Компонент home получает текущего пользователя из службы аутентификации, а затем получает текущего пользователя из API с помощью пользовательской службы. Нам действительно нужно было получить пользователя только из службы аутентификации, но я включил получение его из службы пользователей, а также демонстрацию получения данных из защищенной конечной точки API.

import { Component } from '@angular/core';
import { first } from 'rxjs/operators';

import { User } from '@app/_models';
import { UserService, AuthenticationService } from '@app/_services';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
    loading = false;
    user: User;
    userFromApi?: User;

    constructor(
        private userService: UserService,
        private authenticationService: AuthenticationService
    ) {
        this.user = <User>this.authenticationService.userValue;
    }

    ngOnInit() {
        this.loading = true;
        this.userService.getById(this.user.id).pipe(first()).subscribe(user => {
            this.loading = false;
            this.userFromApi = user;
        });
    }
}

Шаблон компонента Login

Путь: /src/app/login/login.component.html

Шаблон компонента login содержит форму входа с полями имени пользователя и пароля. Он отображает сообщения проверки для недопустимых полей при нажатии кнопки отправки. Событие отправки формы привязано к методу onSubmit() компонента входа.

<div class="col-md-6 offset-md-3 mt-5">
    <div class="alert alert-info">
        <strong>Normal User</strong> - U: user P: user<br />
        <strong>Administrator</strong> - U: admin P: admin
    </div>
    <div class="card">
        <h4 class="card-header">Angular 14 Role Based Auth Example</h4>
        <div class="card-body">
            <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
                <div class="mb-3">
                    <label class="form-label">Username</label>
                    <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
                    <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
                        <div *ngIf="f.username.errors.required">Username is required</div>
                    </div>
                </div>
                <div class="mb-3">
                    <label class="form-label">Password</label>
                    <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                    <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                        <div *ngIf="f.password.errors.required">Password is required</div>
                    </div>
                </div>
                <button [disabled]="loading" class="btn btn-primary">
                    <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                    Login
                </button>
                <div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
            </form>
        </div>
    </div>
</div>

Компонент Login

Путь: /src/app/login/login.component.ts

Компонент login использует службу аутентификации для входа в приложение. Если пользователь уже авторизовался, он автоматически перенаправляется на главную страницу.

Объект loginForm: FormGroup определяет элементы управления и средства проверки формы и используется для доступа к данным, введенным в форму. FormGroup является частью модуля Angular Reactive Forms и привязан к шаблону входа выше с помощью директивы formGroup ([formGroup]="loginForm").

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AuthenticationService } from '@app/_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
    loginForm!: FormGroup;
    loading = false;
    submitted = false;
    error = '';

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private authenticationService: AuthenticationService
    ) {
        // redirect to home if already logged in
        if (this.authenticationService.userValue) {
            this.router.navigate(['/']);
        }
    }

    ngOnInit() {
        this.loginForm = this.formBuilder.group({
            username: ['', Validators.required],
            password: ['', Validators.required]
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.loginForm.controls; }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.loginForm.invalid) {
            return;
        }

        this.loading = true;
        this.authenticationService.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    // get return url from query parameters or default to home page
                    const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                    this.router.navigateByUrl(returnUrl);
                },
                error: error => {
                    this.error = error;
                    this.loading = false;
                }
            });
    }
}

Модуль маршрутизации приложений

Путь: /src/app/app-routing.module.ts

Маршрутизация для приложения Angular настроена как массив Routes, каждый компонент сопоставляется с путем, поэтому Angular Router знает, какой компонент отображать, на основе URL-адреса в адресной строке браузера. Домашний и административный маршруты защищены путем передачи AuthGuard в свойство canActivate маршрута. Маршрут администратора также устанавливает для свойства данных roles значение [Role.Admin], поэтому доступ к нему могут получить только пользователи с правами администратора.

Массив Routes передается методу RouterModule.forRoot(), который создает модуль маршрутизации со всеми настроенными маршрутами приложения, а также включает всех поставщиков и директивы Angular Router, такие как <router-outlet></router-outlet > директива. Для получения дополнительной информации об Angular Routing и навигации см. https://angular.io/guide/router.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home';
import { AdminComponent } from './admin';
import { LoginComponent } from './login';
import { AuthGuard } from './_helpers';
import { Role } from './_models';

const routes: Routes = [
    {
        path: '',
        component: HomeComponent,
        canActivate: [AuthGuard]
    },
    {
        path: 'admin',
        component: AdminComponent,
        canActivate: [AuthGuard],
        data: { roles: [Role.Admin] }
    },
    {
        path: 'login',
        component: LoginComponent
    },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Шаблон компонента App

Путь: /src/app/app.component.html

Шаблон компонента приложения является корневым шаблоном компонента приложения, он содержит основную панель навигации, которая отображается только для аутентифицированных пользователей, и директиву router-outlet для отображения содержимого каждого представления на основе текущего маршрута/пути.

Панель навигации содержит ссылки на домашнюю страницу, страницу администратора и ссылку для выхода. Ссылка администратора отображается только для пользователей с ролью Admin при использовании геттера isAdmin в директиве *ngIf. Ссылка для выхода вызывает метод logout() компонента приложения при щелчке.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="user">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
        <a class="nav-item nav-link" routerLink="/admin" routerLinkActive="active" *ngIf="isAdmin">Admin</a>
        <button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button>
    </div>
</nav>

<!-- main app container -->
<div class="container">
    <router-outlet></router-outlet>
</div>

Компонент App

Путь: /src/app/app.component.ts

Компонент app является корневым компонентом приложения, он определяет корневой тег приложения как <app-root></app-root> со свойством selector декоратора @Component().

Он подписывается на user, наблюдаемого в службе аутентификации, чтобы он мог реактивно показывать/скрывать главную панель навигации, когда пользователь входит/выходит из приложения. Обычно рекомендуется отказаться от подписки, когда компонент будет уничтожен, чтобы избежать утечек памяти. В этом нет необходимости для корневого компонента приложения, поскольку он будет уничтожен только при закрытии всего приложения.

Компонент app содержит метод logout(), который вызывается по ссылке выхода из главной панели навигации выше, чтобы выйти из системы и перенаправить пользователя на страницу входа. Геттер isAdmin() возвращает true, если вошедший в систему пользователь находится в роли Admin, или false для пользователей, не являющихся администраторами.

import { Component } from '@angular/core';

import { AuthenticationService } from './_services';
import { User, Role } from './_models';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    user?: User | null;

    constructor(private authenticationService: AuthenticationService) {
        this.authenticationService.user.subscribe(x => this.user = x);
    }

    get isAdmin() {
        return this.user?.role === Role.Admin;
    }

    logout() {
        this.authenticationService.logout();
    }
}

Модуль App

Путь: /src/app/app.module.ts

Модуль app определяет корневой модуль приложения вместе с метаданными о модуле.

Здесь в приложение добавляется поддельный бэкенд-провайдер, чтобы переключиться на настоящий бэкэнд, просто удалите провайдеров, расположенных под комментарием // provider used to create fake backend.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { HomeComponent } from './home';
import { AdminComponent } from './admin';
import { LoginComponent } from './login';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        AdminComponent,
        LoginComponent
    ],
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})

export class AppModule { }

Конфигурация производственной среды

Путь: /src/environments/environment.prod.ts

Конфигурация производственной среды содержит переменные, необходимые для запуска приложения в производственной среде. Это позволяет вам создавать приложение с различной конфигурацией для каждой среды (например, производства и разработки) без обновления кода приложения.

Когда вы создаете приложение для производства с помощью команды ng build --prod, выходной файл environment.ts заменяется на environment.prod.ts.

export const environment = {
    production: true,
    apiUrl: 'http://localhost:4000'
};

Конфигурация среды разработки

Путь: /src/environments/environment.ts

Конфигурация среды разработки содержит переменные, необходимые для запуска приложения в процессе разработки.

Доступ к конфигурации среды осуществляется путем импорта объекта среды в любую службу или компонент Angular с помощью строки import { environment } from '@environments/environment' и доступа к свойствам объекта environment, см. пример пользовательской службы.

export const environment = {
    production: false,
    apiUrl: 'http://localhost:4000'
};

HTML-файл основного индекса

Путь: /src/index.html

Основной файл index.html — это начальная страница, загружаемая браузером, с которой все начинается. Angular CLI (с Webpack под капотом) объединяет все скомпилированные файлы javascript вместе и внедряет их в тело страницы index.html, чтобы скрипты могли быть загружены и выполнены браузером.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 14 - Role Based Authorization Tutorial with Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <app-root></app-root>
</body>
</html>

Основной (загрузочный) файл

Путь: /src/main.ts

Основной файл — это точка входа, используемая angular для запуска и начальной загрузки приложения.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));

Полифиллы

Путь: /src/polyfills.ts

Некоторые функции, используемые Angular, еще не поддерживаются изначально всеми основными браузерами, полифиллы используются для добавления поддержки функций там, где это необходимо, чтобы ваше приложение Angular 14 работало во всех основных браузерах.

Этот файл создается Angular CLI при создании нового проекта с помощью команды ng new, для краткости мы исключили комментарии в файле.

import 'zone.js';  // Included with Angular CLI.

Package.json

Путь: /package.json

Файл package.json содержит информацию о конфигурации проекта, включая зависимости пакетов, которые устанавливаются при запуске npm install, и скрипты, которые выполняются при запуске npm start или npm run build и т. д. Полная документация доступна по адресу https://docs.npmjs.com/files/package.json.

{
    "name": "angular-14-example",
    "version": "0.0.0",
    "scripts": {
        "ng": "ng",
        "start": "ng serve --open",
        "build": "ng build",
        "watch": "ng build --watch --configuration development",
        "test": "ng test"
    },
    "private": true,
    "dependencies": {
        "@angular/animations": "^14.2.0",
        "@angular/common": "^14.2.0",
        "@angular/compiler": "^14.2.0",
        "@angular/core": "^14.2.0",
        "@angular/forms": "^14.2.0",
        "@angular/platform-browser": "^14.2.0",
        "@angular/platform-browser-dynamic": "^14.2.0",
        "@angular/router": "^14.2.0",
        "rxjs": "~7.5.0",
        "tslib": "^2.3.0",
        "zone.js": "~0.11.4"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^14.2.8",
        "@angular/cli": "~14.2.8",
        "@angular/compiler-cli": "^14.2.0",
        "@types/jasmine": "~4.0.0",
        "jasmine-core": "~4.3.0",
        "karma": "~6.4.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage": "~2.2.0",
        "karma-jasmine": "~5.1.0",
        "karma-jasmine-html-reporter": "~2.0.0",
        "typescript": "~4.7.2"
    }
}

TypeScript tsconfig.json

Путь: /tsconfig.json

Файл tsconfig.json содержит базовую конфигурацию компилятора TypeScript для всех проектов в рабочей области Angular, он настраивает, как код TypeScript будет компилироваться/преобразовываться в JavaScript, понятный браузеру.

Большая часть файла не изменилась с момента его создания с помощью Angular CLI, было добавлено только свойство paths для сопоставления псевдонимов @app и @environments с каталогами /src/app и /src/environments. Это позволяет выполнять импорт относительно папок приложений и сред, добавляя к путям импорта префиксы псевдонимов вместо использования длинных относительных путей (например, import MyComponent from '@app/MyComponent' вместо import MyComponent from '../../../MyComponent').

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "allowSyntheticDefaultImports": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": false,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "es2020",
        "module": "es2020",
        "lib": [
            "es2020",
            "dom"
        ],
        "paths": {
            "@app/*": ["src/app/*"],
            "@environments/*": ["src/environments/*"]
        }
    },
    "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
    }
}
#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу