MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

使用TypeScript装饰器进行权限验证

2021-11-285.1k 阅读

什么是TypeScript装饰器

在深入探讨使用TypeScript装饰器进行权限验证之前,我们先来了解一下TypeScript装饰器是什么。装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、属性或参数上。装饰器使用 @expression 这种形式,其中 expression 是一个表达式,在运行时会被求值,求值结果必须是一个函数,这个函数会在装饰器应用的声明上执行。

装饰器的基本类型

  1. 类装饰器:类装饰器应用于类的定义。它接收一个参数,这个参数是被装饰类的构造函数。例如:
function classDecorator(constructor: Function) {
    console.log('类装饰器被调用,构造函数:', constructor);
}

@classDecorator
class MyClass {
    constructor() {
        console.log('MyClass 实例化');
    }
}

在上述代码中,当 MyClass 被定义时,classDecorator 函数就会被调用,并且传入 MyClass 的构造函数作为参数。

  1. 方法装饰器:方法装饰器应用于类的方法。它接收三个参数:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;成员的名字;成员的属性描述符。例如:
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log('方法被调用前');
        const result = originalMethod.apply(this, args);
        console.log('方法被调用后');
        return result;
    };
    return descriptor;
}

class MethodExample {
    @methodDecorator
    myMethod() {
        console.log('执行 myMethod');
    }
}

const example = new MethodExample();
example.myMethod();

在这段代码中,methodDecorator 装饰器修改了 myMethod 的行为,在方法执行前后打印日志。

  1. 属性装饰器:属性装饰器应用于类的属性。它接收两个参数:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;成员的名字。例如:
function propertyDecorator(target: any, propertyKey: string) {
    let value;
    Object.defineProperty(target, propertyKey, {
        get() {
            console.log('获取属性值');
            return value;
        },
        set(newValue) {
            console.log('设置属性值');
            value = newValue;
        }
    });
}

class PropertyExample {
    @propertyDecorator
    myProperty;
}

const propertyExample = new PropertyExample();
propertyExample.myProperty = 'Hello';
console.log(propertyExample.myProperty);

这里,propertyDecorator 装饰器为 myProperty 属性添加了自定义的存取器,在获取和设置属性值时打印日志。

  1. 参数装饰器:参数装饰器应用于类方法的参数。它接收三个参数:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象;成员的名字;参数在函数参数列表中的索引。例如:
function parameterDecorator(target: any, propertyKey: string, parameterIndex: number) {
    console.log(`参数装饰器在 ${propertyKey} 方法的第 ${parameterIndex} 个参数上`);
}

class ParameterExample {
    myFunction(@parameterDecorator param: string) {
        console.log('执行 myFunction,参数:', param);
    }
}

const parameterExample = new ParameterExample();
parameterExample.myFunction('test');

此代码中,parameterDecorator 装饰器在 myFunction 方法的参数上被调用,并打印相关信息。

为什么使用装饰器进行权限验证

在开发应用程序时,权限验证是一个至关重要的部分。它确保只有具有适当权限的用户能够访问特定的功能或资源。传统的权限验证方式通常是在每个需要验证的方法内部进行检查,例如:

class UserService {
    constructor(private userRole: string) {}

    public importantFunction() {
        if (this.userRole === 'admin') {
            console.log('执行重要功能');
        } else {
            console.log('权限不足');
        }
    }
}

const userService = new UserService('user');
userService.importantFunction();

这种方式存在一些问题:

  1. 代码重复:如果有多个方法都需要权限验证,那么在每个方法内部都要重复编写权限验证逻辑。
  2. 可维护性差:当权限验证逻辑发生变化时,需要在多个地方进行修改,容易出错。

而使用装饰器进行权限验证可以很好地解决这些问题。装饰器允许我们将权限验证逻辑从业务逻辑中分离出来,通过统一的装饰器来处理权限验证,使得代码更加清晰、可维护。同时,装饰器可以复用,不同的类和方法可以使用相同的权限验证装饰器,提高了代码的复用性。

使用TypeScript装饰器实现权限验证

简单的权限验证装饰器

我们先从一个简单的权限验证装饰器开始。假设我们有两种权限角色:adminuser,只有 admin 角色的用户能够访问某些特定方法。

function requireAdmin(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
        if (this.userRole === 'admin') {
            return originalMethod.apply(this, args);
        } else {
            console.log('权限不足');
        }
    };
    return descriptor;
}

class AdminService {
    constructor(private userRole: string) {}

    @requireAdmin
    public manageSystem() {
        console.log('执行系统管理操作');
    }
}

const adminService = new AdminService('user');
adminService.manageSystem();

在上述代码中,requireAdmin 装饰器检查调用 manageSystem 方法的对象的 userRole 是否为 admin。如果是,则执行原方法;否则,打印权限不足的提示。

支持多种权限角色的装饰器

