name: delon-auth-authentication-authorization description: “使用 @delon/auth 实现认证和授权。当添加登录/登出流程、JWT令牌管理、基于角色的访问控制(RBAC)、路由守卫、HTTP拦截器和会话管理时,使用此技能。与Firebase Auth和自定义权限系统集成。确保安全的令牌存储、自动令牌刷新和跨组件和服务的一致授权检查。” license: “MIT”
@delon/auth 认证与授权技能
此技能有助于使用 @delon/auth 库实现认证和授权。
核心原则
令牌管理
- JWT存储:使用 DA_SERVICE_TOKEN 安全存储令牌
- 自动刷新:在令牌过期前自动更新
- 拦截器:在HTTP请求中自动注入令牌
- 登出:清除令牌和会话清理
授权
- RBAC:基于角色的访问控制
- 权限:细粒度的权限检查
- 守卫:带有角色/权限检查的路由保护
- ACL集成:与 @delon/acl 一起工作,用于UI级控制
配置
应用配置
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideDelonAuth, DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { JWTTokenModel } from '@delon/auth';
export const appConfig: ApplicationConfig = {
providers: [
provideDelonAuth({
token_send_key: 'Authorization',
token_send_template: 'Bearer ${token}',
token_send_place: 'header',
login_url: '/passport/login',
ignores: [/\/login/, /assets\//],
allow_anonymous_key: 'allow_anonymous',
executeOtherInterceptors: true,
refreshTime: 3000, // 刷新令牌检查间隔(ms)
refreshOffset: 6000 // 令牌过期前刷新(ms)
})
]
};
令牌模型
// src/app/core/models/auth-token.model.ts
import { JWTTokenModel } from '@delon/auth';
export interface AuthTokenModel extends JWTTokenModel {
token: string;
uid: string;
email: string;
displayName: string;
role: string;
permissions: string[];
exp: number;
}
认证服务
// src/app/core/services/auth.service.ts
import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { Auth, signInWithEmailAndPassword, signOut, User } from '@angular/fire/auth';
import { AuthTokenModel } from '@core/models/auth-token.model';
import { signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private auth = inject(Auth);
private tokenService = inject<ITokenService>(DA_SERVICE_TOKEN);
private router = inject(Router);
// 状态
private currentUser = signal<User | null>(null);
private authToken = signal<AuthTokenModel | null>(null);
// 计算属性
isAuthenticated = computed(() => this.tokenService.get()?.token != null);
userRole = computed(() => this.authToken()?.role);
userPermissions = computed(() => this.authToken()?.permissions || []);
constructor() {
// 监听 Firebase 认证状态
this.auth.onAuthStateChanged(async (user) => {
this.currentUser.set(user);
if (user) {
await this.refreshToken();
} else {
this.clearSession();
}
});
}
/**
* 使用电子邮件和密码登录
*/
async login(email: string, password: string): Promise<void> {
try {
const credential = await signInWithEmailAndPassword(this.auth, email, password);
const token = await credential.user.getIdToken();
const tokenData = await this.parseToken(token);
// 在 @delon/auth 中存储令牌
this.tokenService.set(tokenData);
this.authToken.set(tokenData);
// 导航到仪表板
await this.router.navigateByUrl('/dashboard');
} catch (error) {
console.error('登录失败:', error);
throw error;
}
}
/**
* 登出
*/
async logout(): Promise<void> {
try {
await signOut(this.auth);
this.clearSession();
await this.router.navigateByUrl('/passport/login');
} catch (error) {
console.error('登出失败:', error);
throw error;
}
}
/**
* 刷新令牌
*/
async refreshToken(): Promise<void> {
const user = this.currentUser();
if (!user) return;
try {
const token = await user.getIdToken(true); // 强制刷新
const tokenData = await this.parseToken(token);
this.tokenService.set(tokenData);
this.authToken.set(tokenData);
} catch (error) {
console.error('令牌刷新失败:', error);
await this.logout();
}
}
/**
* 检查用户是否具有特定角色
*/
hasRole(role: string): boolean {
return this.userRole() === role;
}
/**
* 检查用户是否具有特定权限
*/
hasPermission(permission: string): boolean {
return this.userPermissions().includes(permission);
}
/**
* 检查用户是否具有指定的任何权限
*/
hasAnyPermission(permissions: string[]): boolean {
const userPerms = this.userPermissions();
return permissions.some(p => userPerms.includes(p));
}
/**
* 检查用户是否具有所有指定的权限
*/
hasAllPermissions(permissions: string[]): boolean {
const userPerms = this.userPermissions();
return permissions.every(p => userPerms.includes(p));
}
/**
* 解析JWT令牌
*/
private async parseToken(token: string): Promise<AuthTokenModel> {
// 解码JWT负载
const payload = JSON.parse(atob(token.split('.')[1]));
// 从Firestore加载用户权限(自定义声明)
const permissions = await this.loadUserPermissions(payload.uid);
return {
token,
uid: payload.uid,
email: payload.email,
displayName: payload.name || payload.email,
role: payload.role || 'member',
permissions,
exp: payload.exp
};
}
/**
* 从Firestore加载用户权限
*/
private async loadUserPermissions(uid: string): Promise<string[]> {
// 实现:根据你的模式从Firestore查询用户的权限
// 这是一个占位符 - 根据需要实现
return ['read:tasks', 'write:tasks'];
}
/**
* 清除会话
*/
private clearSession(): void {
this.tokenService.clear();
this.authToken.set(null);
this.currentUser.set(null);
}
}
认证守卫
// src/app/core/guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '@core/services/auth.service';
/**
* 保护需要认证的路由的守卫
*/
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/passport/login']);
};
角色守卫
// src/app/core/guards/role.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn, ActivatedRouteSnapshot } from '@angular/router';
import { AuthService } from '@core/services/auth.service';
/**
* 按角色保护路由的守卫
*/
export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
const authService = inject(AuthService);
const router = inject(Router);
const requiredRole = route.data['role'] as string;
if (!requiredRole) {
console.warn('角色守卫使用时未指定所需角色');
return true;
}
if (authService.hasRole(requiredRole)) {
return true;
}
// 重定向到未授权页面
return router.createUrlTree(['/exception/403']);
};
/**
* 在路由中的使用:
* {
* path: 'admin',
* component: AdminComponent,
* canActivate: [authGuard, roleGuard],
* data: { role: 'admin' }
* }
*/
权限守卫
// src/app/core/guards/permission.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn, ActivatedRouteSnapshot } from '@angular/router';
import { AuthService } from '@core/services/auth.service';
/**
* 按权限保护路由的守卫
*/
export const permissionGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
const authService = inject(AuthService);
const router = inject(Router);
const requiredPermissions = route.data['permissions'] as string[];
const requireAll = route.data['requireAll'] as boolean ?? true;
if (!requiredPermissions || requiredPermissions.length === 0) {
console.warn('权限守卫使用时未指定所需权限');
return true;
}
const hasAccess = requireAll
? authService.hasAllPermissions(requiredPermissions)
: authService.hasAnyPermission(requiredPermissions);
if (hasAccess) {
return true;
}
return router.createUrlTree(['/exception/403']);
};
/**
* 在路由中的使用:
* {
* path: 'tasks',
* component: TaskListComponent,
* canActivate: [authGuard, permissionGuard],
* data: {
* permissions: ['read:tasks'],
* requireAll: true
* }
* }
*/
登录组件
// src/app/routes/passport/login/login.component.ts
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '@core/services/auth.service';
import { SHARED_IMPORTS } from '@shared';
@Component({
selector: 'app-login',
standalone: true,
imports: [SHARED_IMPORTS],
template: `
<div class="login-container">
<nz-card>
<h2>登录</h2>
<form nz-form [formGroup]="loginForm" (ngSubmit)="handleLogin()">
<nz-form-item>
<nz-form-control nzErrorTip="请输入您的电子邮件!">
<nz-input-group nzPrefixIcon="mail">
<input
nz-input
formControlName="email"
placeholder="电子邮件"
type="email"
/>
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzErrorTip="请输入您的密码!">
<nz-input-group nzPrefixIcon="lock">
<input
nz-input
formControlName="password"
placeholder="密码"
type="password"
/>
</nz-input-group>
</nz-form-control>
</nz-form-item>
@if (error()) {
<nz-alert
nzType="error"
[nzMessage]="error()!"
nzShowIcon
/>
}
<button
nz-button
nzType="primary"
nzBlock
[nzLoading]="loading()"
[disabled]="loginForm.invalid"
>
登录
</button>
</form>
</nz-card>
</div>
`
})
export class LoginComponent {
private fb = inject(FormBuilder);
private authService = inject(AuthService);
loading = signal(false);
error = signal<string | null>(null);
loginForm: FormGroup = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
async handleLogin(): Promise<void> {
if (this.loginForm.invalid) return;
this.loading.set(true);
this.error.set(null);
try {
const { email, password } = this.loginForm.value;
await this.authService.login(email, password);
} catch (err) {
this.error.set(err instanceof Error ? err.message : '登录失败');
} finally {
this.loading.set(false);
}
}
}
HTTP拦截器(自动令牌注入)
@delon/auth 在配置时自动提供拦截器。无需手动实现!
// 所有HTTP请求中自动注入令牌
// 在app.config.ts中通过provideDelonAuth()配置
// 示例HTTP调用 - 自动添加令牌
this.http.get('/api/tasks').subscribe(tasks => {
// 授权:Bearer {token} 头自动添加
});
组件级权限检查
import { Component, inject, computed } from '@angular/core';
import { AuthService } from '@core/services/auth.service';
@Component({
selector: 'app-task-list',
template: `
@if (canCreate()) {
<button nz-button nzType="primary" (click)="createTask()">
创建任务
</button>
}
@if (canEdit()) {
<button nz-button (click)="editTask()"编辑</button>
}
@if (canDelete()) {
<button nz-button nzDanger (click)="deleteTask()"删除</button>
}
`
})
export class TaskListComponent {
private authService = inject(AuthService);
canCreate = computed(() => this.authService.hasPermission('create:tasks'));
canEdit = computed(() => this.authService.hasPermission('edit:tasks'));
canDelete = computed(() => this.authService.hasPermission('delete:tasks'));
}
与@delon/acl集成
// src/app/app.config.ts
import { provideDelonACL } from '@delon/acl';
export const appConfig: ApplicationConfig = {
providers: [
provideDelonACL()
]
};
// 将权限同步到ACL
import { ACLService } from '@delon/acl';
@Injectable({ providedIn: 'root' })
export class AuthService {
private aclService = inject(ACLService);
private syncPermissionsToACL(permissions: string[], role: string): void {
this.aclService.setRole([role]);
this.aclService.setAbility(permissions);
}
}
路由配置
// src/app/routes/routes.ts
import { Routes } from '@angular/router';
import { authGuard } from '@core/guards/auth.guard';
import { roleGuard } from '@core/guards/role.guard';
import { permissionGuard } from '@core/guards/permission.guard';
export const routes: Routes = [
{
path: '',
canActivate: [authGuard],
children: [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
},
{
path: 'admin',
canActivate: [roleGuard],
data: { role: 'admin' },
loadComponent: () => import('./admin/admin.component')
},
{
path: 'tasks',
canActivate: [permissionGuard],
data: {
permissions: ['read:tasks'],
requireAll: true
},
loadComponent: () => import('./tasks/task-list.component')
}
]
},
{
path: 'passport',
children: [
{
path: 'login',
loadComponent: () => import('./passport/login/login.component')
}
]
}
];
检查清单
实现认证时:
- [ ] 在 app.config.ts 中配置 @delon/auth
- [ ] 创建具有令牌管理的 AuthService
- [ ] 实现登录/登出方法
- [ ] 设置令牌刷新机制
- [ ] 为受保护的路由创建认证守卫
- [ ] 根据需要创建角色/权限守卫
- [ ] 与 Firebase Auth 集成
- [ ] 将权限与 @delon/acl 同步
- [ ] 处理令牌过期
- [ ] 测试认证流程
- [ ] 测试授权检查
- [ ] 处理错误状态(无效凭据、网络错误)