实际应用中,可能会有多种权限角色,并且不同的方法可能需要不同的权限。我们可以通过参数化装饰器来实现这一点。

function requireRole(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            if (this.userRole === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                console.log('权限不足');
            }
        };
        return descriptor;
    };
}

class MultiRoleService {
    constructor(private userRole: string) {}

    @requireRole('admin')
    public manageSystem() {
        console.log('执行系统管理操作');
    }

    @requireRole('editor')
    public editContent() {
        console.log('执行内容编辑操作');
    }
}

const multiRoleService = new MultiRoleService('editor');
multiRoleService.manageSystem();
multiRoleService.editContent();

这里,requireRole 是一个高阶函数,它接收一个 requiredRole 参数,并返回一个真正的装饰器函数。这样,我们可以在不同的方法上使用不同的权限要求,如 manageSystem 方法需要 admin 权限,editContent 方法需要 editor 权限。

在类级别应用权限验证

除了在方法级别进行权限验证,有时我们可能需要在类级别进行权限验证。例如,只有具有特定权限的用户才能实例化某个类。

function requireClassRole(requiredRole: string) {
    return function (constructor: Function) {
        return class extends constructor {
            constructor(...args: any[]) {
                if (args[0] === requiredRole) {
                    super(...args);
                } else {
                    console.log('权限不足,无法实例化类');
                }
            }
        };
    };
}

@requireClassRole('admin')
class AdminOnlyClass {
    constructor(private userRole: string) {
        console.log('AdminOnlyClass 实例化');
    }
}

const tryInstance = new AdminOnlyClass('user');

在这段代码中,requireClassRole 装饰器在类定义时进行权限验证。如果传入的角色与 requiredRole 不匹配,则阻止类的实例化。

结合依赖注入和权限验证

在实际的企业级应用开发中,依赖注入是一种常用的设计模式。我们可以将权限验证装饰器与依赖注入结合起来,使权限验证更加灵活和可管理。假设我们使用 InversifyJS 作为依赖注入容器。 首先,安装 inversifyreflect - metadata

npm install inversify reflect - metadata

然后,配置依赖注入和权限验证:

import "reflect - metadata";
import { Container } from "inversify";

function requireRole(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            const userRole = this.userRole;
            if (userRole === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                console.log('权限不足');
            }
        };
        return descriptor;
    };
}

interface IUserService {
    importantFunction(): void;
}

class UserService implements IUserService {
    constructor(private userRole: string) {}

    @requireRole('admin')
    public importantFunction() {
        console.log('执行重要功能');
    }
}

const container = new Container();
container.bind<IUserService>('IUserService').to(UserService).inSingletonScope();

const userService = container.get<IUserService>('IUserService');
userService.importantFunction();

在这个例子中,我们通过 InversifyJS 容器来管理 UserService 的实例化。requireRole 装饰器依然负责权限验证,这样可以将权限验证与依赖注入的优势结合起来,提高代码的可维护性和可扩展性。

异步方法的权限验证

在实际开发中,很多方法可能是异步的,例如通过 async/await 实现的异步操作。我们需要确保装饰器也能正确处理异步方法的权限验证。

function requireRoleAsync(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = async function (...args: any[]) {
            if (this.userRole === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                console.log('权限不足');
            }
        };
        return descriptor;
    };
}

class AsyncService {
    constructor(private userRole: string) {}

    @requireRoleAsync('admin')
    public async asyncImportantFunction() {
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log('执行异步重要功能');
    }
}

const asyncService = new AsyncService('admin');
asyncService.asyncImportantFunction();

这里,requireRoleAsync 装饰器同样检查权限,但由于方法是异步的,它确保在权限验证通过后,正确执行异步操作。

全局权限验证中间件(模拟)

在一些大型应用中,可能需要一个全局的权限验证机制,类似于Web应用中的中间件。我们可以通过创建一个全局的装饰器注册机制来模拟这种行为。

const roleDecorators: { [key: string]: (target: any, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor } = {};

function registerRoleDecorator(role: string, decorator: (target: any, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor) {
    roleDecorators[role] = decorator;
}

function applyGlobalRoleDecorators(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const userRole = target.userRole;
    if (roleDecorators[userRole]) {
        return roleDecorators[userRole](target, propertyKey, descriptor);
    }
    return descriptor;
}

function requireRoleGlobal(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            if (this.userRole === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                console.log('权限不足');
            }
        };
        return descriptor;
    };
}

registerRoleDecorator('admin', requireRoleGlobal('admin'));

class GlobalRoleService {
    constructor(private userRole: string) {}

    @applyGlobalRoleDecorators
    public globalImportantFunction() {
        console.log('执行全局重要功能');
    }
}

const globalRoleService = new GlobalRoleService('admin');
globalRoleService.globalImportantFunction();

在这段代码中,我们通过 registerRoleDecorator 注册不同角色的装饰器,然后在 applyGlobalRoleDecorators 中根据用户角色应用相应的装饰器。这样就模拟了一个全局权限验证中间件的行为。

与前端框架结合使用权限验证装饰器

在前端开发中,我们通常会使用像 ReactVue 等框架。以 React 为例,我们可以将权限验证装饰器应用于 React 组件的方法上。 首先,创建一个 withAuth 高阶组件来处理权限验证:

import React from'react';

function requireRole(requiredRole: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            const userRole = this.props.userRole;
            if (userRole === requiredRole) {
                return originalMethod.apply(this, args);
            } else {
                console.log('权限不足');
            }
        };
        return descriptor;
    };
}

function withAuth(ComposedComponent: React.ComponentType<any>) {
    return class extends React.Component<any, any> {
        @requireRole('admin')
        public handleClick() {
            console.log('执行点击操作');
        }

        render() {
            return <ComposedComponent {...this.props} handleClick={this.handleClick.bind(this)} />;
        }
    };
}

const MyButton = ({ handleClick }: { handleClick: () => void }) => (
    <button onClick={handleClick}>点击我</button>
);

const AuthenticatedButton = withAuth(MyButton);

const App: React.FC = () => {
    return (
        <div>
            <AuthenticatedButton userRole="admin" />
        </div>
    );
};

export default App;

在这个例子中,requireRole 装饰器在 handleClick 方法上进行权限验证,withAuth 高阶组件将权限验证逻辑整合到 React 组件中,使得只有具有 admin 权限的用户点击按钮时,handleClick 方法才会执行。

与后端服务交互时的权限验证

在前后端分离的应用中,后端服务也需要进行权限验证。假设我们使用 Node.jsExpress 框架。 首先,安装所需依赖:

npm install express

然后,编写后端代码:

import express from 'express';
const app = express();
app.use(express.json());

function requireRole(requiredRole: string) {
    return function (req: any, res: any, next: any) {
        const userRole = req.headers['user - role'];
        if (userRole === requiredRole) {
            next();
        } else {
            res.status(403).send('权限不足');
        }
    };
}

app.get('/admin - only', requireRole('admin'), (req, res) => {
    res.send('这是只有admin能访问的内容');
});

const port = 3000;
app.listen(port, () => {
    console.log(`服务器在端口 ${port} 上运行`);
});

在这个后端代码中,requireRole 函数返回一个中间件函数,用于验证请求头中的 user - role 是否符合要求。如果权限不足,返回403状态码。

权限验证装饰器的优化与注意事项

性能优化

  1. 缓存验证结果:如果权限验证的条件不经常变化,可以考虑缓存验证结果。例如,对于一些静态权限配置,可以在装饰器初始化时进行一次验证,并缓存结果,避免每次方法调用都进行验证。
  2. 减少不必要的计算:在权限验证逻辑中,尽量减少复杂的计算。如果可能,将复杂的权限判断逻辑提前计算好,或者将其移到初始化阶段,而不是在每次方法调用时都重新计算。

错误处理

  1. 统一错误处理:在权限验证失败时,应该有统一的错误处理机制。可以通过抛出特定的错误类型,然后在全局的错误处理中间件中进行处理,这样可以保证错误处理的一致性。
  2. 记录错误日志:在权限验证失败时,记录详细的错误日志,包括请求的方法、用户角色、失败原因等信息,以便于排查问题。

兼容性和可移植性

  1. 装饰器提案版本:TypeScript 装饰器基于 ECMAScript 装饰器提案,不同的运行环境可能对装饰器的支持有所不同。确保在目标运行环境中装饰器能够正常工作,并且注意装饰器提案的版本兼容性。
  2. 跨平台使用:如果计划在多个平台(如前端、后端)使用相同的权限验证装饰器,要确保装饰器的代码能够在不同的平台上运行,避免使用特定平台的 API 或特性。

安全性

  1. 防止绕过验证:确保权限验证装饰器的逻辑不能被轻易绕过。例如,在前端代码中,不能通过简单的修改 DOM 或调用内部方法来绕过权限验证,所有关键的权限验证逻辑应该放在后端进行。
  2. 数据验证:在权限验证过程中,对输入的数据进行严格验证,防止恶意用户通过构造恶意数据来绕过权限验证。

总结

通过使用TypeScript装饰器进行权限验证,我们可以有效地将权限验证逻辑与业务逻辑分离,提高代码的可维护性和复用性。从简单的方法级权限验证到复杂的结合依赖注入、前端框架和后端服务的场景,装饰器都能发挥重要作用。在实际应用中,我们还需要注意性能优化、错误处理、兼容性和安全性等方面,以确保权限验证机制的高效和可靠。希望本文介绍的内容能帮助你在项目中更好地实现权限验证功能。